Saltar para o conteúdo principal
Agora o núcleo: uma função lógica que carrega um modelo e um registro, preenche os espaços reservados e salva um novo documento . Vamos escrever a lógica do negócio uma vez na forma de manipulador e então expô-la através de vários gatilhos. Este capítulo conecta duas delas — uma ferramenta de IA e uma ação de fluxo de trabalho.

O auxiliar de renderização

Mantenha uma lógica pura em seu próprio arquivo, para que seja fácil de testar unidades. Este encolher os tokens de um record em {{dot.path}} e substitui-los.
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] };
};
Como este arquivo não tem efeitos colaterais, você pode resolvê-lo com testes de unidade (yarn test:unit). Ver Testing.

O manipulador

O manipulador usa o CoreApiClient para ler e escrever dados de CRM. Ele carrega o modelo, carrega o registro de destino, preenche o corpo e cria um documento.
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 executa uma consulta diferente para uma Pessoa vs. Uma Empresa e flattens o resultado — veja load-record-values.ts.

Expo-na como uma ferramenta e uma ação de fluxo de trabalho

Uma única defineLogicFunction pode carregar vários gatilhos. Aqui, toolTriggerSettings faz com que seja chamável por agentes IA e workflowActionTriggerSettings o transforma em passo do construtor de fluxo de trabalho. Ambos descrevem suas informações com um esquema 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,
});
O esquema de entrada é um esquema JSON simples que descreve templateId e recordId — vê generate-document-input.schema.ts.

Concede-o acesso

Funções lógicas são executadas como o papel do aplicativo. Ele precisa ler templates e registrar e criar documentos, então permitir que em 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 permite que a função envie o PDF gerado na próxima seção. Ver Roles para permissões refinadas.

Anexar arquivo PDF real

Um campo de texto renderizado é útil, mas os usuários querem um documento real. Vamos gerar um PDF e armazená-lo no registro como um arquivo para download. Primeiro, dê ao objeto documento um campo ARQUIVO para segurar o PDF. Aplicativos enviam para seus próprios campos de arquivos, então este campo é o que encaminha o upload:
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
Agora renderize esse PDF. Uma app é um projeto Node real, então você pode adicionar qualquer pacote npm que você precisa e importá-lo como em qualquer outro lugar. Nós usamos pdf-lib para desenhar o PDF e marked para analisar o corpo do Markdown — a CLI os instala no tempo de execução da função para você:
yarn add pdf-lib marked
O auxiliar completo é generate-document-pdf.ts. Ele analisa o Markdown em tokens com marcado. exer, então as envia com pdf-lib: títulos reais, execuções bold/italic, listas de marcadores e numerados, bloqueios e regras — uma renderização A4 polida e multi-página do modelo em si, ao invés de uma parede de texto.
Um PDF gerado polido e comercializável
As fontes internas do pdf-lib usam codificação WinAnsi, portanto os acentos da Europa Ocidental são exibidos por padrão; o utilitário mapeia aspas tipográficas e travessões e descarta caracteres que não consegue codificar. Renderizar scripts não-latinos (chinês, árabe, cirílico) significaria que incorporando uma fonte Unicode.
Em seguida, carregue-a e armazene a referência no registro. Rotas uploadFile bytes para o campo de arquivos de propriedade; o id retornado é o que você salva:
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,
  },
});
O documento gerado agora carrega um PDF:
Um registro de documento com um arquivo PDF gerado
uploadFile destina-se apenas a campos de arquivos propriedade do aplicativo (então o upload sempre requer um aplicativo que possui o campo, mais o sinalizador de papéis UPLOAD_FILE). É por isso que o PDF fica no campo file do próprio registro - o mesmo padrão que o call-recorder app usa para gravações.
Após este passo: cada documento gerado possui um PDF real e para download. Mas nada pode chamar o gerador a partir da interface de usuário — por isso precisamos de uma rota HTTP.

Próximo: Rotas HTTP →

Servir a função sobre HTTP e renderizar documentos como páginas da web.