i18n - docs translations (#22617)

Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-07-07 11:54:54 +02:00
committed by GitHub
parent 07a921f8ca
commit 18ca89bcdd
97 changed files with 13247 additions and 0 deletions
@@ -0,0 +1,80 @@
---
title: 5. An AI agent
icon: robot
description: Laissez un agent générer des documents à partir d'un chat, à l'aide de votre outil.
---
Parce que `generate-document` est exposé comme un **outil**, un agent AI peut l'appeler.
Ajoutons un agent et une compétence pour que les utilisateurs puissent simplement dire *"générer une proposition pour
Jeffery Griffin"*.
## La compétence
Un [skill](/l/fr/developers/extend/apps/logic/skills-and-agents) est des instructions réutilisables
— des connaissances que vous attachez aux agents. Le nôtre enseigne au modèle comment utiliser
l'outil.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## L'agent
Un [agent](/l/fr/developers/extend/apps/logic/skills-and-agents) paie une invite avec un modèle
. Définissez explicitement `responseFormat` pour éviter un avertissement de construction.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
L'agent ne peut appeler l'outil que si son rôle le permet. Nous avons déjà défini
`canAccessAllTools: true` et `canBeAssignedToAgents: true` sur le rôle de l'application dans
[Chapitre 2](/l/fr/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Essayez-le
Ouvrez une conversation avec **Document Assistant** et demandez-lui de rédiger un document pour une personne dans votre CRM. Il trouve l'enregistrement, appelle `generate-document`, et rapporte
le document qu'il a créé — qui apparaît maintenant dans votre vue **Documents**
exactement comme les chemins du menu de commande et du workflow.
C'est le payoff de l'exposition de la logique en tant qu'outil : **une fonction, de nombreuses portes frontales** — Menu de commande
, HTTP, étape de workflow et maintenant langage naturel.
**Après cette étape:** l'application est complète et vraiment utile. Il est temps de
lexpédier.
<Card title="Suivant : publication →" icon="rocket" href="/fr/developers/extend/apps/tutorials/document-generator/publishing">
Ajouter des métadonnées de marketplace et publier.
</Card>
@@ -0,0 +1,304 @@
---
title: 4. Construire l'interface utilisateur
icon: table-columns
description: Vues, navigation dans la barre latérale, une commande et des composants frontaux.
---
Pour le moment, les objets ne sont accessibles que dans les paramètres. Donnons à l'application une présence
réelle dans l'interface utilisateur : vues de la liste, entrées de la barre latérale,
**Générer un document** en un clic, un composant frontal de la page d'enregistrement pour **prévisualiser** un document
et un onglet d'onglet de texte natif **éditeur** pour les modèles.
## Vues et navigation
Une [vue](/l/fr/developers/extend/apps/layout/views) est une liste enregistrée dun objet donné.
Un [élément du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items)
place cette vue dans la barre latérale.
```ts filename="src/views/documents.view.ts"
import { defineView, ViewKey } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
key: ViewKey.INDEX,
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Ajouter la même paire pour les modèles. Les deux affichent maintenant dans la barre latérale :
<Frame caption="Documents et gabarits dans la barre latérale, avec le document généré listé.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Vue des documents avec un document généré" />
</Frame>
## Un composant frontal
Un [composant frontal](/l/fr/developers/extend/apps/layout/front-components) est un composant React
bac à sable à l'intérieur de Twenty. Notre lecture de l'enregistrement sélectionné, charge les gabarits
par l'intermédiaire de `CoreApiClient`, et POSTs vers la route à partir du dernier chapitre
.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const apiBaseUrl = process.env.TWENTY_API_URL;
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ templateId, recordId }),
}).then((r) => r.json());
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Style avec des variables CSS en ligne (`var(--t-color-blue)`), pas de valeurs importées de
`21ui`. Les mocks SDK que ce paquet pendant la compilation, donc les importations au niveau des modules de constantes de thème
seraient `indéfinies`. Voir le
[composant complet](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Une commande pour l'ouvrir
Un [lien de menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) avec
`availabilityType: 'RECORD_SELECTION'` s'affiche quand une personne est sélectionnée, et
ouvre le composant dans le panneau latéral.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Essayer tout le flux
Ouvrez **People**, cochez une personne et appuyez sur <kbd>ΩK</kbd> / <kbd>Ctrl K</kbd>.
"Générer un document" apparaît, marqué avec votre application :
<Frame caption="La commande s'affiche lorsqu'une personne est sélectionnée.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu de commande avec Générer un document" />
</Frame>
Exécutez-le — votre composant s'ouvre dans le panneau latéral. Choisissez un modèle, cliquez sur
**Générer**, et un nouveau record se trouve dans **Documents**.
<Frame caption="Le composant avant, le chargement des modèles et la génération au clic.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Générer le panneau latéral du document" />
</Frame>
Chaque document généré enregistre votre application comme auteur:
<Frame caption="Créé par le générateur de documents, statut généré.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Un enregistrement de document généré" />
</Frame>
## Aperçu d'un document sur sa page de dossier
Un composant frontal nest pas seulement destiné aux menus de commandes — vous pouvez en monter un comme **onglet sur une page denregistrement**. Ajoutons un onglet *Aperçu* à l'enregistrement du document qui rend le corps
Markdown comme une page lisse et imprimable.
Le composant lit l'id de l'enregistrement courant depuis son contexte d'exécution, charge le document
et le rendu. Les composants frontaux s'exécutent dans un **sandbox** qui n'autorise qu'une liste
de balises HTML — l'injection HTML brute (`dangerouslySetInnerHTML`) et
`\<style>` sont bloqués — donc nous rendons le Markdown comme des éléments React avec des styles
en ligne via un petit [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Montez-le avec une [mise en page de la page] (/developers/extend/apps/layout/page-layouts). Une mise en page
`RECORD_PAGE` ajoute des onglets à la vue d'un enregistrement d'un objet ; un widget `FRONT_COMPONENT`
dans un onglet `CANVAS` héberge le composant:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Ouvrez n'importe quel document — un onglet **Aperçu** le rend magnifiquement, avec des liens vers la page
et le PDF :
<Frame caption="L'onglet Aperçu affiche le document avec des styles en ligne, plus des liens rapides.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Composant frontal de la visionneuse de documents dans un onglet de page d'enregistrement" />
</Frame>
## Modifier un modèle avec l'éditeur de texte riche
Les gabarits n'ont pas du tout besoin d'un composant personnalisé. Parce que le `body` est un champ
`RICH_TEXT`, Vingt fournissent déjà un éditeur de texte complet — le
est le même que les objets standard Note et Tâche. Nous nous contentons de le mettre en surface sur la page d'enregistrement du gabarit
.
Ajouter un onglet avec un widget `FIELD` en mode d'affichage `EDITOR`, pointant vers le champ `body`
via `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Un champ `RICH_TEXT` stocke à la fois le bloc JSON de l'éditeur et une projection Markdown
. Le pipeline de génération lit que Markdown projection, donc
placeholders, le PDF, et la page web partageable fonctionnent tous de façon inchangée —
voir
[`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Maintenant, les éditeurs écrivent des modèles dans un éditeur de texte riche approprié:
<Frame caption="Onglet Modèle : l'éditeur natif de texte riche de Vingt lié au champ corps.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Enregistrement du modèle avec l'onglet de l'éditeur natif de texte riche" />
</Frame>
**Après cette étape:** les documents d'aperçu et les modèles sont modifiables
dans l'application. Ensuite, laissez un agent IA les générer depuis un chat.
<Card title="Suivant : un agent IA →" icon="robot" href="/fr/developers/extend/apps/tutorials/document-generator/ai-agent">
Ajoutez un agent et une compétence qui appellent votre outil.
</Card>
@@ -0,0 +1,135 @@
---
title: 1. Modèle de données
icon: database
description: Modèles de documents et de modèles avec des objets, des champs et une relation.
---
Notre application a besoin de deux objets personnalisés : **modèles de documents** (quoi écrire) et
**documents** (le résultat généré). Définissons-les.
Échappez chaque fichier d'entité avec le CLI — il génère un UUID valide et le dossier
correct pour vous :
```bash filename="Terminal"
yarn twenty dev:add object
```
Ci-dessous nous montrons les fichiers finis.
<Note>
Chaque constante `*_UNIVERSAL_IDENTIFIER` vit dans
`src/constants/universal-identifiers.ts` et est importée quand elle est utilisée. Les snippets
ci-dessous omettent ces importations par souci de brièveté — conservez-les dans vos propres fichiers.
</Note>
## L'objet modèle
Un modèle a un `nom`, un `body` avec `{{placeholders}}`, et un `target` que
dit s'il est écrit pour une personne ou une entreprise. Le `body` est un champ
`RICH_TEXT`. Vingt lui donne donc un éditeur de texte complet.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
L'option `SELECT` **values** doit être `UPPER_CASE` (`PERSON`, pas `person`), et le
`defaultValue` est enveloppé par des guillemets supplémentaires : `` `'PERSON'` ``. Le `label` est ce que les utilisateurs de
voient.
</Warning>
## L'objet du document
Le document généré stocke le contenu `content` et un `status`. Définissez
de la même manière, avec un `status` de `DRAFT` / `GENERATED`. Fichier complet :
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Les lier avec une relation
Chaque document devrait revenir au modèle dont il provient. Les relations sont
**bidirectionnelles** — vous définissez les deux côtés, chacun dans son propre fichier de champs.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
L'autre côté (`template-documents-relation.field.ts`) est un champ
`RelationType.ONE_TO_MANY` nommé `documents` qui pointe dans la direction opposée.
Voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle complet.
## Voyez-la en 20
Avec `yarn twenty dev` en cours d'exécution, ouvrez **Paramètres → Modèle de données**. Les deux objets
apparaissent, taggés avec votre application.
<Frame caption="Les deux objets personnalisés, détenus par l'application Générateur de documents.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Réglages du modèle de données montrant les modèles de documents et de documents" />
</Frame>
Créez un modèle avec lequel tester — nommez-le *proposition de vente*, définissez **Cible** à
*Personne*, et collez un corps avec quelques marqueurs :
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Un enregistrement de gabarit. Le corps garde ses espaces réservés jusqu'à ce qu'un document soit généré.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Un enregistrement de modèle de proposition de vente avec le corps du placeholder" />
</Frame>
**Après cette étape :** vous avez des objets `documentTemplate` et `document`, liés par
par une relation, et un modèle à générer. Ensuite, la logique qui le remplit.
<Card title="Suivant : génération de documents →" icon="bolt" href="/fr/developers/extend/apps/tutorials/document-generator/generating-documents">
Écrit la fonction logique qui remplit le modèle.
</Card>
@@ -0,0 +1,239 @@
---
title: 2. Génération des documents
icon: bolt
description: 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 dentre eux — un **outil dIA** et une
**action de workflow**.
## 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"
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"
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 Person par rapport à une Company 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"
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
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"
{
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"
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="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF brossé et commercialisable généré" />
</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
dinté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"
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="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Un enregistrement de document avec un fichier PDF généré" />
</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>
@@ -0,0 +1,148 @@
---
title: 3. Routes HTTP
icon: globe
description: Déclencher la fonction sur HTTP et afficher les documents en tant que pages Web.
---
Le même gestionnaire peut également répondre aux requêtes HTTP. Nous allons ajouter deux itinéraires :
* un point de terminaison **POST** que l'interface utilisateur appelle pour générer un document, et
* un point de terminaison public **GET** qui rend un document en tant que page web imprimable.
Les deux utilisent `httpRouteTriggerSettings`. Les routes des applis sont servies dans `/s` sur votre
Serveur Vingt (par exemple `http://localhost:2020/s/documents/generate`).
## Itinéraire POST — générer à la demande
Ceci réutilise `generateDocumentHandler`, donc il n'y a pas de logique à répéter — juste un adaptateur
mince qui lit le corps de la requête.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
Le gestionnaire partagé renvoie un `status` suggéré en cas d'échec, donc la route peut
répondre avec un code approprié `4xx`/`5xx`. `isAuthRequired: true` signifie que l'appelant
doit présenter un jeton valide — le composant frontal dans le chapitre suivant passe automatiquement le jeton d'accès de l'utilisateur
.
## Obtenir la route - afficher en tant que page web
Pour retourner du HTML au lieu de JSON, enveloppez le corps dans un en-tête `Response` avec un
`Content-Type`. Cette route est publique (`isAuthRequired: false`) donc un document généré par
peut être partagé comme un lien.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` rend le corps de Markdown en HTML (avec [marked](https://marked.js.org/),
assaini et le dépose dans une netteté page imprimable qui ne montre que le contenu du modèle
— la même apparence que le PDF et l'aperçu dans l'application.
[Voir l'aide](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Essayez-le
Avec un modèle et une personne dans votre espace de travail, appelez la route (récupérez un jeton à partir de
**Paramètres → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Ouvrez le document retourné dans votre navigateur :
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="L'itinéraire public GET rend le document en tant que page imprimable.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Une page web de document rendu" />
</Frame>
<Tip>
Vous pouvez également diffuser les logs d'une fonction en testant avec
`yarn twenty dev:function:logs`, ou l'appeler directement avec
`yarn twenty dev:function:exec`.
</Tip>
**Après cette étape :** l'application peut générer des documents via HTTP et les servir comme
pages web. Maintenant, rendons-le utilisable sans `curl`.
<Card title="Prochaine étape : construire l'interface utilisateur →" icon="table-columns" href="/fr/developers/extend/apps/tutorials/document-generator/building-the-ui">
Vues, navigation, commande et composant frontal.
</Card>
@@ -0,0 +1,70 @@
---
title: "Tutoriel : Générateur de documents"
icon: wand-magic-sparkles
description: Créez une véritable application Twenty qui génère des documents personnalisés à partir des données de votre CRM.
---
Dans ce tutoriel, vous allez créer **Document Generator** — une application qui transforme des modèles réutilisables
en documents personnalisés en utilisant les données déjà présentes dans votre CRM.
Rédigez un modèle une seule fois avec `{{placeholders}}`, puis générez un document rempli
pour nimporte quelle personne ou entreprise en un clic — depuis le menu de commande, depuis un
agent IA ou depuis un workflow.
<Frame caption="Un modèle, généré pour une personne spécifique, ouvert comme une page imprimable.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Un document de proposition commerciale généré" />
</Frame>
## Ce que vous apprendrez
Chaque chapitre ajoute une capacité. À la fin, vous aurez utilisé la plupart du SDK.
| Chapitre | Capacité | Référence |
| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [1. Modèle de données](/l/fr/developers/extend/apps/tutorials/document-generator/data-model) | Objets, champs et une relation | [Données](/l/fr/developers/extend/apps/data/overview) |
| [2. Générer des documents](/l/fr/developers/extend/apps/tutorials/document-generator/generating-documents) | Une fonction logique (outil IA + action de workflow) qui remplit un modèle Markdown et y joint un PDF soigné | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| [3. Routes HTTP](/l/fr/developers/extend/apps/tutorials/document-generator/http-routes) | Servir du JSON et une page HTML partageable depuis des routes | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| [4. Créer linterface utilisateur](/l/fr/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vues, navigation, menu de commandes et composants frontaux qui prévisualisent un document et modifient un modèle | [Mise en page](/l/fr/developers/extend/apps/layout/overview) |
| [5. Un agent IA](/l/fr/developers/extend/apps/tutorials/document-generator/ai-agent) | Agent + compétence | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| [6. Publication](/l/fr/developers/extend/apps/tutorials/document-generator/publishing) | Livrez-la sur la place de marché | [Publication](/l/fr/developers/extend/apps/operations/publishing) |
## Prérequis
Vous devez avoir terminé le [démarrage rapide](/l/fr/developers/extend/apps/getting-started/quick-start) :
un serveur Twenty local en cours dexécution sur le port `2020` et le CLI authentifié dessus.
Sinon, générez-en un et lancez-le maintenant :
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Vous préférez lire le code finalisé ? Lapplication complète se trouve dans
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Chaque extrait ci-dessous en est tiré.
</Note>
## Comment lapplication sassemble
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Un modèle avec des espaces réservés est transformé en un document soigné avec un PDF, déclenché depuis le menu de commandes, un agent IA, un workflow ou un lien partageable" />
</Frame>
Vous rédigez un **modèle** une seule fois dans un éditeur de texte enrichi, avec `{{placeholders}}`. Choisir un
modèle et un enregistrement CRM remplit les espaces réservés et stocke un
**document** soigné (avec un fichier PDF). Tout le reste — le menu de commandes, lagent IA,
l’étape de workflow, le lien partageable — nest quune façon différente de déclencher ce
même générateur.
## Gardez cette boucle en marche
Laissez `yarn twenty dev` sexécuter dans un terminal pendant tout le tutoriel. Chaque fois que
vous ajoutez ou modifiez un fichier sous `src/`, il est resynchronisé avec votre serveur en quelques
secondes, afin que vous puissiez voir chaque capacité apparaître dans linterface utilisateur au fur et à mesure que vous la construisez.
<Card title="Commencez à construire →" icon="database" href="/l/fr/developers/extend/apps/tutorials/document-generator/data-model">
Chapitre 1 : modéliser des documents et des modèles.
</Card>
@@ -0,0 +1,137 @@
---
title: 6. Publication
icon: rocket
description: Ajoutez des métadonnées de marketplace et publiez votre application.
---
Votre application fonctionne. La dernière étape consiste à la décrire pour le marché et à la publier.
## Ajouter des métadonnées de marketplace
La [configuration de l'application](/l/fr/developers/extend/apps/config/application) porte l'identité
qui apparaît sur le marché : auteur, catégorie, logo et liens de support
. Mettez un logo dans `public/` et faites-le référence avec `logoUrl`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logoUrl: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/fr/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
Le rôle par défaut est déclaré avec `defineApplicationRole()` dans son propre fichier — vous
ne passez plus ici `defaultRoleUniversalIdentifier`.
</Tip>
Ajoute également le mot-clé `21app` à `package.json` pour que l'application soit découverte:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Ajouter des captures d'écran de la galerie
Une annonce boursière se vend avec des captures d'écran. Déposez quelques PNGs dans
`public/gallery/` et référencez-les avec `screenshots` — ils apparaissent comme une galerie
sur la page de liste.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
screenshots: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Menez avec le gain : faire la première capture d'écran le résultat fini (un document
généré), puis montrez comment il est déclenché et créé. Utilisez des captures
à haute résolution - c'est la première chose qu'un utilisateur voit.
</Tip>
Donnez le même traitement à `README.md` - c'est la première page sur npm et GitHub.
Ouvrez avec la proposition de valeur et une capture d'écran, listez les fonctionnalités du titre,
puis gardez les détails de construction sous le pli.
## Vérifiez avant d'expédier
Exécuter les mêmes portes CI :
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty dev --once --dry-run # preview the metadata diff
```
La course à sec imprime exactement ce qui pourrait changer sur le serveur sans l'appliquer —
une bonne vérification de l'état d'esprit. Voir
[Testing](/l/fr/developers/extend/apps/operations/testing) et
[Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery).
## Publier
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` construit et publie sur npm par défaut; `--private` télécharge une archive
vers un registre privé du serveur Vingt à la place. Pour faire surface à une application publiée
dans une instance, déclenchez une synchronisation de catalogue :
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Tous les détails et la liste de vérification de la version :
[Publishing](/l/fr/developers/extend/apps/operations/publishing).
## Vous avez construit une application 🎉
Dans six chapitres, vous avez utilisé la plupart des surfaces du SDK :
* **Objets, champs et relations** pour modéliser les données
* Une **fonction logique** exposée comme un **outil IA**, une **action de workflow**, et des **routes HTTP**
* **Voires, navigation, une commande et un composant frontal** pour l'interface utilisateur
* Un **agent + compétence** pour la génération de langage naturel
* **Métadonnées du Marketplace** et le flux de publication
L'application terminée est à
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Où aller ensuite
<CardGroup cols={2}>
<Card title="Référence des données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Chaque type de champ, chaque relation et chaque option d'index.
</Card>
<Card title="Référence logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Cron et les déclencheurs d'événements de base de données, le stockage de valeurs clés, les connexions OAuth.
</Card>
<Card title="Référence de mise en page" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Mise en page des pages, widgets du tableau de bord et plus de surfaces de l'interface utilisateur.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, télécommandes et CI.
</Card>
</CardGroup>