> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2. Génération des documents

> Une fonction logique, exposée comme un outil AI et une action de workflow.

Maintenant le cœur : une [fonction logique](/l/fr/developers/extend/apps/logic/logic-functions)
qui charge un modèle et un enregistrement, remplit les marqueurs et enregistre un nouveau document
.

Nous allons écrire la logique commerciale une fois en tant que **gestionnaire**, puis l'exposer à travers
plusieurs déclencheurs. Ce chapitre connecte deux d’entre eux — un **outil d’IA** et une
**action de flux de travail**.

## L'assistant de rendu

Gardez une logique pure dans son propre fichier donc il est facile de le tester. Cela aplanit un record
en jetons `{{dot.path}}` et les substitue.

```ts filename="src/logic-functions/utils/render-template.ts" theme={null}
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] };
};
```

<Tip>
  Comme ce fichier n'a pas d'effets secondaires, vous pouvez le couvrir avec des tests unitaires rapides
  (`yarn test:unit`). Voir [Testing](/l/fr/developers/extend/apps/operations/testing).
</Tip>

## Le gestionnaire

Le gestionnaire utilise le [`CoreApiClient`](/l/fr/developers/extend/apps/logic/logic-functions)
généré pour lire et écrire les données CRM. Il charge le modèle, charge l'enregistrement cible, remplit
le corps, et crée un `document`.

```ts filename="src/logic-functions/handlers/generate-document-handler.ts" theme={null}
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` exécute une requête différente pour une personne par rapport à une entreprise et aplatit
le résultat — voir
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).

## Exposer comme un outil et une action de workflow

Une seule `defineLogicFunction` peut contenir plusieurs déclencheurs. Ici, `toolTriggerSettings`
le rend appelable par les agents AI, et `workflowActionTriggerSettings` le transforme en une étape
dans le constructeur de workflow visuel. Les deux décrivent leur entrée avec un schéma JSON.

```ts filename="src/logic-functions/generate-document.ts" theme={null}
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,
});
```

Le schéma d'entrée est un schéma JSON simple décrivant `templateId` et `recordId` —
voir [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).

## Accorder l'accès

Les fonctions logiques s'exécutent en tant que rôle de l'application. Il a besoin de lire les modèles et les enregistrements
et de créer des documents, donc permettez cela dans `src/roles/default-role.ts`:

```ts theme={null}
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` permet à la fonction d'envoyer le PDF généré dans la section suivante.
Voir [Roles](/l/fr/developers/extend/apps/config/roles) pour des autorisations plus fines.

## Joindre un vrai fichier PDF

Un champ de texte rendu est utile, mais les utilisateurs veulent un vrai document. Nous allons générer un
**PDF** et le stocker dans l'enregistrement en tant que fichier téléchargeable.

Premièrement, donnez à l'objet `document` un champ `FILES` pour contenir le PDF. Les applications téléchargent
dans leurs champs **propres** de fichiers, donc ce champ permet de router le téléchargement:

```ts filename="src/objects/document.object.ts" theme={null}
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
```

Maintenant, renvoie ce PDF. Une application est un vrai projet Node, vous pouvez donc ajouter n'importe quel package npm
dont vous avez besoin et l'importer n'importe où ailleurs. Nous utilisons **[pdf-lib](https://pdf-lib.js.org/)**
pour dessiner le PDF et **[marked](https://marked.js.org/)** pour analyser le corps du Markdown
— le CLI les installe dans le runtime de la fonction pour vous :

```bash filename="Terminal" theme={null}
yarn add pdf-lib marked
```

L'aide complète est
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Il analyse le Markdown en jetons avec `marked. exer`, puis les pose avec
pdf-lib: de vraies rubriques, **gras**/*italique* exécute, puces et listes numérotées,
blockquotes et règles — un rendu A4 polyvalent et polyvalent du modèle
lui-même, plutôt qu'un mur de texte.

<Frame caption="Le PDF généré : typographie réelle et mise en forme Markdown, rendant le corps du modèle.">
  <img src="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=ab29f3ed244aa424e850abe73f8c082d" alt="Un PDF brossé et commercialisable généré" width="1286" height="1200" data-path="images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" />
</Frame>

<Note>
  Les polices intégrées de pdf-lib's utilisent l'encodage WinAnsi, de sorte que les accents WesternEuropean rendent
  hors de la boite; l'aide mappe les guillemets intelligents et les tirets et drops les caractères qu'elle
  ne peut pas encoder. Le rendu de scripts non latins (chinois, arabe, cyrillique) impliquerait
  d’intégrer une police Unicode.
</Note>

Ensuite téléchargez-le et stockez la référence sur l'enregistrement. `uploadFile` achemine les octets
vers votre champ de fichiers appartenant à l'application; le `id` retourné est ce que vous sauvegardez:

```ts filename="src/logic-functions/handlers/generate-document-handler.ts" theme={null}
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,
  },
});
```

Le document généré possède maintenant un PDF téléchargeable:

<Frame caption="Le PDF généré, stocké dans le champ Fichier du document.">
  <img src="https://mintcdn.com/twenty/sqJBeTZq-W-RDBPU/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png?fit=max&auto=format&n=sqJBeTZq-W-RDBPU&q=85&s=afbc1091577e93482b71dcc0d73af110" alt="Un enregistrement de document avec un fichier PDF généré" width="2880" height="1800" data-path="images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" />
</Frame>

<Note>
  `uploadFile` ne cible que les champs de fichiers **app-owned** (donc les téléchargements nécessitent toujours une application
  qui possède le champ, plus le paramètre `UPLOAD_FILE`). C'est pourquoi le PDF
  se trouve sur le propre champ `file` de l'enregistrement — le même motif que l'application
  [call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
  utilise pour les enregistrements.
</Note>

**Après cette étape:** chaque document généré a un PDF réel, téléchargeable. Mais
rien ne peut *appeler* le générateur de l'interface utilisateur — pour cela nous avons besoin d'une route HTTP.

<Card title="Suivant : Routes HTTP →" icon="globe" href="/fr/developers/extend/apps/tutorials/document-generator/http-routes">
  Servez la fonction via HTTP et rendez les documents en tant que pages Web.
</Card>
