メインコンテンツへスキップ
コア: ロジック関数 テンプレートとレコードをロードし、プレースホルダを埋め、新しい ドキュメントを保存します。 ハンドラとしてビジネスロジックを記述し、 いくつかのトリガーを通してそれを公開します。 この章では、AI ツールワークフローアクション の 2 つを配線します。

レンダリングヘルパー

単体テストを簡単に行えるように、純粋なロジックを独自のファイルに保存します。 これにより、レコード を {{dot.path}} トークンに平坦化し、それらを置き換えます。
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;

export const renderTemplate = (body: string, values: Record<string, string>) => {
  const missingTokens = new Set<string>();
  const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
    const value = values[token];
    if (value === undefined || value === '') { missingTokens.add(token); return ''; }
    return value;
  });
  return { content, missingTokens: [...missingTokens] };
};
副作用がないため、高速ユニットテスト (yarn test:unit) でカバーできます。 Testing を参照してください。

The handler

ハンドラは生成された CoreApiClient を使用してCRMデータを読み書きします。 テンプレートをロードし、ターゲットレコードをロードし、 本文を埋め、documentを作成します。
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';

export const generateDocumentHandler = async (
  input: { templateId: string; recordId: string },
) => {
  const client = new CoreApiClient();

  // Use a filtered list query, not the singular lookup: the singular query
  // throws when nothing matches, which would become a 500 instead of a 404.
  const { documentTemplates } = await client.query({
    documentTemplates: {
      __args: { filter: { id: { eq: input.templateId } }, first: 1 },
      edges: { node: { id: true, name: true, body: true, target: true } },
    },
  });
  const documentTemplate = documentTemplates?.edges?.[0]?.node;
  if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };

  const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
  if (!record.found) return { success: false, status: 404, message: 'Record not found.' };

  const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);

  const { createDocument } = await client.mutation({
    createDocument: {
      __args: { data: {
        name: `${documentTemplate.name}${record.displayName}`,
        content, status: 'GENERATED', templateId: documentTemplate.id,
      } },
      id: true, name: true,
    },
  });

  return { success: true, documentId: createDocument.id, content, missingTokens };
};
loadRecordValuesはPerson vs.a Company に対して異なるクエリを実行し、 結果を平坦化します。 load-record-values.tsを参照してください。

ツールとワークフローアクションとして公開する

一つの defineLogicFunction は複数のトリガーを持つことができます。 ここでは、toolTriggerSettings をAIエージェントから呼び出すことができ、workflowActionTriggerSettings をビジュアルワークフロービルダーの ステップに変換します。 どちらもJSONスキーマを使用して入力を記述します。
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';

export default defineLogicFunction({
  universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
  name: 'generate-document',
  description: 'Generate a document from a template and a CRM record.',
  timeoutSeconds: 30,
  toolTriggerSettings: {
    inputSchema: generateDocumentInputSchema,
  },
  workflowActionTriggerSettings: {
    label: 'Generate Document',
    icon: 'IconFileText',
    inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
    outputSchema: [{ type: 'object', properties: {
      success: { type: 'boolean' }, documentId: { type: 'string' },
    } }],
  },
  handler: generateDocumentHandler,
});
入力スキーマは templateIdrecordId を説明するプレーンな JSON スキーマです — は generate-document-input.schema.ts を参照してください。

アクセスを許可

ロジック関数はアプリのロールとして実行されます。 テンプレートとレコード を読み込み、ドキュメントを作成する必要があります。これは src/roles/default-role.ts で許可します。
export default defineApplicationRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: 'Document Generator default role',
  canReadAllObjectRecords: true,
  canUpdateAllObjectRecords: true,
  canAccessAllTools: true,
  canBeAssignedToAgents: true,
  permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
UPLOAD_FILEでは、次のセクションで生成されたPDFをアップロードできます。 詳細な権限については、 Roles を参照してください。

実際の PDF ファイルを添付する

レンダリングされたテキストフィールドは役に立ちますが、ユーザは実際のドキュメントを望んでいます。 PDFを生成して、ダウンロード可能なファイルとしてレコードに保存しましょう。 まず、document オブジェクトに FILES フィールドを指定し、PDFを保持します。 アプリはファイルフィールドに をアップロードするので、このフィールドはアップロードのルートとなります。
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
今度はそのPDFをレンダリングします。 アプリは本物の Node プロジェクトなので、必要な npm パッケージを自由に追加し、他の場所と同じようにインポートできます。 私たちは pdf-lib を使って PDF と marked を Markdown 本文を解析します。 — CLI はそれらを関数のランタイムにインストールします。
yarn add pdf-lib marked
フルヘルパーは generate-document-pdf.ts。 これは、Markdown を marked.lexer でトークンにパースし、その後 pdf-lib を使ってレイアウトします。実際の見出し、太字斜体 のラン、箇条書きおよび番号付きリスト、引用ブロックや罫線などを備え、単なるテキストの羅列ではなく、テンプレート自体を洗練された複数ページの A4 レイアウトとしてレンダリングします。
洗練された、市場性のある生成されたPDF
pdf-lib に組み込まれているフォントは WinAnsi エンコーディングを使用しているため、西ヨーロッパ言語のアクセント付き文字はそのままレンダリングされます。ヘルパーはスマートクォートやダッシュをマッピングし、エンコードできない文字は削除します。 ラテン語以外の文字 (中国語、アラビア語、キリル文字) をレンダリングすることは、 が Unicode フォントを埋め込むことを意味します。
次に、それをアップロードし、参照をレコードに保存します。 uploadFile は、あなたのアプリが所有するファイルフィールドにバイト をルートします。返されたidはあなたが保存したものです。
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';

const documentName = `${documentTemplate.name}${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';

const uploaded = await new MetadataApiClient().uploadFile(
  Buffer.from(bytes),
  fileName,
  'application/pdf',
  DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);

await client.mutation({
  updateDocument: {
    __args: {
      id: documentId,
      data: { file: [{ fileId: uploaded.id, label: fileName }] },
    },
    id: true,
  },
});
生成されたドキュメントには、ダウンロード可能な PDF が含まれています。
生成された PDF ファイルを持つドキュメント レコード
uploadFileapp-owned ファイル項目だけを対象にしています (アップロードには常にフィールドを所有する アプリとUPLOAD_FILE ロールフラグが必要です)。 そのため、PDF は file フィールド上に配置されています。録音には call-recorder app が使用するパターンと同じです。
このステップの後: それぞれの生成されたドキュメントには、実際のダウンロード可能なPDFがあります。 しかし、 はまだ UI からジェネレーターを呼び出すことはできません。そのために HTTP ルートが必要です。

次へ: HTTP ルート →

HTTP 経由で関数を提供し、ドキュメントを Web ページとして表示します。