snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
title: Configuration de l'application
description: Déclarez l'identité de votre application, son rôle par défaut, ses variables et ses métadonnées de la marketplace avec defineApplication.
icon: rocket
---
Chaque application doit avoir exactement un appel à `defineApplication`. Il déclare :
* **Identité** — identifiant universel, nom d'affichage, description.
* **Autorisations** — le rôle sous lequel s'exécutent ses fonctions logiques et ses composants front-end.
* **Variables** *(facultatif)* — paires clé–valeur exposées à votre code en tant que variables d'environnement.
* **Hooks de pré-installation / post-installation / désinstallation** *(facultatif)* — voir [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
Notes :
* Les champs `universalIdentifier` sont des identifiants déterministes que vous contrôlez. Générez-les une fois et conservez-les stables entre les synchronisations.
* `applicationVariables` deviennent des variables d'environnement pour vos fonctions et vos composants front-end. Dans les fonctions logiques (côté serveur), elles sont disponibles sous `process.env.VARIABLE_NAME`. Dans les composants front-end, utilisez `getApplicationVariable('VARIABLE_NAME')` depuis `twenty-sdk/front-component`. Les variables marquées avec `isSecret: true` sont uniquement injectées dans les fonctions logiques. Les composants front-end ne reçoivent que des variables non secrètes.
* Le rôle par défaut est détecté automatiquement à partir du fichier de rôle marqué avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) — vous n’avez pas besoin d’y faire référence depuis `defineApplication()`.
* Les fonctions de pré-installation, de post-installation et de désinstallation sont détectées automatiquement lors de la construction du manifeste — vous n'avez pas besoin de les référencer dans `defineApplication()`.
* Le passage explicite de `defaultRoleUniversalIdentifier` est toujours pris en charge pour des raisons de rétrocompatibilité, mais il est obsolète au profit de `defineApplicationRole()`.
* `serverVariables` sont des configurations et des secrets au niveau de l’instance (par exemple des clés d’API). Contrairement à `applicationVariables`, ils ne déclarent aucune valeur dans le manifeste — l’opérateur de l’espace de travail les renseigne dans les paramètres de l’application, et ils sont injectés dans les fonctions logiques uniquement une fois définis.
* Les deux types de variable acceptent `isDeprecated: true`. Utilisez-la pour mettre une variable hors service plutôt que de la supprimer : garder la clé déclarée préserve la valeur stockée (la supprimer détruit la valeur saisie par l’utilisateur), et la variable est toujours injectée, de sorte que votre code peut s’y rabattre — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Une variable dépréciée disparaît des paramètres de l'application lorsqu'elle n'a plus de valeur et n'est jamais prise en compte dans la vérification de la configuration de l'application, donc `isDeprecated` l'emporte sur `isRequired`.
* Pour afficher une interface utilisateur de configuration personnalisée dans l’onglet **Settings** de l’application (à la place de la section de configuration des variables par défaut), déclarez un composant frontal avec [`defineSettingsFrontComponent()`](/l/fr/developers/extend/apps/layout/front-components#custom-settings-component) dans son propre fichier. Un seul est autorisé par application. Les sections gérées par le système (mise à niveau automatique, App URL, connexions) restent toujours visibles.
## Types de variables
`applicationVariables` et `serverVariables` acceptent tous deux un `type` optionnel (et, pour `SELECT` / `MULTI_SELECT`, une liste `options`). Types pris en charge : `TEXT` (par défaut), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Le `type` affecte uniquement **la présentation et la validation** — il sélectionne le champ de saisie correspondant dans l’interface des paramètres de l’espace de travail (un bouton bascule, un champ numérique, une liste déroulante, un sélecteur de date, un éditeur JSON, …) et permet au build de valider votre configuration (par exemple, `SELECT` / `MULTI_SELECT` doivent déclarer des `options` non vides). Il ne change **pas** la façon dont la valeur atteint votre code.
Les valeurs sont **toujours injectées sous forme de chaînes de caractères** — cela est inhérent aux variables d’environnement (`process.env.*` est uniquement composé de chaînes). Lorsque votre fonction logique s’exécute, l’exécuteur sérialise chaque valeur selon son `type` déclaré lors de la construction de `process.env`, de sorte que le format de chaîne soit cohérent, quelle que soit la manière dont la valeur a été définie (valeur par défaut du manifeste, interface des paramètres ou version précédente) :
| Type | chaîne `process.env` |
| ------------------------------------- | ---------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | la valeur brute (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | chaîne décimale (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | tableau JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | objet JSON (`'{"retries":3}'`) |
Analysez la chaîne pour la convertir dans le type attendu :
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Il en va de même pour les composants front qui lisent les valeurs via `getApplicationVariable('VARIABLE_NAME')` — la valeur renvoyée est une chaîne ; analysez-la selon vos besoins.
## Rôle de fonction par défaut
Le rôle déclaré avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) contrôle ce à quoi les fonctions logiques de l’application et les composants front peuvent accéder :
* Les jetons d'exécution injectés dans vos fonctions logiques sont dérivés de ce rôle. Un appel agissant en tant que personne est davantage limité par ce que cette personne peut faire, de sorte qu'il ne peut jamais dépasser l'un ou l'autre. Voir [Les accès qu'un appel utilise](/l/fr/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Le client d'API typé est limité aux autorisations accordées à ce rôle.
* Appliquez le principe du moindre privilège : déclarez uniquement les autorisations dont vos fonctions ont besoin.
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut à `src/roles/default-role.ts`. Voir [Rôles et autorisations](/l/fr/developers/extend/apps/config/roles) pour la référence complète.
## Métadonnées de la marketplace
Si vous prévoyez de [publier votre application](/l/fr/developers/extend/apps/operations/publishing), ces champs optionnels contrôlent la façon dont elle apparaît dans la marketplace :
| Champ | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Nom de l'auteur ou de l'entreprise |
| `category` | Catégorie de l'application pour le filtrage dans la marketplace |
| `logo` | Chemin vers le logo de votre application empaqueté dans `public/` (p. ex., `public/logo.png`) |
| `galleryImages` | Tableau de chemins d’images de galerie empaquetées dans `public/` (p. ex., `public/screenshot-1.png`) |
| `aboutDescription` | Description markdown plus longue pour l'onglet "À propos". S'il est omis, la marketplace utilise le `README.md` du package sur npm |
| `websiteUrl` | Lien vers votre site web |
| `termsUrl` | Lien vers les conditions d'utilisation |
| `emailSupport` | Adresse e-mail du support |
| `issueReportUrl` | Lien vers le système de suivi des problèmes |
<Note>
`logoUrl` et `screenshots` sont des alias obsolètes de `logo` et `galleryImages`. Les URL absolues externes (`http://` ou `https://`) ne sont pas prises en charge pour ces champs : elles sont ignorées avec un avertissement au moment de la compilation. Placez plutôt les images dans le dossier `public/` de votre application.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hooks d'installation
description: Exécutez de la logique pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation — préremplissez des données, sauvegardez des enregistrements, validez la mise à niveau, nettoyez les ressources externes.
icon: wrench
---
Les hooks d'installation sont des fonctions logiques spéciales qui s'exécutent pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation. Ils partagent le même environnement d'exécution que les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) classiques, mais ils sont déclarés avec leurs propres fonctions de définition et ne relèvent pas du modèle de déclencheur habituel (HTTP, cron, événements de base de données). Les hooks d'installation reçoivent un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` est `undefined` lors d'une nouvelle installation) ; le hook de désinstallation reçoit un `UninstallPayload` (`{ version?: string }` — la version supprimée).
Chaque application peut définir **au maximum un seul** hook de chaque type (pré-installation, post-installation, désinstallation). La génération du manifeste renvoie une erreur si plus d'un hook de n'importe quel type est détecté.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## En un coup d'œil
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Exécutions | Avant la migration des métadonnées — le schéma et les données **précédents** sont toujours intacts | Après la migration et la génération du SDK — le **nouveau** schéma est en place |
| Exécution | Toujours synchrone ; bloque l'installation | Asynchrone par défaut (mis en file d'attente, 3 nouvelles tentatives) ; mode synchrone en option via `shouldRunSynchronously: true` |
| En cas d'échec | L'installation est **abandonnée** avant toute modification du schéma | Asynchrone : jusqu'à 3 réessais. Synchrone : l'appelant reçoit `POST_INSTALL_ERROR` (les modifications de schéma **ne** sont pas annulées) |
| Utilisation typique | Sauvegarder ou corriger les données qu'une migration ferait perdre ; refuser une mise à niveau risquée en levant une exception | Initialiser des données par défaut, configurer l'espace de travail, enregistrer des ressources externes |
**Règle empirique :** privilégiez post-install par défaut. Ne recourez à la pré-installation que lorsque la migration elle-même est destructive et que vous devez intercepter l'état précédent avant qu'il ne disparaisse.
| Vous souhaitez... | Utiliser |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Initialiser des données, configurer l'espace de travail, enregistrer des ressources externes | `post-install` |
| Travail de longue durée qui ne doit pas bloquer la réponse d'installation | `post-install` (mode asynchrone par défaut, avec nouvelles tentatives du worker) |
| Configuration rapide dont l'appelant dépend immédiatement après le retour de l'installation | `post-install` avec `shouldRunSynchronously: true` |
| Lire ou sauvegarder des données que la migration à venir ferait perdre | `pre-install` |
| Rejeter une mise à niveau qui corromprait des données existantes | `pre-install` (lancer une exception depuis le gestionnaire) |
| Réconciliation à chaque mise à niveau | N'importe quel hook avec `shouldRunOnVersionUpgrade: true` |
## Comportement partagé par les deux hooks
* La configuration est une configuration `defineLogicFunction` moins les paramètres de déclencheur, plus `shouldRunOnVersionUpgrade`.
* **Quand il s'exécute** : uniquement lors des nouvelles installations, par défaut. Définissez `shouldRunOnVersionUpgrade: true` pour qu'il s'exécute également lors des mises à niveau. Utilisez `previousVersion` / `newVersion` pour bifurquer selon le chemin de mise à niveau.
* **L'idempotence est importante** : le post-install asynchrone peut être relancé, et chaque hook est réexécuté lors des mises à niveau lorsque `shouldRunOnVersionUpgrade` est activé.
* L'environnement habituel des fonctions logiques (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) est injecté, ce qui vous permet d'appeler l'API Twenty avec le jeton de votre application.
* Le hook est rattaché automatiquement au manifeste de l'application au moment de la compilation (`preInstallLogicFunction` / `postInstallLogicFunction`) — rien à référencer dans [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* La valeur par défaut de `timeoutSeconds` est 300 pour permettre des tâches de configuration plus longues comme l'initialisation des données.
* **Non exécuté en mode dev** : `yarn twenty dev` ignore le flux d'installation et synchronise directement les fichiers, donc les hooks ne s'exécutent jamais dans ce cas. Déclenchez-les manuellement à la place :
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="S'exécute après l'application de la migration des métadonnées de l'espace de travail">
S'exécute une fois que votre application a terminé son installation : métadonnées synchronisées, client SDK généré, nouveau schéma interrogeable. Exemple — initialiser un enregistrement par défaut lors des nouvelles installations :
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Le drapeau `shouldRunSynchronously` contrôle le modèle d'exécution :
* `false` *(par défaut)* — mis en file d'attente dans la file de messages (`retryLimit: 3`) et exécuté par un worker. La réponse d'installation est renvoyée dès que la tâche est mise en file d'attente. **À utiliser pour les travaux de longue durée** — initialisation de grands ensembles de données, API tierces lentes.
* `true` — exécuté en ligne pendant le flux d'installation. La requête d'installation est bloquée jusqu'à ce que le gestionnaire ait terminé ; une erreur levée apparaît comme `POST_INSTALL_ERROR` pour l'appelant (aucune nouvelle tentative). **À utiliser pour les travaux rapides qui doivent être terminés avant la réponse.** La migration a déjà été appliquée à ce stade, donc un échec n'annule pas les modifications du schéma — il ne fait que remonter l'erreur.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="S'exécute avant l'application de la migration des métadonnées de l'espace de travail">
S'exécute avant la migration des métadonnées, sur le schéma **précédent** — l'endroit idéal pour sauvegarder des données qu'une migration ferait perdre, ou pour refuser une mise à niveau risquée. Avant l'exécution, le serveur effectue une « synchronisation réduite » purement additive qui enregistre uniquement la fonction de pré-installation de la nouvelle version ; tout le reste — les objets, champs et données de la version précédente — reste intact lorsque votre gestionnaire s'exécute.
La pré-installation est toujours **synchrone** et bloque l'installation. Si le gestionnaire lève une exception, l'installation est abandonnée avant toute modification du schéma — l'espace de travail reste sur la version précédente dans un état cohérent. C'est intentionnel : la pré-installation est votre dernière chance de refuser une mise à niveau risquée.
Exemple — copier les valeurs d'un champ hérité avant que la migration ne le supprime :
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## Hook de désinstallation
`defineUninstallLogicFunction` déclare un hook qui s'exécute lorsqu'un utilisateur désinstalle votre application. Il s'exécute **avant** que les métadonnées, les données et le code de l'application ne soient supprimés — une fois que la migration de suppression est exécutée, il ne reste plus rien à exécuter — de sorte que votre gestionnaire peut encore interroger les objets et enregistrements de l'application. Utilisez-le pour nettoyer les ressources externes : déprovisionner les ressources d'API, supprimer les bots restants, révoquer les webhooks.
Notes :
* Le hook fonctionne selon le principe du "best effort" : il s'exécute de manière synchrone, mais un échec est consigné dans les journaux et **ne bloque jamais la désinstallation** — le nettoyage ne doit pas rendre une application impossible à supprimer.
* Il reçoit `UninstallPayload` (`{ version?: string }` — la version supprimée).
* Il **ne** s'exécute **pas** lorsqu'une nouvelle installation ayant échoué est annulée — l'application n'a jamais terminé son installation.
* Le hook ne peut pas s'exécuter après la suppression de l'application, donc le nettoyage externe qui dépend des données de l'application (par exemple des identifiants de bots stockés dans des enregistrements) doit être effectué ici, et non dans une tâche planifiée externe.
* Comme les hooks d'installation, il **n'est pas exécuté en mode dev** — déclenchez-le manuellement à la place :
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Vue d'ensemble
description: Configurer l'application elle-même — son identité, ses autorisations par défaut et ce qui s'exécute au moment de l'installation.
icon: screwdriver-wrench
---
La **couche de configuration** d'une application Twenty est ce qui décrit l'application *à la plateforme* — son identité, les autorisations qu'elle détient et le code qui s'exécute lors de l'installation ou de la mise à niveau. Ces déclarations n'ajoutent pas de nouvelles structures de données ni de comportement à l'exécution ; elles indiquent à Twenty *qui est l'application* et *comment la configurer*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Configuration de l'application" icon="rocket" href="/l/fr/developers/extend/apps/config/application">
`defineApplication` — identité, rôle par défaut, variables, métadonnées de la marketplace.
</Card>
<Card title="Rôles et autorisations" icon="shield-halved" href="/l/fr/developers/extend/apps/config/roles">
`defineRole` — déclarez ce que les fonctions logiques de votre application peuvent lire et écrire.
</Card>
<Card title="Hooks d'installation" icon="wrench" href="/l/fr/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` et `defineUninstallLogicFunction` — sauvegarder les données, préremplir les valeurs par défaut, valider les mises à niveau, nettoyer lors de la suppression.
</Card>
</CardGroup>
## Comment les éléments s'articulent
* L'**application** est le point d'entrée. Chaque application comporte exactement un appel à `defineApplication()` qui pointe vers un **Rôle** par défaut.
* Le **Rôle** contrôle ce que les fonctions logiques et les composants frontaux de l'application peuvent lire et écrire. Suivez le principe du moindre privilège : accordez uniquement les autorisations dont votre code a réellement besoin.
* Les **hooks d'installation** s'exécutent lors de l'installation ou de la mise à niveau — la pré-installation avant la migration des métadonnées (ce qui permet de refuser une mise à niveau risquée), la post-installation après la migration (ce qui permet de préremplir les données par défaut selon le nouveau schéma). Le hook de désinstallation s’exécute juste avant que l’application ne soit supprimée, ce qui lui permet de nettoyer les ressources externes tant que les données de l’application sont encore lisibles.
<Note>
Les hooks d'installation partagent l'environnement d'exécution des [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) — même signature de handler, mêmes variables d'environnement, même client d'API typé — mais ils sont déclarés avec leurs propres fonctions `define` et vivent en dehors du modèle de déclencheurs habituel (HTTP, cron, événements de base de données).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Ressources publiques
description: Distribuez des fichiers statiques — images, icônes, polices — avec votre application via le dossier public/.
icon: folder-open
---
Le dossier `public/` à la racine de votre application contient des fichiers statiques — images, icônes, polices ou toute autre ressource dont votre application a besoin à l'exécution. Ces fichiers sont automatiquement inclus dans les builds, synchronisés pendant le mode de développement et téléversés vers le serveur.
Les fichiers placés dans `public/` sont :
* **Accessibles publiquement** — une fois synchronisés avec le serveur, les fichiers sont servis à une URL publique. Aucune authentification n'est nécessaire pour y accéder.
* **Disponibles dans les composants frontaux** — utilisez les URL des ressources pour afficher des images, des icônes ou tout média dans vos composants React.
* **Disponibles dans les fonctions logiques** — référencez des URL de ressources dans des e-mails, des réponses d'API ou toute logique côté serveur.
* **Utilisés pour les métadonnées de marketplace** — les champs `logo` et `galleryImages` dans `defineApplication()` référencent des fichiers de ce dossier (p. ex., `public/logo.png`). Celles-ci sont affichées dans la marketplace lorsque votre application est publiée. Les URL absolues externes sont ignorées pour ces champs — intégrez plutôt les images dans `public/`. Chaque image ne doit pas dépasser 10 Mo.
* **Synchronisés automatiquement en mode dev** — lorsque vous ajoutez, mettez à jour ou supprimez un fichier dans `public/`, il est automatiquement synchronisé avec le serveur. Aucun redémarrage nécessaire.
* **Inclus dans les builds** — `yarn twenty dev:build` regroupe toutes les ressources publiques dans la sortie de distribution.
## Accéder aux ressources publiques avec `getPublicAssetUrl`
Utilisez l'utilitaire `getPublicAssetUrl` de `twenty-sdk` pour obtenir l'URL complète d'un fichier dans votre répertoire `public/`. Cela fonctionne à la fois dans les **fonctions logiques** et les **composants frontaux**.
**Dans une fonction logique :**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**Dans un composant frontal :**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
L'argument `path` est relatif au dossier `public/` de votre application. Les deux `getPublicAssetUrl('logo.png')` et `getPublicAssetUrl('public/logo.png')` aboutissent à la même URL — le préfixe `public/` est supprimé automatiquement s'il est présent.

View File

@@ -0,0 +1,201 @@
---
title: Rôles et autorisations
description: Déclarez quels objets et champs les fonctions de logique et les composants frontaux de votre application peuvent lire et écrire.
icon: shield-halved
---
Un **rôle** est un ensemble d’autorisations : quels objets une application peut lire ou écrire, quels champs elle peut voir et quelles fonctionnalités de plateforme elle peut utiliser. Les fonctions de logique et les composants frontaux de chaque application héritent des autorisations du rôle marqué avec `defineApplicationRole()` (voir [Le rôle de fonction par défaut](#the-default-function-role) ci-dessous).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Sécurité au niveau des lignes
Les autorisations d’objet et de champ déterminent à *quels objets et champs* un rôle peut accéder. Les **prédicats d’autorisation au niveau des lignes** vont plus loin et déterminent *quels enregistrements* un rôle peut voir et sur lesquels il peut agir — par exemple, un rôle en libre-service où chaque utilisateur externe ne voit que ses propres enregistrements.
Déclarez les prédicats avec `rowLevelPermissionPredicates` sur le rôle. Comme le reste du manifeste, chaque prédicat porte son propre `universalIdentifier` et fait référence à un objet et à un champ par leur `universalIdentifier`, un `operand` et (facultativement) un champ workspaceMember dont la valeur est injectée au moment de la requête — vous pouvez ainsi exprimer : "la relation de propriétaire de l’enregistrement **est** le membre de l’espace de travail actuel" :
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Comme les prédicats sont livrés dans le manifeste, ils sont créés, mis à jour et supprimés en même temps que le rôle à chaque installation et mise à niveau — il n’y a aucune étape distincte après l’installation à garder synchronisée.
### Combiner des prédicats avec des groupes
Par défaut, les prédicats d’un rôle sont combinés avec `AND`. Pour en combiner certains avec `OR` (ou pour imbriquer la logique), déclarez une entrée `rowLevelPermissionPredicateGroups` et faites pointer chaque prédicat vers celle-ci via `predicateGroupUniversalIdentifier`. Ce rôle permet à un partenaire de voir une opportunité qu’il **possède** ou pour laquelle il est le **point de contact** :
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Notes :
* Donnez à chaque prédicat et groupe un `universalIdentifier` stable (n’importe quel uuid) — il sert de clé à l’entité lors des mises à niveau, et les prédicats référencent les groupes via celui-ci.
* Les prédicats peuvent référencer des objets et des champs appartenant à votre application ou aux objets standard de Twenty.
* La sécurité au niveau des lignes est appliquée pour les espaces de travail sur les offres qui l’incluent ; les prédicats sont tout de même synchronisés sur les autres offres, ils ne sont simplement pas appliqués.
## Le rôle de fonction par défaut
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut déclaré avec `defineApplicationRole()` :
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` est un léger wrapper autour de `defineRole()` qui marque le rôle utilisé comme rôle par défaut de votre application au moment de l’installation. La validation est identique à `defineRole`, mais le pipeline de build connecte automatiquement son `universalIdentifier` au champ `defaultRoleUniversalIdentifier` du manifeste de l’application — vous n’avez donc pas besoin de le référencer vous-même depuis [`defineApplication`](/l/fr/developers/extend/apps/config/application).
Notes :
* Exactement **un** `defineApplicationRole(...)` est autorisé par application — la génération du manifeste échouera si elle en trouve plus d’un.
* Utilisez `defineRole()` (et non `defineApplicationRole()`) pour tous les rôles **supplémentaires** fournis par votre application.
* Le fait de définir explicitement `defaultRoleUniversalIdentifier` dans `defineApplication()` est toujours pris en charge pour assurer la rétrocompatibilité, mais c’est obsolète et remplacé par `defineApplicationRole()`.
## Meilleures pratiques
* Partez du rôle généré par le gabarit, puis restreignez-le progressivement — par défaut, il accorde un large accès en lecture, ce qui correspond rarement à ce que vous voulez en production.
* Remplacez `objectPermissions` et `fieldPermissions` par les objets et champs dont vos fonctions ont réellement besoin.
* `permissionFlagUniversalIdentifiers` contrôlent l’accès aux capacités au niveau de la plateforme. Maintenez-les au strict minimum.
* Voir un exemple fonctionnel : [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
title: Extension des objets
description: Ajoutez des champs aux objets standard de Twenty (Person, Company, …) ou aux objets d'autres applications en utilisant defineField.
icon: wand-magic-sparkles
---
Utilisez `defineField()` pour ajouter un champ à un objet que vous ne possédez pas — un objet standard de Twenty comme Person ou Company, ou un objet fourni par une autre application installée. Contrairement aux champs en ligne déclarés à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects), les champs autonomes nécessitent un `objectUniversalIdentifier` pour préciser l'objet qu'ils étendent.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## Points clés
* `objectUniversalIdentifier` identifie l'objet cible. Pour les objets standard de Twenty, importez la constante depuis `twenty-sdk` :
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Lorsque vous définissez des champs **en ligne dans `defineObject()`**, vous n'avez **pas** besoin de `objectUniversalIdentifier` — il est hérité de l'objet parent.
* `defineField()` est la seule manière d'ajouter des champs à des objets que vous n'avez pas créés avec `defineObject()`.
* L’emplacement du fichier est à votre discrétion. La convention est `src/fields/\<name>.field.ts`, mais le SDK détecte les champs n'importe où dans `src/`.
* La `writability` peut également être définie par champ : `MetadataWritability.APPLICATION` limite les écritures à la logique interne de votre application, `MetadataWritability.SYSTEM` est réservé aux données gérées par la plateforme. La `writability` d’un champ ne peut être que plus stricte que le niveau de son objet, et les lectures ne sont pas affectées. Voir [Objects](/l/fr/developers/extend/apps/data/objects) pour la sémantique complète.
* Pour ajouter un onglet à une mise en page standard (par exemple la page de détail de Task ou de Company), utilisez [`definePageLayoutTab`](/l/fr/developers/extend/apps/layout/page-layouts#definepagelayouttab) avec `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` depuis `twenty-sdk/define`.
## Ajouter une relation à un objet existant
Pour ajouter un champ de relation (par exemple pour lier votre objet personnalisé à un `Person` standard), utilisez `defineField()` avec `FieldType.RELATION`. Le modèle est le même que pour les relations en ligne, mais avec `objectUniversalIdentifier` défini explicitement. Voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle bidirectionnel.

View File

@@ -0,0 +1,143 @@
---
title: Objets
description: Déclarez de nouveaux types d’enregistrements — des tables personnalisées avec leurs propres champs — à l’aide de defineObject.
icon: table
---
Les **objets** personnalisés sont de nouveaux types d’enregistrements que votre application ajoute à un espace de travail — carte postale, facture, abonnement, tout ce qui est spécifique à votre domaine. Chaque objet déclare son schéma (champs, relations, valeurs par défaut) et un identifiant universel stable qui survit aux synchronisations et aux déploiements.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## Points clés
* Le `universalIdentifier` doit être unique et stable entre les déploiements.
* Chaque champ nécessite un `name`, un `type`, un `label` et son propre `universalIdentifier` stable.
* Le tableau `fields` est facultatif — vous pouvez définir des objets sans champs personnalisés.
* `openRecordIn` définit où les enregistrements de cet objet s’ouvrent lorsqu’on clique dessus : `ObjectOpenRecordIn.USER_CHOICE` (la valeur par défaut, qui suit la préférence de chaque membre de l’espace de travail dans Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` ou `ObjectOpenRecordIn.RECORD_PAGE`. Épinglez-le sur `RECORD_PAGE` pour les enregistrements qui ont besoin d’une page complète pour être utilisables, comme les flux de travail et les tableaux de bord, ou sur `SIDE_PANEL` pour les enregistrements qui n’ont de sens qu’en tant que panneau rapide, comme les événements de calendrier.
* `writability` contrôle qui peut écrire des enregistrements de l’objet en général, avant que les autorisations de rôle ne s’appliquent : `MetadataWritability.OPEN` (la valeur par défaut — les rôles de l’espace de travail décident), `MetadataWritability.APPLICATION` (seules les fonctions logiques propres à votre application peuvent créer, mettre à jour ou supprimer des enregistrements ; utilisez ceci pour des objets de type configuration dont les enregistrements confèrent un comportement, afin que les membres de l’espace de travail ayant un large accès aux enregistrements ne puissent pas les modifier via l’API), ou `MetadataWritability.SYSTEM` (réservé aux données gérées par la plateforme). Les opérations de lecture ne sont pas affectées — ceci est imposé côté serveur, contrairement à `isUIEditable`, qui ne fait que masquer les éléments de l’interface utilisateur. Cela existe aussi au niveau de chaque champ, où cela ne peut être que plus strict que le niveau de l’objet.
* Les champs en ligne définis ici n’ont **pas** besoin d’un `objectUniversalIdentifier` — il est hérité de l’objet parent. Utilisez [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) pour ajouter des champs aux objets que vous ne possédez pas.
* Vous pouvez générer de nouveaux objets avec `yarn twenty dev:add object`, qui vous guide à travers le nommage, les champs et les relations. Voir [Architecture → Scaffolding entities](/l/fr/developers/extend/apps/getting-started/scaffolding).
<Note>
**Les champs de base sont ajoutés automatiquement.** Lorsque vous définissez un objet personnalisé, Twenty crée pour vous des champs standard comme `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` et `deletedAt`. Vous n’avez pas besoin de les déclarer dans votre tableau `fields` — uniquement vos champs personnalisés. Vous pouvez remplacer un champ par défaut en en déclarant un avec le même nom, mais c’est rarement une bonne idée.
</Note>
## Types de champ
L’ensemble complet des valeurs de `FieldType`, exportées depuis `twenty-sdk/define` :
| Catégorie | Types |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Texte | `TEXT`, `RICH_TEXT`, `ARRAY` (de chaînes), `RAW_JSON` |
| Numérique | `NUMBER` (`universalSettings.dataType` : `'float'` / `'int'` / `'bigint'`), `NUMERIC` (précision arbitraire), `RATING`, `POSITION` |
| Dates | `DATE`, `DATE_TIME` |
| Choix | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Composés | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identifiants et relations | `UUID`, `RELATION`, `MORPH_RELATION` (voir [Relations](/l/fr/developers/extend/apps/data/relations)) |
| Système | `TS_VECTOR` (vecteur de recherche en texte intégral, géré par le serveur) |
Les types composés stockent plusieurs sous-champs (par exemple `FULL_NAME` = prénom + nom de famille ; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` et `MULTI_SELECT` nécessitent un tableau `options` comme dans l’exemple ci-dessus.
## Valeurs par défaut
Les valeurs par défaut de type chaîne littérale doivent être entourées de guillemets simples **à l’intérieur** de la chaîne — `defaultValue: "'Draft'"`, et non `defaultValue: "Draft"`. C’est pourquoi le champ `status` ci-dessus utilise `` `'${PostCardStatus.DRAFT}'` ``.
Les chaînes sans guillemets sont réservées aux valeurs par défaut calculées, évaluées lors de la création d’un enregistrement :
* `'uuid'` — génère un UUID (pour les champs `UUID`)
* `'now'` — l’horodatage actuel (pour les champs `DATE_TIME`)
La même convention s’applique aux sous-champs de type chaîne des valeurs par défaut composites (par exemple `{ source: "'MANUAL'" }` sur un champ `ACTOR`) ainsi qu’aux valeurs `SELECT`/`MULTI_SELECT`. Une valeur par défaut littérale de type chaîne laissée sans guillemets génère un avertissement lors de la compilation de votre application.
## Nullabilité
`isNullable` détermine si un champ accepte `NULL`. Sa valeur par défaut est `true` — omettez-le pour les champs facultatifs. Définissez `isNullable: false` pour rendre un champ obligatoire au niveau de la base de données.
Les modifications de `isNullable` sont appliquées à chaque synchronisation, y compris celles qui mettent à jour un champ existant — vous pouvez donc modifier la nullabilité d’un champ en éditant le manifeste puis en relançant la synchronisation.
<Note>
**Rendre un champ existant non nullable nécessite une valeur par défaut.** Lorsque vous modifiez un champ avec `isNullable: false`, vous devez également fournir une `defaultValue` non nulle. La valeur par défaut remplit rétroactivement toutes les lignes `NULL` existantes avant que la contrainte `NOT NULL` ne soit appliquée ; sans cela, la synchronisation échoue avec `Default value cannot be null for non-nullable fields`. Les champs de relation et les champs `TS_VECTOR` sont toujours nullables, donc `isNullable` n’a aucun effet sur eux.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## Et après
* **Connectez cet objet aux autres** — voir [Relations](/l/fr/developers/extend/apps/data/relations) pour le modèle de relation bidirectionnelle.
* **Ajoutez des champs aux objets d’autres applications** — voir [Extending Objects](/l/fr/developers/extend/apps/data/extending-objects) pour `defineField()`.
* **Afficher cet objet dans l’interface utilisateur** — voir [Éléments du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) pour ajouter une entrée dans la barre latérale ; voir [Vues](/l/fr/developers/extend/apps/layout/views) pour ajouter des configurations de liste personnalisées.

View File

@@ -0,0 +1,101 @@
---
title: Vue d'ensemble
description: Façonnez les données que votre application ajoute à un espace de travail — objets, champs et relations.
icon: database
---
La **couche de données** d’une application Twenty correspond aux données que votre application *ajoute* à un espace de travail — les nouveaux types d’enregistrements qu’elle déclare, les colonnes qu’elle ajoute aux objets existants et la manière dont ces enregistrements sont reliés les uns aux autres.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Objets" icon="table" href="/l/fr/developers/extend/apps/data/objects">
`defineObject` — déclarez de nouveaux types d’enregistrements avec leurs propres champs.
</Card>
<Card title="Extension des objets" icon="wand-magic-sparkles" href="/l/fr/developers/extend/apps/data/extending-objects">
`defineField` — ajoutez des champs aux objets standard ou à ceux d’autres applications.
</Card>
<Card title="Relations" icon="diagram-project" href="/l/fr/developers/extend/apps/data/relations">
Connexions bidirectionnelles `MANY_TO_ONE` / `ONE_TO_MANY` entre objets.
</Card>
<Card title="Types d'activités de la chronologie" icon="clock-rotate-left" href="/l/fr/developers/extend/apps/data/timeline-activity-types">
Définissez des événements d'audit automatiques et des événements d'application explicites pour les chronologies d'enregistrements.
</Card>
</CardGroup>
## Aperçu des entités
| Entité | Objectif | Défini avec |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Objet** | Un nouveau type d’enregistrement personnalisé (par ex. PostCard, Invoice) avec ses propres champs | `defineObject()` |
| **Champ** | Une colonne d’un objet. Les champs autonomes peuvent étendre des objets que vous n’avez pas créés (par ex. ajouter `loyaltyTier` à Company) | `defineField()` |
| **Relation** | Un lien bidirectionnel entre deux objets — les deux côtés étant déclarés comme des champs | `defineField()` avec `FieldType.RELATION` |
| **Index** | Un index de base de données pour accélérer une requête récurrente sur l’un de vos objets | `defineIndex()` |
| **Type d'activité de la chronologie** | Un événement d'audit automatique ou un événement de domaine explicite affiché dans les chronologies des enregistrements. | `defineTimelineActivityType()` |
Le SDK les détecte via une analyse AST au moment de la compilation, donc l’organisation des fichiers vous appartient — la convention est `src/objects/`, `src/fields/`, `src/indexes/` et `src/timeline-activity-types/`. Des UUID `universalIdentifier` stables relient tout d’un déploiement à l’autre.
## Index (optionnels)
Les applications peuvent fournir des index avec leurs objets pour que les requêtes récurrentes restent rapides. Le cas le plus courant est une colonne de statut ou de clé étrangère que vous lisez fréquemment.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Index uniques
`defineIndex` accepte `isUnique: true` pour l’unicité sur une ou plusieurs colonnes. C’est la primitive recommandée — `defineField({ isUnique: true })` est obsolète et sera supprimé dans une future version.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Autres contraintes
* Les clauses `WHERE` partielles restent sous le contrôle de l’administrateur — les applications ne peuvent pas les déclarer.
* Chaque objet est limité à 10 index personnalisés (les index propres au framework ne comptent pas).
Ordonnez le tableau `fields` dans l’ordre dans lequel Postgres doit l’utiliser — colonne la plus à gauche en premier, comme dans un annuaire téléphonique. Les index ne sont pas gratuits : chaque écriture dans la table les met à jour. Ajoutez-en un uniquement lorsque vous avez une requête qui en a besoin.
<Note>
Vous cherchez **Application Config** ou **Roles & Permissions** ? Ces éléments décrivent l’application elle-même plutôt que les données qu’elle ajoute — ils se trouvent sous [Config](/l/fr/developers/extend/apps/config/overview). Vous cherchez **Connections** (Linear, GitHub, Slack OAuth) ? Ceux-ci existent pour être appelés *depuis* des fonctions logiques et se trouvent sous [Logic](/l/fr/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relations
description: Reliez des objets entre eux avec des relations MANY_TO_ONE / ONE_TO_MANY bidirectionnelles.
icon: diagram-project
---
Les relations relient deux objets entre eux. Dans Twenty, les relations sont toujours **bidirectionnelles** — chaque relation a deux côtés, et chaque côté est déclaré comme un champ qui référence l'autre.
| Type de relation | Description | Possède une clé étrangère ? |
| ---------------- | ------------------------------------------------------------------------------------ | --------------------------- |
| `MANY_TO_ONE` | De nombreux enregistrements de cet objet pointent vers un enregistrement de la cible | Oui (`joinColumnName`) |
| `ONE_TO_MANY` | Un enregistrement de cet objet possède de nombreux enregistrements de la cible | Non (le côté inverse) |
## Fonctionnement des relations
Chaque relation nécessite **deux champs** qui se référencent mutuellement :
1. Le côté **MANY_TO_ONE** — réside sur l'objet qui détient la clé étrangère.
2. Le côté **ONE_TO_MANY** — réside sur l'objet qui possède la collection.
Les deux champs utilisent `FieldType.RELATION` et se référencent mutuellement via `relationTargetFieldMetadataUniversalIdentifier`.
## Exemple : une carte postale a de nombreux destinataires
Un `PostCard` peut être envoyé à de nombreux enregistrements `PostCardRecipient`. Chaque destinataire appartient à exactement une carte postale.
**Étape 1: Définir le côté ONE_TO_MANY sur PostCard** (le côté "one") :
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Étape 2: Définir le côté MANY_TO_ONE sur PostCardRecipient** (le côté "many" — détient la clé étrangère) :
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Imports circulaires :** les deux champs de relation se référencent l'un l'autre via leur `universalIdentifier`. Pour éviter les problèmes d'import circulaire, exportez les ID de vos champs en tant que constantes nommées depuis chaque fichier, puis importez-les dans l'autre. Le système de build les résout au moment de la compilation.
</Note>
## Relations de jonction
Un objet de jonction connecte un enregistrement source à un ou plusieurs enregistrements cibles. Par exemple, `PostCardRecipient` peut contenir à la fois une relation `postCard` et une relation `recipient`. Dans le champ `ONE_TO_MANY` de l'objet source, définissez `junctionTargetFieldUniversalIdentifier` sur le champ de relation de l'objet de jonction qui pointe vers la cible :
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` doit identifier une relation `MANY_TO_ONE` ou une relation morph sur l'objet de jonction. Il indique à Twenty quel enregistrement associé est la cible lorsqu'une fonctionnalité telle que la [diffusion des activités de chronologie](/l/fr/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) parcourt la jonction.
## Relation avec des objets standard
Pour créer une relation avec un objet Twenty intégré (Person, Company, etc.), utilisez `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` :
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## Propriétés des champs de relation
| Propriété | Obligatoire | Description |
| ---------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `type` | Oui | Doit être `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` de l'objet cible |
| `relationTargetFieldMetadataUniversalIdentifier` | Oui | Le `universalIdentifier` du champ correspondant sur l'objet cible |
| `universalSettings.relationType` | Oui | `RelationType.MANY_TO_ONE` ou `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE uniquement | Ce qui se passe lorsque l'enregistrement référencé est supprimé : `CASCADE`, `SET_NULL`, `RESTRICT` ou `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE uniquement | Nom de la colonne de base de données pour la clé étrangère (p. ex., `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Jonction `ONE_TO_MANY` uniquement | Champ de relation de l'objet de jonction qui pointe vers la cible de diffusion |
## Champs de relation en ligne
Vous pouvez également déclarer une relation directement à l'intérieur de [`defineObject`](/l/fr/developers/extend/apps/data/objects). Lorsqu'il est en ligne, omettez `objectUniversalIdentifier` — il est hérité de l'objet parent :
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,210 @@
---
title: Ciblage des métadonnées système
description: Résolvez les identifiants universels déterministes des métadonnées Twenty provisionnées automatiquement sur chaque objet, afin que votre application puisse s’y référer sans les coder en dur.
icon: gears
---
Chaque objet dans Twenty est fourni avec des **métadonnées système** que vous ne déclarez jamais vous‑même, comme un ensemble de champs et une vue principale en liste avec ses colonnes. Le serveur crée l’ensemble de ces éléments lorsque l’objet est provisionné, et cet ensemble s’agrandit au fur et à mesure que Twenty évolue.
Comme vous ne les déclarez pas, il n’existe pas de constante `universalIdentifier` que vous puissiez importer. À la place, le serveur **dérive** chaque identifiant de manière déterministe, et `twenty-sdk` expose la même dérivation afin que votre manifeste puisse résoudre la valeur exacte utilisée par le serveur.
## Champs système
Les champs scalaires présents sur chaque objet, que vous ne déclarez jamais avec [`defineField()`](/l/fr/developers/extend/apps/data/extending-objects) :
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Alors, comment référencer `createdAt` en tant que colonne dans une [vue](/l/fr/developers/extend/apps/layout/views) ?
### Le problème
Depuis Twenty 2.19, l'identifiant universel d'un champ système est **dérivé de manière déterministe** par le serveur à partir de trois entrées : l'identifiant universel de l'application, l'identifiant universel de l'objet et le nom du champ. Inventer un id et le coder en dur ne fonctionnera pas : il ne correspond à rien sur le serveur, et la synchronisation rejette la référence orpheline :
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La solution
<Note>
`getFieldUniversalIdentifier` est disponible à partir de `twenty-sdk` 2.21.
</Note>
Utilisez `getFieldUniversalIdentifier` pour résoudre exactement la même valeur que celle utilisée par le serveur. Elle prend les trois entrées et renvoie l'identifiant universel du champ :
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` est l'identifiant de votre application, celui que vous transmettez à [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* `objectUniversalIdentifier` est l'identifiant de l'objet auquel le champ appartient.
* `name` est le nom du champ système, l'une des valeurs listées ci-dessus.
### Exemple : une colonne createdAt dans une vue
Le cas typique consiste à ajouter une colonne `createdAt` à une vue de l'un de vos objets personnalisés. Résolvez l'id du champ et référencez-le comme n'importe quel autre `fieldMetadataUniversalIdentifier` :
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Le même id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu : champs de vue, filtres, tris, regroupements et widgets de mise en page.
<Note>
Résolvez l'id, ne le codez pas en dur. Parce que le serveur dérive la valeur à partir de
l'id de l'application, de l'id de l'objet et du nom du champ, appeler
`getFieldUniversalIdentifier` garde votre référence correcte même si ces
entrées changent, et évite les divergences si la dérivation évolue un jour.
</Note>
### Champs de relation système
<Note>
`getSystemRelationFieldUniversalIdentifier` est disponible dans `twenty-sdk`
à partir de la version 2.23 et nécessite un serveur Twenty en version 2.23 ou ultérieure.
</Note>
Outre les champs système scalaires ci-dessus, le serveur met également à disposition quatre **champs de relation système** sur chaque objet : `timelineActivities`, `attachments`, `noteTargets` et `taskTargets`, chacun pointant vers l’objet de relation standard correspondant.
Ainsi, ces champs ne sont pas résolus avec `getFieldUniversalIdentifier` : leur identifiant est dérivé **indépendamment du nom**, à partir de l’objet qui héberge le champ et de l’objet vers lequel le champ pointe. De cette façon, renommer un objet ne modifie jamais les identifiants de ses champs de relation.
Utilisez `getSystemRelationFieldUniversalIdentifier` pour les résoudre :
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` est l’objet qui **héberge** le champ.
* `relationTargetObjectUniversalIdentifier` est l’objet vers lequel le champ **pointe**.
La direction est encodée par l’ordre des arguments. Pour résoudre le côté inverse (par exemple `attachment.targetRocket`, le champ morph que le serveur crée sur l’objet de relation standard), inversez les deux :
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Comme pour les champs système scalaires, l’id résolu fonctionne partout où un `fieldMetadataUniversalIdentifier` est attendu.
## Vues système
<Note>
`getSystemViewUniversalIdentifier` et `getSystemViewFieldUniversalIdentifier`
sont disponibles dans `twenty-sdk` à partir de la version 2.26 et nécessitent
un serveur Twenty en version 2.26 ou ultérieure.
</Note>
Le serveur provisionne également une **vue système** sur chaque objet : la vue principale en liste (`All {objectLabelPlural}`, indexée sur `ViewKey.INDEX` (dérivée de `SYSTEM_VIEW_KEYS.INDEX`)), avec une colonne par champ affichable. Comme pour les champs de relation système, leurs identifiants sont dérivés **sans nom**, de sorte que renommer un objet ou un champ ne les modifie jamais.
Utilisez `getSystemViewUniversalIdentifier` pour la résoudre :
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` correspond à l’application propriétaire de l’**objet**, ce qui sert d’espace de noms pour la vue.
* `objectUniversalIdentifier` est l’objet que la vue répertorie.
* `viewKey` est le discriminateur de vue système : `SYSTEM_VIEW_KEYS.INDEX` pour la vue principale en liste, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` pour la vue du widget de champs de la page d’enregistrement. Il sert de clé pour la dérivation ; seul `INDEX` est également stocké sur la ligne de la vue.
L’identifiant résolu fonctionne partout où un `viewUniversalIdentifier` est attendu, par exemple pour une entrée de barre latérale [`NavigationMenuItemType.VIEW`](/l/fr/developers/extend/apps/layout/navigation-menu-items). Pour simplement ouvrir la liste principale d’un objet, privilégiez `NavigationMenuItemType.OBJECT` avec `targetObjectUniversalIdentifier` : cela ne nécessite aucune dérivation.
`getSystemViewFieldUniversalIdentifier` résout une seule **colonne** sur une vue système, à partir de la vue et du champ qu’elle affiche :
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Notez le premier argument : une colonne est placée dans l’espace de noms de l’application propriétaire du **champ qu’elle affiche**, et non de celle propriétaire de la vue. Un champ que votre application ajoute à un objet standard obtient sa colonne dérivée sous votre application, sur une vue appartenant à Twenty.
<Warning>
Les vues système et leurs colonnes sont **la propriété du serveur** : résolvez leurs identifiants pour les référencer, jamais pour les déclarer. `key` sur
[`defineView()`](/l/fr/developers/extend/apps/layout/views) est obsolète et ignoré, de sorte qu’une vue de manifeste ne peut jamais revendiquer la clé `INDEX`, et le serveur provisionne déjà une colonne pour chaque champ que vous ajoutez, de sorte que déclarer votre propre
`defineViewField()` pour ce même champ sur une vue système entre en conflit avec celle‑ci.
</Warning>
## Objets standard de Twenty
Pour un objet Twenty **standard** (Person, Company, Opportunity, …), vous n’avez rien à dériver : les identifiants sont des constantes pré‑calculées que vous pouvez importer directement, à la fois pour les champs et pour les vues.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Utilisez les fonctions utilitaires ci‑dessus lorsque l’objet est l’un de ceux que **votre application** définit avec [`defineObject()`](/l/fr/developers/extend/apps/data/objects), et pour lesquels il n’existe pas une telle constante.
<Note>
`name` est un champ **par défaut**, pas un champ système. Il conserve son propre identifiant universel codé en dur et n'est pas résolu via
`getFieldUniversalIdentifier`. Sur les objets que vous définissez, référencez le champ
`name` avec l'identifiant que vous lui avez donné dans `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Types d'activités de chronologie
description: Définissez les événements d'audit automatique et les événements explicites de l'application qui s'affichent dans les calendriers d'enregistrement.
icon: clock-rotate-left
---
Un type d'activité de la timeline définit le vocabulaire stable pour un événement affiché dans la timeline d'un enregistrement. Les objets standards et les objets d'application utilisent le même contrat : un type a une étiquette et une icône, peut éventuellement déclarer lorsqu'il est émis, et peut éventuellement être rendu à travers l'un des [composants frontaux] de votre application (/developers/extend/apps/layout/front-components).
<Note>
Les types d'activité de la chronologie sont en bêta et arrivent bientôt dans Vingt 2.34. L'API
peut évoluer pendant que nous apprenons des cas d'utilisation des développeurs d'applications.
</Note>
En créer un avec l'échafaudage :
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Ou définissez-le directement :
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Événements automatiques et explicites
Ajoute `emit` quand Vingt devrait créer ce type automatiquement. `emit.on` supporte `created`, `updated`, `deleted`, `restored`, `linked`, et `unlinked`, tandis que `emit.objectUniversalIdentifier` identifie l'objet source. Un seul type efficace peut gérer la même clé d'émission dans un espace de travail : `on`, object et optionnel via une relation.
Sans `emit.through`, l'événement est écrit sur la timeline de l'enregistrement source. Pour l'encourager aux enregistrements liés, définissez `emit.through. elationFieldUniversalIdentifier` à une relation directe de plusieurs à un ou à une relation de jonction [un à plusieurs] (/developers/extend/apps/data/relations#junction-relations) sur l'objet source. Les relations de morphe se répandent vers chaque membre de leur groupe de morphes, donc une seule déclaration peut cibler plusieurs types d'objets.
Pour une relation directe, créer ou restaurer l'enregistrement source produit `linked`, supprimer cela produit `unlinked`, et repointant la relation produit `unlinked` sur la cible précédente plus `linked` sur la nouvelle cible. Les autres mises à jour de sources ne produisent pas d'événements de liens. Il s'agit du contrat utilisé par les pièces jointes, dont la cible est une relation de morphe directe.
Pour une relation de jonction, `universalSettings.junctionTargetFieldUniversalIdentifier` identifie la relation de l'objet de la jonction à la cible. Créer ou supprimer une ligne de jonction produit `linked` ou `unlinked`. Repointer de chaque côté d'une ligne de jonction produit également un événement de lien; les mises à jour des champs de jonction non liés ne le font pas.
Les événements `linked` et `unlinked` nécessitent `emit.through`, car leur déclenchement est un changement de la relation directe ou de la jonction configurée.
Par exemple, il s'agit du même contrat générique utilisé par les notes, les tâches, les messages et les événements du calendrier :
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Une `mise à jour` à travers un événement est émise pour chaque mise à jour source par défaut. Définissez `emit.through.triggerFieldUniversalIdentifiers` lorsque seuls les changements aux champs source sélectionnés devraient apparaître sur les fils cibles.
Omettre `emit` pour un événement de domaine explicite que votre fonction logique se crée. Cela évite de produire à la fois une ligne d'audit automatique et une ligne explicite pour la même opération.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Créez des événements explicites avec [`createTimelineActivity()`](/l/fr/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Le code d'application utilise des identifiants universels stables ; vingt résolvent les identifiants de métadonnées spécifiques à l'installation.
## Rendu personnalisé
Sans composant frontal, Twenty affiche une ligne générique native à partir de l'étiquette de type, de l'icône et des métadonnées de l'objet lié. Cela fonctionne pour les objets standards et personnalisés sans moteur de rendu spécifique à l'objet.
Pour des détails personnalisés, définissez `frontComponentUniversalIdentifier` à un composant frontal appartenant à la même application:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
La ligne native reste la présentation réduite. Vingt montent le composant avant seulement après que l'utilisateur ait agrandi cette rangée, en évitant un bac à sable et un travailleur pour chaque événement visible. À l'intérieur du composant, appelez `useTimelineActivityId()` à partir de `vingt-sdk/front-component` pour lire l'ID de ligne et récupérer toutes les données dont votre présentation a besoin. Il retourne `null` lorsque le composant est rendu en dehors d'une ligne de timeline.
## Remplacer la présentation d'une autre application
Une application possède des contrats automatiques de timeline pour ses propres objets. Pour personnaliser un événement sur un objet appartenant à une autre application, déclarer explicitement le type existant avec `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Une substitution doit inclure `emit`, car elle remplace un emplacement d'émission automatique existant plutôt qu'un type explicite. Le type référencé doit appartenir à l'application de l'objet cible et décrire la même action et la même route. La suppression de l'application écrasante restaure le type de base ; la suppression du type de base désactive la substitution.
## Outrepasser et rendre muet l'espace de travail
Les administrateurs de l'espace de travail peuvent modifier la présentation d'un type ou couper en sourdine ses événements automatiques et explicites sans avoir à creuser l'application. Utilisez la mutation `updateTimelineActivityType` de l'API de métadonnées avec l'ID de type spécifique à l'installation :
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` et `icon` sont stockés en tant que substituts d'espace de travail, donc les mises à jour ultérieures des applications n'écrasent pas les choix de l'administrateur. Définissez `isActive` à `false` pour arrêter l'émission automatique et rejeter les nouveaux événements explicites de ce type. Les lignes existantes restent visibles.
Restaurer les valeurs par défaut de l'application, y compris l'état actif, avec:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Champs de configuration
| Champ | Obligatoire | Description |
| ------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
| `universalIdentifier` | Oui | UUID stable pour le type d'installations et de mises à jour |
| `name` | Oui | Nom programmatique local à l'application ; doit être unique au sein de l'application |
| `label` | Oui | Texte d'action utilisé par le moteur de rendu natif |
| `icon` | Non | Vingt icônes affichées à côté de l'événement |
| `émettre` | Non | Déclaration d'émission automatique ; omet pour les types explicites |
| `emit.on` | Avec émission | Action d'audit qui permet d'écrire ce type |
| `emit.objectUniversalIdentifier` | Avec émission | Objet dont les enregistrements émettent ce type |
| `emit.through.relationFieldUniversalIdentifier` | Non | Relation directe ou de jonction utilisée pour alimenter les fils connexes |
| `emit.through.triggerFieldUniversalIdentifiers` | Non | Champs source qui peuvent déclencher une `mise à jour` à travers l'événement |
| `frontComponentUniversalIdentifier` | Non | Composant frontal appartenant à une application monté lorsque la ligne est étendue |
| `replacesTimelineActivityTypeUniversalIdentifier` | Non | Type d'émission existant remplacé; nécessite `emit` |
Vingt instantanés instantanés d'identité sémantique et présentation durable se replient quand un événement est créé. Les lignes historiques conservent leur action originale et leur signification d'objet. Pendant que le type est installé, son étiquette traduite en cours, son icône et son composant frontal sont rendus en direct. Après désinstallation, la clé de secours de l'instantané conserve la ligne lisible. La résolution utilise l'identifiant universel, donc la présentation survit également à la réinstallation de l'application.

View File

@@ -0,0 +1,102 @@
---
title: Concepts
description: Fonctionnement des applications Twenty — modèle d’entité, sandboxing et cycle de vie de l’installation.
icon: sitemap
---
Les applications Twenty sont des packages TypeScript qui étendent votre espace de travail avec des objets personnalisés, de la logique, des composants d’interface utilisateur et des capacités d’IA. Elles s’exécutent sur la plateforme Twenty avec une isolation complète en bac à sable et des contrôles d’autorisations.
## Fonctionnement des applications
Une application est un ensemble d’**entités** déclarées à l’aide des fonctions `defineEntity()` du package `twenty-sdk`. Le SDK détecte ces déclarations via une analyse de l’AST au moment de la compilation et produit un **manifeste** — une description complète de ce que votre application ajoute à un espace de travail. Ces fonctions valident votre configuration au moment de la compilation et offrent l’autocomplétion dans l’IDE et une sécurité de typage.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**L’organisation des fichiers vous revient.** La détection des entités est basée sur l’AST — le SDK trouve les appels à `export default defineEntity(...)` quel que soit l’emplacement du fichier. La structure de dossiers ci-dessus est une convention, pas une obligation.
</Note>
## Types d’entités
| Entité | Objectif | Documentation |
| --------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Application** | Identité de l’application, rôle par défaut, variables | [Configuration de l’application](/l/fr/developers/extend/apps/config/application) |
| **Rôle** | Ensembles d’autorisations pour les objets et les champs | [Rôles et autorisations](/l/fr/developers/extend/apps/config/roles) |
| **Objet** | Types d’enregistrements personnalisés avec des champs | [Objets](/l/fr/developers/extend/apps/data/objects) |
| **Champ** | Ajouter des champs à des objets provenant d’autres applications | [Extension des objets](/l/fr/developers/extend/apps/data/extending-objects) |
| **Relation** | Liens bidirectionnels entre les objets | [Relations](/l/fr/developers/extend/apps/data/relations) |
| **Fonction logique** | TypeScript côté serveur avec des déclencheurs | [Fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) |
| **Compétence** | Instructions réutilisables pour des agents d’IA | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | Assistants d’IA avec des prompts personnalisés | [Compétences et agents](/l/fr/developers/extend/apps/logic/skills-and-agents) |
| **Fournisseur de connexion** | Identifiants OAuth pour les API tierces | [Connexions](/l/fr/developers/extend/apps/logic/connections) |
| **Vue** | Vues de liste d’enregistrements préconfigurées | [Vues](/l/fr/developers/extend/apps/layout/views) |
| **Élément de menu de navigation** | Entrées personnalisées de la barre latérale | [Éléments du menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) |
| **Mise en page** | Onglets et widgets sur la page de détails d’un enregistrement | [Mises en page](/l/fr/developers/extend/apps/layout/page-layouts) |
| **Composant frontal** | Interface React isolée en bac à sable dans Twenty | [Composants frontaux](/l/fr/developers/extend/apps/layout/front-components) |
| **Dépendances partagées** | Dépendances partagées par les composants frontaux de l’application, déclarées dans package.json | [Dépendances partagées](/l/fr/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **Élément du menu de commande** | Actions rapides et entrées Cmd+K | [Éléments du menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) |
## Exécution en bac à sable
* **Les fonctions logiques** s’exécutent dans des processus Node.js isolés sur le serveur. Ils accèdent uniquement aux données via le client d’API typé, qui agit soit en tant que personne ayant déclenché l’exécution, soit en tant que l’application elle-même. Consultez [Les accès utilisés par un appel](/l/fr/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **Les composants frontaux** s’exécutent dans des Web Workers en utilisant Remote DOM — isolés en bac à sable de la page principale mais rendant des éléments DOM natifs (pas d’iframes). Ils communiquent avec Twenty via une API hôte de passage de messages.
* **Les autorisations** sont appliquées au niveau de l’API. Deux jetons d’exécution sont injectés, tous deux dérivés du rôle déclaré avec `defineApplicationRole()` ; le jeton par défaut est davantage restreint à ce que la personne ayant déclenché l’exécution peut faire lorsqu’une personne est à l’origine de l’exécution.
## Cycle de vie de l’application
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — surveille vos fichiers source et synchronise en direct les modifications avec un serveur Twenty connecté. Le client d’API typé est régénéré automatiquement lorsque le schéma change.
* **`yarn twenty dev:build`** — compile TypeScript, regroupe les fonctions logiques et les composants frontaux avec esbuild, et produit un manifeste.
* **Hooks pré/post-install et de désinstallation** — fonctions optionnelles qui s’exécutent pendant l’installation ou juste avant la désinstallation. Voir [Install Hooks](/l/fr/developers/extend/apps/config/install-hooks) pour plus de détails.
## Prochaines étapes
<CardGroup cols={2}>
<Card title="Configuration" icon="screwdriver-wrench" href="/l/fr/developers/extend/apps/config/overview">
Identité de l’application, rôle par défaut et hooks d’installation et de désinstallation.
</Card>
<Card title="Données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Objets, champs et relations bidirectionnelles.
</Card>
<Card title="Logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Fonctions logiques, compétences, agents et connexions OAuth.
</Card>
<Card title="Disposition" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Vues, navigation, mises en page, composants front-end.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, dépôts distants, CI et publication de votre application.
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: Serveur local
description: Gérez le serveur Docker local de Twenty — démarrage, arrêt, mise à niveau, instance de test parallèle et configuration manuelle du SDK.
icon: server
---
## Gestion du serveur local
Utilisez `yarn twenty docker:*` pour contrôler le conteneur Twenty local :
| Commande | Ce que cela fait |
| -------------------------------------- | ---------------------------------------------------------- |
| `yarn twenty docker:start` | Démarre le serveur (récupère l’image si nécessaire) |
| `yarn twenty docker:start 2.2.0` | Démarrer une version spécifique du serveur |
| `yarn twenty docker:start --port 3030` | Démarre sur un port personnalisé |
| `yarn twenty docker:stop` | Arrête le serveur (préserve les données) |
| `yarn twenty docker:status` | Affiche l’URL, la version et les identifiants de connexion |
| `yarn twenty docker:logs` | Diffuse les journaux du serveur |
| `yarn twenty docker:reset` | Efface les données et repart de zéro |
| `yarn twenty docker:upgrade` | Récupère la dernière image `twenty-app-dev` |
| `yarn twenty docker:upgrade 2.2.0` | Met à niveau vers une version spécifique |
Les données sont conservées entre les redémarrages dans deux volumes Docker (`twenty-app-dev-data` pour PostgreSQL, `twenty-app-dev-storage` pour les fichiers). Utilisez `reset` pour tout effacer.
## Figer la version du serveur
Lorsqu’aucune version n’est fournie, `docker:start` résout la version à partir de la plage `engines.twenty` de votre `package.json` — la même plage que le serveur utilise pour la validation lors de l’installation de votre application. Il démarre l’image `twenty-app-dev` la plus récente publiée qui satisfait la plage, et revient à `latest` lorsque le champ est absent ou qu’aucune version publiée ne correspond :
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Passez explicitement une version pour remplacer la plage pour une seule exécution : `yarn twenty docker:start 2.3.0`. Si un conteneur existe déjà avec une version différente, `docker:start` le met à niveau sur place (en recréant le conteneur tout en préservant vos volumes de données).
## Mise à niveau de l’image du serveur
`yarn twenty docker:upgrade` récupère la dernière image, compare les empreintes et ne recrée le conteneur que si quelque chose a réellement changé. Les volumes sont conservés — seul le conteneur est remplacé. Si une nouvelle image a été récupérée et que le conteneur était en cours d’exécution, la mise à niveau démarre automatiquement un nouveau conteneur ; exécutez ensuite `yarn twenty docker:start` pour attendre qu’il devienne opérationnel.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Vérifiez la version en cours d’exécution avec `yarn twenty docker:status` (elle affiche la valeur de `APP_VERSION` intégrée au conteneur).
## Exécuter une instance de test parallèle
Passez `--test` à n’importe quelle commande `docker:*` pour gérer une deuxième instance totalement isolée — utile pour les tests d’intégration ou pour expérimenter sans toucher à vos données de développement principales :
| Commande | Ce que cela fait |
| ----------------------------------- | ------------------------------------------------- |
| `yarn twenty docker:start --test` | Démarre l’instance de test (port 2021 par défaut) |
| `yarn twenty docker:stop --test` | L’arrête |
| `yarn twenty docker:status --test` | Affiche son statut |
| `yarn twenty docker:logs --test` | Diffuse ses journaux |
| `yarn twenty docker:reset --test` | Efface ses données |
| `yarn twenty docker:upgrade --test` | Met à niveau son image |
L’instance de test possède son propre conteneur (`twenty-app-dev-test`), ses volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) et sa configuration — elle s’exécute parallèlement à votre instance principale sans conflits. Combinez `--test` avec `--port` pour remplacer le port 2021.
## Configuration manuelle (sans le générateur)
Ignorez le générateur si vous ajoutez le SDK à un projet existant :
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Ajoutez le script à `package.json` :
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Vous pouvez maintenant exécuter `yarn twenty dev`, `yarn twenty docker:start` et le reste.
<Note>
N’installez pas `twenty-sdk` globalement — figez-le par projet afin que chaque application utilise sa propre version.
</Note>

View File

@@ -0,0 +1,79 @@
---
title: Structure du projet
description: Ce qui se trouve à l’intérieur d’une application Twenty générée par un modèle — fichiers, dossiers et le rôle de chacun.
icon: folder-tree
---
Une nouvelle application générée par `npx create-twenty-app` ressemble à ceci :
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## Fichiers clés
| Fichier / Dossier | Objectif |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Requis.** Le fichier de configuration principal de votre application. |
| `src/default-role.ts` | Rôle par défaut qui contrôle ce à quoi vos fonctions de logique peuvent accéder. |
| `src/constants/universal-identifiers.ts` | UUID générés automatiquement et métadonnées de l’application (nom d’affichage, description). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Une page d’accueil de démarrage : un composant frontal rendu par une mise en page autonome, accessible depuis la barre latérale. |
| `src/__tests__/` | Un test unitaire plus un test d’intégration (avec sa configuration globale) qui synchronise l’application avec un serveur réel. |
| `public/` | Ressources statiques (images, polices) servies avec votre application. |
| `AGENTS.md` / `CLAUDE.md` | Consignes pour les agents IA de programmation travaillant sur l’application. |
| `CHANGELOG.md` / `SETUP.md` | Journal des modifications notables et instructions de configuration pour le développement local. |
<Note>
**L’organisation des fichiers vous revient.** Les dossiers ci-dessus sont des conventions — le SDK détecte les entités via une analyse AST sur les appels à `export default defineEntity(...)` quel que soit l’emplacement du fichier.
</Note>
## Dépendances
Les deux packages du SDK Twenty doivent être placés dans `devDependencies`, et non dans `dependencies` :
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
Le générateur de projet fige `twenty-sdk` et `twenty-client-sdk` sur sa propre version — gardez les deux synchronisés lors de la mise à niveau.
* **`twenty-sdk`** fournit le CLI `twenty` ainsi que les outils de build et de scaffolding. Il ne s’exécute qu’au moment du développement et du build et n’est jamais importé par le runtime de l’application que vous publiez.
* **`twenty-client-sdk`** *est* importé par le code de votre application (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), mais Twenty le fournit au moment de l’exécution : les fonctions de logique l’obtiennent à partir d’une couche SDK générée, et les composants front le résolvent à partir de modules servis par le serveur. La copie que vous avez installée est uniquement utilisée pour la vérification de type et le build au moment du déploiement, elle n’a donc jamais besoin d’être incluse dans le bundle déployé.
Conserver l’un ou l’autre package dans `dependencies` l’intègre dans le bundle runtime de l’application installée, où il ne fait que l’alourdir inutilement. `twenty dev:build` émet un avertissement lorsque l’un ou l’autre est encore répertorié dans `dependencies`.
Ajoutez les dépendances runtime propres à votre application (les bibliothèques que vos fonctions logiques importent réellement à l’exécution) dans `dependencies` comme d’habitude. Gardez l’arborescence légère : les `dependencies` sont installées dans une couche d’exécution limitée à 250 Mo décompressés, et une synchronisation ou une installation échoue avec une erreur de taille des dépendances au-delà de cette limite — les bibliothèques d’interface utilisateur et les outils de développement doivent aller dans `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Démarrage rapide
icon: rocket
description: Créez votre première application Twenty en quelques minutes.
---
## Prérequis
* **Node.js 24.5+** — [Télécharger](https://nodejs.org/)
* **Yarn 4** — fourni avec Node.js via Corepack. Activez-le : `corepack enable`
* **Docker** — [Télécharger](https://www.docker.com/products/docker-desktop/). Nécessaire pour exécuter un serveur Twenty local. Ignorez si vous avez déjà Twenty en cours d’exécution ailleurs.
La création d’une application Twenty comporte trois phases. Le générateur les regroupe en une seule commande pour le parcours idéal, mais chaque phase est un concept distinct — en cas d’échec, savoir dans quelle phase vous vous trouvez indique ce qu’il faut corriger.
| Phase | Ce que vous faites | Outil | Résultat |
| -------------------------- | ------------------------------------------------------ | ----------------------------------- | ----------------------------------------------------------- |
| **1. Génération** | Générer le code source de l’application | `npx create-twenty-app` | Un projet TypeScript sur le disque |
| **2. Exécuter un serveur** | Démarrer un serveur Twenty vers lequel se synchroniser | Docker + `yarn twenty docker:start` | Une instance Twenty en cours d’exécution |
| **3. Synchroniser** | Synchroniser en direct votre code avec le serveur | `yarn twenty dev` | Vos modifications apparaissent dans l’interface utilisateur |
---
## Phase 1 — Générez le squelette de votre projet
Créez une nouvelle application à partir du modèle :
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Le générateur est non interactif : le nom du répertoire devient le nom de l’application. Passez `--display-name` et `--description` pour personnaliser les métadonnées générées (vous pouvez également les modifier plus tard dans `src/constants/universal-identifiers.ts`). Cela génère un projet TypeScript dans `my-twenty-app/` avec un fichier de démarrage `application-config.ts`, un rôle par défaut, des workflows CI/CD et un test d’intégration.
**Après cette phase :** vous disposez du code source d’une application sur votre machine. Elle ne s’exécute pas encore — c’est la phase 2.
---
## Phase 2 — Exécuter un serveur Twenty local
Votre application a besoin d’un serveur Twenty vers lequel se synchroniser. Le serveur est une instance Twenty complète — interface utilisateur, API GraphQL, PostgreSQL — exécutée localement dans Docker. Votre code local envoie ses définitions à ce serveur, qui les fait apparaître dans l’interface utilisateur.
Le générateur de projet en crée un pour vous : avec Docker en cours d’exécution, il récupère l’image `twentycrm/twenty-app-dev`, la démarre sur le port `2020`, et authentifie le CLI auprès de l’espace de travail de démonstration prérempli (`tim@apple.dev`) — aucune connexion requise.
Pour vous connecter à un serveur Twenty existant à la place, passez `--url \<your-server-url>`. Les serveurs distants s’authentifient avec OAuth : un navigateur s’ouvre pour que vous puissiez vous connecter et cliquer sur **Authorize**, ce qui donne au CLI l’accès à votre espace de travail. (Vous pouvez aussi choisir OAuth en local avec `--authentication-method oauth` — connectez-vous avec `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Écran de connexion Twenty" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Écran d’autorisation de la CLI Twenty" />
</div>
Votre terminal confirmera que tout est configuré.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Application initialisée avec succès" />
</div>
**Après cette phase :** vous disposez d’un serveur Twenty en cours d’exécution sur [http://localhost:2020](http://localhost:2020) avec votre CLI autorisée à s’y synchroniser.
<Note>
Si Docker n’est pas installé ou en cours d’exécution, le générateur vous indiquera la bonne commande de démarrage pour votre système d’exploitation. Une fois Docker lancé, vous pouvez reprendre avec `yarn twenty docker:start` — pas besoin de régénérer le squelette.
</Note>
---
## Phase 3 — Synchroniser vos modifications
C’est la boucle interne dans laquelle vous passerez la plupart de votre temps.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Cela surveille `src/`, reconstruit à chaque modification et synchronise le résultat avec le serveur. Modifiez un fichier, enregistrez, et en quelques secondes le serveur reflète la modification. Vous verrez un panneau d’état en direct dans votre terminal.
Pour une sortie plus détaillée (journaux de build, requêtes de synchronisation, traces d’erreurs), ajoutez `--verbose`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Sortie du terminal en mode dev" />
</div>
Ouvrez [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Vous devriez voir votre application sous **Your Apps**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="La liste Your Apps affichant My twenty app" />
</div>
Cliquez sur **My twenty app** pour voir son **application registration** — un enregistrement au niveau du serveur décrivant votre application (nom, identifiant, informations d’identification OAuth, source). Un même enregistrement peut être installé dans plusieurs espaces de travail sur le même serveur.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Détails de l’enregistrement d’application" />
</div>
Cliquez sur **View installed app** pour voir l’installation dans l’espace de travail. L’onglet **About** affiche la version et les options de gestion.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Application installée" />
</div>
**Après cette phase :** vous disposez d’une boucle de développement en direct. Modifiez n’importe quel fichier dans `src/` et il apparaît dans l’interface utilisateur.
### Synchronisation ponctuelle pour la CI et les scripts
Utilisez `plan` et `apply` pour exécuter une fois le même pipeline, sans surveillance :
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| Commande | Comportement | Quand l'utiliser : |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `yarn twenty dev` | Surveille et resynchronise à chaque modification. Reste en cours d’exécution jusqu’à ce que vous l’arrêtiez. | Développement local interactif. |
| `yarn twenty apply` | Une seule opération de build + synchronisation, se termine avec le code `0` en cas de réussite et `1` en cas d’échec. Demande une confirmation pour les modifications destructrices (passez `--force` pour l’ignorer). | CI, hooks de pré-commit, agents IA et flux de travail scriptés. |
| `yarn twenty plan` | Construit et affiche les modifications de métadonnées **sans les appliquer**. | Inspection de ce qu’une synchronisation changerait avant de s’y engager. |
Tous les modes nécessitent un serveur distant authentifié. Voir [synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) pour plus d’informations sur `plan`.
<Note>
`yarn twenty dev --once` et `yarn twenty dev --once --dry-run` sont des alias obsolètes pour `yarn twenty apply` et `yarn twenty plan`.
</Note>
### Options du mode de développement
| Option | Description |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `--force` | Applique les modifications destructrices (suppressions) sans confirmation. |
| `--debounceMs \<ms>` | Définir le délai de temporisation des modifications de fichiers en millisecondes (valeur par défaut : `1000`). |
| `--verbose` / `--debug` | Afficher des journaux de build détaillés, les requêtes de synchronisation et les traces d’erreur. |
## Ce que vous pouvez créer
Les applications sont composées d’**entités** — chacune définie comme un fichier TypeScript avec un seul `export default` :
| Entité | Ce que cela fait |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Objets et champs** | Modèles de données personnalisés (carte postale, facture, etc.) avec des champs typés |
| **Fonctions logiques** | Fonctions TypeScript côté serveur déclenchées par des routes HTTP, des tâches cron ou des événements de base de données |
| **Composants d’interface utilisateur (UI)** | Composants React qui s’affichent dans l’interface de Twenty (panneau latéral, widgets, menu de commandes) |
| **Compétences et agents** | Fonctionnalités d’IA — instructions réutilisables et assistants autonomes |
| **Vues et navigation** | Vues de liste préconfigurées et éléments de menu de la barre latérale |
| **Mises en page** | Pages de détail d’enregistrement personnalisées avec des onglets et des widgets |
Référence complète : [Concepts](/l/fr/developers/extend/apps/getting-started/concepts).
## Prochaines étapes
<CardGroup cols={2}>
<Card title="Configuration" icon="screwdriver-wrench" href="/l/fr/developers/extend/apps/config/overview">
Identité de l’application, rôle par défaut, hooks d’installation et de désinstallation, ressources publiques.
</Card>
<Card title="Données" icon="database" href="/l/fr/developers/extend/apps/data/overview">
Objets, champs et relations bidirectionnelles.
</Card>
<Card title="Logique" icon="bolt" href="/l/fr/developers/extend/apps/logic/overview">
Fonctions logiques, compétences, agents et connexions OAuth.
</Card>
<Card title="Disposition" icon="table-columns" href="/l/fr/developers/extend/apps/layout/overview">
Vues, navigation, mises en page, composants front-end.
</Card>
<Card title="Opérations" icon="rocket" href="/l/fr/developers/extend/apps/operations/overview">
CLI, tests, dépôts distants, CI et publication de votre application.
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: Génération
description: Générez des fichiers d'entité de manière interactive avec `yarn twenty dev:add` — objets, champs, vues, fonctions logiques et plus encore.
icon: wand-magic-sparkles
---
Au lieu de créer manuellement les fichiers d'entité, utilisez l'outil de génération interactif :
```bash filename="Terminal"
yarn twenty dev:add
```
Il vous invite à choisir un type d'entité, vous guide à travers les champs requis, puis écrit un fichier prêt à l'emploi avec un `universalIdentifier` stable et l'appel `defineEntity()` correct.
Vous pouvez également passer directement le type d'entité pour ignorer la première invite :
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Types d’entités disponibles
| Type d’entité | Commande | Fichier généré |
| --------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| Objet | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Champ | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Fonction logique | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Composant frontal | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Rôle | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Compétence | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Vue | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Élément de menu de navigation | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Mise en page | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| Onglet Mise en page | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Élément du menu de commande | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Champ de vue | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Fournisseur de connexion | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Type d'activité de la chronologie | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## Ce que génère l'outil de génération
Chaque type d'entité a son propre modèle. Par exemple, `yarn twenty dev:add object` demande :
1. **Nom (singulier)** — p. ex., `invoice`
2. **Nom (pluriel)** — p. ex., `invoices`
3. **Libellé (singulier)** — rempli automatiquement depuis le nom (p. ex., `Invoice`)
4. **Libellé (pluriel)** — rempli automatiquement (p. ex., `Invoices`)
5. **Créer une vue et un élément de navigation ?** — si vous répondez oui, l'outil génère également une vue correspondante et un lien de barre latérale pour le nouvel objet.
Les autres types d'entités ont des invites plus simples — la plupart ne demandent qu'un nom.
Le type d'entité `field` est plus détaillé : il demande le nom du champ, son libellé, son type (à partir d'une liste de tous les types de champs disponibles comme `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), et le `universalIdentifier` de l'objet cible.
## Chemin de sortie personnalisé
Utilisez l'option `--path` pour placer le fichier généré à un emplacement personnalisé :
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: Résolution des problèmes
description: Problèmes courants lors de la première exécution — Docker, version de Node, Yarn, dépendances.
icon: wrench
---
* **Erreurs Docker** — Assurez-vous que Docker Desktop (ou le démon) est en cours d’exécution avant `yarn twenty docker:start`. Le message d’erreur indiquera la bonne commande de démarrage pour votre système d’exploitation.
* **Mauvaise version de Node** — Version 24.5+ requise (`engines.node: ^24.5.0`). Vérifiez avec `node -v`.
* **Yarn 4 manquant** — Exécutez `corepack enable`.
* **Dépendances cassées** — `rm -rf node_modules && yarn install`.
* **Erreurs de `twenty-sdk` après la mise à niveau vers la v2.8.0** — il est passé de `dependencies` à `devDependencies` dans la v2.8.0. Voir [Structure du projet → Dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty dev:build` avertit au sujet de `twenty-client-sdk` dans `dependencies`** — il est fourni au moment de l'exécution par Twenty, donc il devrait être déplacé vers `devDependencies` avec `twenty-sdk`. Voir [Structure du projet → Dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
Bloqué ? Demandez de l’aide sur le [Discord de Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).

View File

@@ -0,0 +1,118 @@
---
title: Éléments du menu de commande
description: Exposez les composants frontaux comme actions rapides et entrées du menu de commande (Cmd+K) avec defineCommandMenuItem.
icon: terminal
---
Un **élément de menu de commande** est le lien entre l’utilisateur et un [front component](/l/fr/developers/extend/apps/layout/front-components). Il enregistre le composant dans le menu de commande (Cmd+K) de Twenty et, éventuellement, comme bouton d’action rapide épinglé dans le coin supérieur droit de la page.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Champs de configuration
| Champ | Obligatoire | Description |
| --------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Oui | ID unique et stable pour la commande |
| `label` | Oui | Libellé complet affiché dans le menu de commande (Cmd+K) |
| `frontComponentUniversalIdentifier` | Oui | L'`universalIdentifier` du composant frontal que cette commande ouvre |
| `shortLabel` | Non | Libellé plus court affiché sur le bouton d'action rapide épinglé |
| `icon` | Non | **Obsolète** — ignoré au profit de l’icône de l’application ; la build émet un avertissement si elle est définie |
| `isPinned` | Non | Lorsque `true`, affiche la commande comme un bouton d'action rapide dans le coin supérieur droit de la page |
| `availabilityType` | Non | Contrôle l’emplacement d’apparition de la commande : `'GLOBAL'` (toujours disponible), `'GLOBAL_OBJECT_CONTEXT'` (uniquement sur les pages avec un contexte d’objet — pages d’index et d’enregistrement), `'RECORD_SELECTION'` (uniquement lorsque des enregistrements sont sélectionnés) ou `'FALLBACK'` (affichée lorsqu’aucune autre commande ne correspond) |
| `availabilityObjectUniversalIdentifier` | Non | Restreint la commande aux pages d’un type d’objet spécifique (p. ex., uniquement sur les enregistrements « Company ») |
| `conditionalAvailabilityExpression` | Non | Une expression booléenne qui contrôle dynamiquement la visibilité (voir ci-dessous) |
## Commandes sans interface
Un élément de menu de commande associé à un [headless front component](/l/fr/developers/extend/apps/layout/front-components#headless-vs-non-headless) est la manière idiomatique de proposer une action en un clic — exécuter du code, naviguer, ou confirmer puis exécuter. La page Front Components couvre les [SDK Command components](/l/fr/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) qui gèrent le modèle action-et-démontage.
Un flux typique : un composant headless affiche `<Command execute={...} />` (voir [l’exemple complet](/l/fr/developers/extend/apps/layout/front-components#sdk-command-components)), et l’élément de menu de commande y pointe :
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## Expressions de disponibilité conditionnelle
Le champ `conditionalAvailabilityExpression` vous permet de contrôler quand une commande est visible en fonction du contexte de la page actuelle. Importez des variables typées et des opérateurs depuis `twenty-sdk` pour construire des expressions :
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` implique déjà une sélection non vide — utilisez `numberOfSelectedRecords` uniquement pour des décomptes spécifiques (par exemple `>= 2`).
</Note>
### Variables de contexte
Elles représentent l’état actuel de la page :
| Variable | Type | Description |
| ------------------------------ | --------- | -------------------------------------------------------------------- |
| `pageType` | `string` | Type de page actuel (p. ex. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Indique si le composant est rendu dans un panneau latéral |
| `numberOfSelectedRecords` | `number` | Nombre d'enregistrements actuellement sélectionnés |
| `isSelectAll` | `boolean` | Indique si "tout sélectionner" est actif |
| `selectedRecords` | `array` | Les objets d'enregistrement sélectionnés |
| `favoriteRecordIds` | `array` | IDs des enregistrements ajoutés aux favoris |
| `objectPermissions` | `object` | Autorisations pour le type d'objet actuel |
| `targetObjectReadPermissions` | `object` | Autorisations de lecture pour l'objet cible |
| `targetObjectWritePermissions` | `object` | Autorisations d'écriture pour l'objet cible |
| `featureFlags` | `object` | Drapeaux de fonctionnalité actifs |
| `objectMetadataItem` | `object` | Métadonnées du type d'objet actuel |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Indique si la vue actuelle possède un filtre de suppression logique |
### Opérateurs
Combinez des variables en expressions booléennes :
| Opérateur | Description |
| ----------------------------------- | ---------------------------------------------------------------------- |
| `isDefined(value)` | `true` si la valeur n'est pas null/undefined |
| `isNonEmptyString(value)` | `true` si la valeur est une chaîne non vide |
| `includes(array, value)` | `true` si le tableau contient la valeur |
| `includesEvery(array, prop, value)` | `true` si la propriété de chaque élément inclut la valeur |
| `every(array, prop)` | `true` si la propriété est truthy sur chaque élément |
| `everyDefined(array, prop)` | `true` si la propriété est définie pour chaque élément |
| `everyEquals(array, prop, value)` | `true` si la propriété est égale à la valeur pour chaque élément |
| `some(array, prop)` | `true` si la propriété est truthy sur au moins un élément |
| `someDefined(array, prop)` | `true` si la propriété est définie sur au moins un élément |
| `someEquals(array, prop, value)` | `true` si la propriété est égale à la valeur sur au moins un élément |
| `someNonEmptyString(array, prop)` | `true` si la propriété est une chaîne non vide sur au moins un élément |
| `none(array, prop)` | `true` si la propriété est falsy sur chaque élément |
| `noneDefined(array, prop)` | `true` si la propriété est undefined sur chaque élément |
| `noneEquals(array, prop, value)` | `true` si la propriété n'est égale à la valeur sur aucun élément |

View File

@@ -0,0 +1,849 @@
---
title: Composants frontaux
description: Créez des composants React qui s’affichent dans l’interface utilisateur (UI) de Twenty avec une isolation en bac à sable.
icon: window-maximize
---
Les composants frontaux sont des composants React qui s'affichent directement dans l'interface utilisateur de Twenty. Ils s'exécutent dans un **Web Worker isolé** en utilisant Remote DOM — votre code s'exécute dans un iframe à origine opaque et sandboxé, mais son interface utilisateur continue de s'afficher nativement dans la page plutôt que d'être confinée à cet iframe.
<Warning>
Les composants Front sont encore en cours de développement actif. Votre code s’exécute sur un DOM partiel, et non sur une véritable page de navigateur, de sorte que les utilisations avancées peuvent échouer, souvent sans message d’erreur. Voir [Limitations actuelles](#current-limitations).
</Warning>
## Où les composants frontaux peuvent être utilisés
Les composants frontaux peuvent s'afficher à trois emplacements au sein de Twenty :
* **Panneau latéral** — Les composants frontaux non-headless s'ouvrent dans le panneau latéral droit. Il s'agit du comportement par défaut lorsqu'un composant frontal est déclenché depuis le menu de commande.
* **Widgets (tableaux de bord et pages d'enregistrement)** — Les composants frontaux peuvent être intégrés comme widgets dans les [mises en page](/l/fr/developers/extend/apps/layout/page-layouts). Lors de la configuration d'un tableau de bord ou d'une page d'enregistrement, les utilisateurs peuvent ajouter un widget de composant frontal.
* **Paramètres de l'application** — Défini avec [`defineSettingsFrontComponent()`](#custom-settings-component), le composant frontal s'affiche comme une section dans l'onglet **Settings** de l'application, à la place de l'interface utilisateur par défaut de configuration des variables.
Un composant frontal seul n'est pas accessible depuis l'interface utilisateur — vous devez l'*exposer*. Les trois façons de le faire sont :
* **L'associer à un [élément de menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items)** — l'enregistre dans le menu de commande (Cmd+K) et, éventuellement, comme action rapide épinglée.
* **L'intégrer comme widget dans une [mise en page](/l/fr/developers/extend/apps/layout/page-layouts)** — le place sur la page de détails d'un enregistrement ou sur un tableau de bord.
* **Le définir avec [`defineSettingsFrontComponent()`](#custom-settings-component)** — l'affiche comme une section dans l'onglet **Settings** de l'application, à la place de l'interface utilisateur par défaut de configuration des variables.
## Exemple de base
La façon la plus rapide de voir un composant frontal en action est de l'associer à un [`defineCommandMenuItem`](/l/fr/developers/extend/apps/layout/command-menu-items), afin qu'il apparaisse comme bouton d'action rapide dans le coin supérieur droit de la page :
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Après la synchronisation avec `yarn twenty dev` (ou en exécutant une seule fois `yarn twenty apply`), l'action rapide apparaît dans le coin supérieur droit de la page :
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Bouton d'action rapide dans le coin supérieur droit" />
</div>
Cliquez dessus pour afficher le composant en ligne.
## Champs de configuration
| Champ | Obligatoire | Description |
| --------------------- | ----------- | -------------------------------------------------------------------------------- |
| `universalIdentifier` | Oui | ID unique et stable pour ce composant |
| `component` | Oui | Une fonction de composant React |
| `name` | Non | Nom d'affichage |
| `description` | Non | Description de ce que fait le composant |
| `isHeadless` | Non | Définir sur `true` si le composant n'a pas d'interface visible (voir ci-dessous) |
## Placer un composant frontal sur une page
Au-delà des commandes, vous pouvez intégrer un composant frontal directement dans une page d'enregistrement en l'ajoutant comme widget dans une **mise en page**. Voir [mises en page](/l/fr/developers/extend/apps/layout/page-layouts) pour plus de détails.
## Composant de paramètres personnalisé
Pour remplacer l'interface utilisateur générée automatiquement pour la configuration des variables dans l'onglet **Settings** de votre application par votre propre composant, définissez-le avec `defineSettingsFrontComponent` au lieu de `defineFrontComponent`. Il utilise les mêmes [champs de configuration](#configuration-fields) (sauf `isHeadless`, qui n’est pas accepté puisqu’un composant de paramètres affiche toujours une interface utilisateur visible) et marque en plus le composant comme interface de paramètres de l’application.
Le composant est affiché comme une section **à l’intérieur** de l’onglet Settings, et non comme un remplacement de l’onglet entier. Les sections gérées par le système de Twenty — mise à niveau automatique, App URL et connexions — sont toujours affichées au-dessus et ne peuvent pas être remplacées par l’application.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
Un seul composant frontal de paramètres est autorisé par application ; en déclarer plus d’un provoque l’échec de la compilation. Lorsqu’il est présent, l’onglet **Settings** de l’application affiche ce composant à la place de l’interface utilisateur de configuration des variables par défaut.
## Headless vs non-headless
Les composants frontaux existent en deux modes de rendu contrôlés par l’option `isHeadless` :
**Non-headless (par défaut)** — Le composant affiche une interface visible. Lorsqu'il est déclenché depuis le menu de commande, il s'ouvre dans le panneau latéral. Il s'agit du comportement par défaut lorsque `isHeadless` est `false` ou omis.
**Headless (`isHeadless: true`)** — Le composant se monte de façon invisible en arrière-plan. Il n'ouvre pas le panneau latéral. Les composants headless sont conçus pour des actions qui exécutent une logique puis se démontent — par exemple, lancer une tâche asynchrone, naviguer vers une page ou afficher une fenêtre modale de confirmation. Ils s'associent naturellement aux composants Command du SDK décrits ci-dessous.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
Comme le composant retourne `null`, Twenty n'affiche pas de conteneur pour celui-ci — aucun espace vide n'apparaît dans la mise en page. Le composant a toujours accès à tous les hooks et à l'API de communication de l'hôte.
## Composants Command du SDK
Le package `twenty-sdk` fournit quatre composants utilitaires Command conçus pour les composants frontaux headless. Chaque composant exécute une action au montage, gère les erreurs en affichant une notification snackbar et démonte automatiquement le composant frontal une fois terminé.
Importez-les depuis `twenty-sdk/front-component` :
* **`Command`** — Exécute un callback asynchrone via la prop `execute`.
* **`CommandLink`** — Navigue vers un chemin d'application. Props : `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Ouvre une fenêtre modale de confirmation. Si l'utilisateur confirme, exécute le callback `execute`. Props : `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Ouvre une page du panneau latéral. Les propriétés dépendent de `page` — par exemple, `ViewRecord` accepte `recordId` + `objectNameSingular` (plus un identifiant `tab` facultatif pour ouvrir l’enregistrement dans un onglet spécifique), les autres pages acceptent `pageTitle` + `pageIcon`.
Voici un exemple complet d'un composant frontal headless utilisant `Command` pour exécuter une action depuis le menu de commande :
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
Et un exemple utilisant `CommandModal` pour demander une confirmation avant l'exécution :
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/front-component';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
Et un exemple utilisant `CommandOpenSidePanelPage` pour ouvrir l’enregistrement actuel dans le panneau latéral sur un onglet spécifique. `tab` est un identifiant d’onglet de mise en page (les mises en page par défaut utilisent des identifiants comme `company-tab-emails` ou `company-tab-timeline` ; les mises en page personnalisées utilisent l’identifiant propre de l’onglet). Si l’identifiant n’existe pas dans la mise en page de l’enregistrement, l’onglet par défaut s’ouvre à la place :
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Appel d’une fonction logique
Les composants front s'exécutent côté navigateur dans un Web Worker isolé (sandboxé) à l'intérieur d'un iframe à origine opaque, tandis que les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) s'exécutent côté serveur. Il n’y a aucun appel intra-processus direct entre les deux — à la place, un composant front appelle une fonction logique via HTTP.
Une fonction logique déclarée avec `httpRouteTriggerSettings` est accessible via HTTP à son chemin de route. `RestApiClient` traite les chemins commençant par `/s/` comme des routes d’application, les résout vers l’URL à partir de laquelle vos fonctions sont servies et les authentifie avec `TWENTY_APP_ACCESS_TOKEN`.
> **Sur Twenty Cloud, les fonctions logiques déclenchées par HTTP sont servies sur un domaine dédié par espace de travail** à l’adresse `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Pour les appelants externes, copiez l’URL exacte à partir des paramètres **HTTP trigger** de la fonction ou de l’onglet **Settings** de l’application.
Un composant front sans interface (headless) peut effectuer l’appel au montage via le composant `Command`, puis se démonter automatiquement :
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
Le chemin transmis à `RestApiClient` est la propriété `httpRouteTriggerSettings.path` de la fonction logique, préfixée par `/s`. Conservez `isAuthRequired: true` ; le `TWENTY_APP_ACCESS_TOKEN` que Twenty génère pour votre composant authentifie la requête :
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` est injecté automatiquement — voir [Variables d’application](#application-variables). Comme les variables d’application secrètes ne sont jamais exposées aux composants front, conservez les clés d’API et les autres éléments sensibles dans la fonction logique, et non dans le composant front.
</Note>
### Appeler l’API REST de Twenty
Pour appeler des routes HTTP d’application ou lire et écrire des enregistrements Twenty depuis un composant frontal, utilisez `RestApiClient` depuis `twenty-client-sdk/rest`. Il envoie les chemins `/s/...` vers l’URL de base des fonctions de votre espace de travail et tous les autres chemins, y compris `/rest/...`, vers `TWENTY_API_URL`.
Il agit toujours en tant que la personne qui consulte la page. `runAs: 'application'` est une option réservée aux fonctions logiques : un composant ne reçoit jamais le jeton propre à votre application, donc le demander ici génère une erreur. Placez le travail nécessitant l'accès propre à l'application derrière une fonction logique et appelez-la à la place.
| Méthode | Description |
| --------------------------------- | ------------------------------------------------------------------------------- |
| `get(path, options?)` | Envoie une requête `GET` |
| `post(path, body?, options?)` | Envoie une requête `POST` |
| `put(path, body?, options?)` | Envoie une requête `PUT` |
| `patch(path, body?, options?)` | Envoie une requête `PATCH` |
| `delete(path, options?)` | Envoie une requête `DELETE` |
| `request(method, path, options?)` | Requête générique avec n’importe quelle méthode HTTP |
| `resolveUrl(path, options?)` | Résout un chemin vers son URL complète sans envoyer de requête (pour les liens) |
`options` accepte `headers`, `query` (un enregistrement de paramètres de chaîne de requête ; les valeurs nullish sont ignorées), et un `AbortSignal` via `signal`. Un objet `body` qui n’est pas de type `FormData` est automatiquement sérialisé en JSON. Sur un `401`, le client actualise une fois le jeton d’accès via l’hôte puis retente la requête.
Par défaut, l’URL de base et le jeton sont résolus à partir de l’environnement. Passez des valeurs de remplacement (overrides) au constructeur lorsque nécessaire — par exemple dans les tests :
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Les requêtes ayant échoué lèvent une erreur `RestApiClientError` exposant `status`, `statusText`, `url` et le `body` analysé :
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## Accéder au contexte d'exécution
Dans votre composant, utilisez les hooks du SDK pour accéder à l'utilisateur actuel, à l'enregistrement et à l'instance du composant :
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
Hooks disponibles :
| Hook | Renvoie | Description |
| --------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
| `useUserId()` | `string` ou `null` | L'ID de l'utilisateur actuel |
| `useSelectedRecordIds()` | `string[]` | Tous les ID des enregistrements sélectionnés (tableau vide si aucun n'est sélectionné) |
| `useRecordId()` | `string` ou `null` | **Obsolète.** Utilisez `useSelectedRecordIds()` à la place |
| `useFrontComponentId()` | `string` | L'ID de cette instance de composant |
| `useTimelineActivityId()` | `string` ou `null` | L’ID de l’activité actuelle de la chronologie lors du rendu d’une ligne de chronologie personnalisée |
| `useColorScheme()` | `'light'` ou `'dark'` | Schéma de couleur actif de l’interface hôte (`System` est déjà résolu) |
| `useFrontComponentExecutionContext(selector)` | variable | Accédez au contexte d'exécution complet avec une fonction sélecteur |
## Variables d'application
Les variables d'application définies dans [`defineApplication()`](/l/fr/developers/extend/apps/config/application) avec `isSecret: false` sont disponibles dans les composants front via l'utilitaire `getApplicationVariable` :
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Les variables secrètes (`isSecret: true`) ne sont **pas** exposées aux composants front. Elles sont uniquement disponibles dans les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions), qui s'exécutent côté serveur. Cela empêche l’envoi au navigateur de valeurs sensibles comme les clés d’API.
</Warning>
`getApplicationVariable` renvoie toujours une **chaîne** (ou `undefined`), quel que soit le `type` déclaré de la variable. La chaîne est sérialisée de manière cohérente selon le type (booléens sous la forme `"true"` / `"false"`, nombres sous forme de chaînes décimales, tableaux / objets en JSON), dans le même format utilisé pour la fonction logique `process.env` — analysez-la vous-même (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Voir [Types de variables](/l/fr/developers/extend/apps/config/application#variable-types).
Les variables système suivantes sont toujours disponibles via `process.env` :
| Variable | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | URL de base de l’API principale de Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Jeton à courte durée de vie dont la portée correspond à l'intersection entre le rôle de la personne connectée et celui de votre application, de sorte qu'un composant ne peut jamais faire plus que la personne qui le consulte |
### `TWENTY_FUNCTIONS_URL`
Twenty injecte également `TWENTY_FUNCTIONS_URL` dans les composants frontaux et les fonctions logiques : l’URL de base à partir de laquelle les fonctions logiques déclenchées par HTTP de votre application sont servies.
Elle existe parce que cette URL n’est pas toujours le serveur Twenty lui-même. Sur Twenty Cloud, les routes d’application sont servies sur un domaine dédié par espace de travail (`https://\<your-workspace-subdomain>.withtwenty.com`, ou le domaine public principal de l’application lorsqu’il est configuré) afin que les réponses créées par l’application s’exécutent sur une origine isolée plutôt que sur l’origine de l’application Twenty. Les instances auto-hébergées et locales servent les routes d’application sous le préfixe `/s` sur le serveur lui-même et peuvent ne pas définir la variable du tout. Comme l’URL de base varie selon l’espace de travail et l’instance, votre code ne peut pas la coder en dur — le serveur injecte la bonne valeur à l’exécution.
Vous avez rarement besoin de la lire directement. Appelez vos routes via `RestApiClient` avec un chemin préfixé par `/s/` et le client résout l’URL pour vous : il retire le préfixe `/s` et cible `TWENTY_FUNCTIONS_URL`, en revenant à `\<TWENTY_API_URL>/s` lorsque la variable n’est pas définie. Utilisez `resolveUrl('/s/\<path>')` pour obtenir l’URL absolue sans envoyer de requête, par exemple pour un lien. Lisez la variable directement uniquement lorsque vous construisez une URL manuellement :
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## API de communication de l'hôte
Les composants frontaux peuvent déclencher la navigation, des modales et des notifications en utilisant des fonctions de `twenty-sdk` :
| Fonction | Description |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Naviguer vers une page de l'application |
| `openSidePanelPage(params)` | Ouvrir un panneau latéral |
| `closeSidePanel()` | Fermer le panneau latéral |
| `openCommandConfirmationModal(params)` | Afficher une boîte de dialogue de confirmation |
| `enqueueSnackbar(params)` | Afficher une notification toast |
| `unmountFrontComponent()` | Démonter le composant |
| `updateProgress(progress)` | Mettre à jour un indicateur de progression |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Téléverse un `Blob` dans un champ FILES ; renvoie `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` ou `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }`. |
Voici un exemple qui utilise l'API hôte pour afficher une snackbar et fermer le panneau latéral après la fin d'une action :
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Stockage
`localStorage` et `sessionStorage` fonctionnent comme sur une page normale, avec l’API synchrone standard. Vos clés sont limitées à votre installation de l’application et à l’utilisateur connecté : aucune autre application ne peut les lire, et un autre utilisateur se connectant dans le même navigateur commence avec un stockage vide. Les valeurs écrites dans `localStorage` restent sur l’appareil entre les rechargements ; `sessionStorage` dure pendant la session du navigateur.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty stocke les valeurs pour le compte de votre application, de sorte qu’une écriture est appliquée localement immédiatement et sauvegardée en arrière-plan. Les lectures ne bloquent jamais sur l’hôte. Rien n’est synchronisé : les valeurs ne suivent pas l’utilisateur vers un autre navigateur ou une autre machine. Utilisez donc le [magasin clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store) d’une fonction logique pour tout ce qui doit survivre à un changement d’appareil.
Les écritures sont plafonnées, et chaque limite compte les caractères plutôt que les octets : les clés comportent au maximum 512 caractères, une valeur unique au maximum 262 144 caractères, et chaque stockage au maximum 1 048 576 caractères par application et par utilisateur. Une écriture qui dépasse une limite lève une `QuotaExceededError`, comme avec l’API du navigateur.
### Travailler avec plusieurs enregistrements
Utilisez `useSelectedRecordIds()` pour gérer plusieurs enregistrements sélectionnés. C'est utile pour les opérations groupées :
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
Affichez-la avec un [élément de menu de commande](/l/fr/developers/extend/apps/layout/command-menu-items) limité aux sélections d'enregistrements :
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Ressources publiques
Les composants frontaux peuvent accéder aux fichiers du répertoire `public/` de l'application à l'aide de `getPublicAssetUrl` :
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
Voir la [section sur les ressources publiques](/l/fr/developers/extend/apps/config/public-assets) pour plus de détails.
## Partage des dépendances entre les composants frontaux
Par défaut, chaque composant frontal contient sa propre copie des bibliothèques qu'il importe, donc une application avec cinq composants envoie React cinq fois. Déclarer les dépendances partagées dans le `package de votre application. son` pour construire ces bibliothèques une fois et que chaque composant de l'application les charge à partir d'un fichier mis en cache:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Chaque composant importe ensuite ses dépendances exactement comme avant, rien ne change dans le code de votre composant:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Quelques points à connaître :
* **Un lot de dépendances partagées par application.** Le bundle est construit à partir des propres dépendances de votre application, de sorte que vous gardez le contrôle total des versions que vous envoyez.
* **Lister les spécifications exactes que vous importez.** `vingt-ui/input` et `vingt-ui/display` sont deux entrées; un nom de paquet seul ne couvre pas ses sous-chemins. La liste `react` couvre automatiquement `react/jsx-runtime`.
* **Partagez `react-dom/client` avec `react`.** Chaque composant est rendu via `createRoot`, donc le laisser dehors signifie que chaque composant contient toujours React DOM.
* \*\*Le bundle est mis en cache. \* Il est servi sous une URL de hachage de contenu avec une cache immuable de longue durée, ainsi il est téléchargé une fois et réutilisé dans tous les composants de l'application jusqu'à ce que l'une de ses dépendances change.
* **Les composants qui n'importent aucun des paquets partagés ne le téléchargent jamais.**
## Stylisation
Les composants frontaux prennent en charge plusieurs approches de stylisation. Vous pouvez utiliser :
* **Styles en ligne** — `style={{ color: 'red' }}`
* **Composants d’interface utilisateur Twenty** — la bibliothèque de composants propre à Twenty ; voir [Utilisation des composants d’interface utilisateur Twenty](#using-twenty-ui-components) ci-dessous
* **Emotion** — CSS-in-JS avec `@emotion/react`
* **Styled-components** — modèles `styled.div`
* **Tailwind CSS** — classes utilitaires
* **Toute bibliothèque CSS-in-JS** compatible avec React
## Utilisation des composants d’interface utilisateur Twenty
Twenty fournit sa bibliothèque de composants sous forme de package [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). Les composants frontaux peuvent l’utiliser pour les boutons, tags, pastilles de statut, chips, avatars, icônes, typographie et jetons de thème qui s’adaptent automatiquement aux thèmes clair et sombre de l’espace de travail.
### Installation
Ajoutez le package à votre application, épinglé à la version livrée avec votre instance Twenty :
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` est intégré à votre composant front au moment du build, il n’a donc besoin d’être qu’une dépendance de votre application — il n’y a rien à configurer à l’exécution.
### Importation de composants
Importez depuis le sous-chemin correspondant plutôt que depuis la racine du package, afin que seuls les composants que vous utilisez se retrouvent dans votre bundle :
| Sous-chemin | Ce qu’il exporte |
| --------------------------- | -------------------------------------------------------- |
| `twenty-ui/input` | `Button` et champs de formulaire |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar`, et plus encore |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info`, et plus encore |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label`, et plus encore |
| `twenty-ui/icon` | Composants `Icon*` (par exemple `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
### Icônes
Importez des icônes individuelles depuis `twenty-ui/icon` :
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Chaque icône nommée bénéficie du tree-shaking ; en importer quelques-unes n’ajoute donc que très peu à votre bundle. Évitez `IconsProvider`, `useIcons` et `iconsState` — ils intègrent l’ensemble complet d’icônes Tabler (plusieurs Mo).
### Thématisation et jetons de thème
Les composants Twenty UI correspondent automatiquement au thème clair et sombre de l’espace de travail — le moteur de rendu applique le jeu de couleurs actif sur l’hôte, et les composants résolvent leurs couleurs par rapport à celui-ci.
Pour utiliser les mêmes jetons de design dans vos propres styles inline, appelez le hook `useTheme()`. Il renvoie les jetons de thème de Twenty (espacement, couleurs, rayons, polices) connectés au thème actif, sans qu’aucune configuration de `ThemeProvider` ne soit nécessaire dans votre composant :
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Comme `useTheme()` est un hook, vous lisez les jetons à l’intérieur du corps du composant, de sorte que les valeurs reflètent toujours le thème en cours. La même table de jetons est également exportée en tant que constante `themeCssVariables`, mais privilégiez `useTheme()` dans les composants front — une constante au niveau du module qui déréférence `themeCssVariables` peut être indéfinie pendant l’extraction du manifeste de l’application.
Pour bifurquer explicitement selon le jeu de couleurs actif, lisez-le avec `useColorScheme()` depuis `twenty-sdk/front-component`, qui renvoie `'light'` ou `'dark'`.
## Limitations actuelles
Les composants Front sont en cours de développement actif. Le rendu, le style, la gestion des événements, la mesure des éléments et le stockage du navigateur fonctionnent bien. Tout ce qui va *au-delà* de ces éléments (appeler une méthode du DOM sur une ref, observer les redimensionnements d’éléments, créer un portail en dehors de votre arbre) est manquant ou incomplet aujourd’hui, et la plupart de ces opérations échouent silencieusement : aucune exception, et aucune erreur TypeScript non plus, puisque l’échafaudage est typé par rapport au DOM complet du navigateur.
Si l’un de ces points vous bloque, [ouvrez un ticket](https://github.com/twentyhq/twenty/issues/new/choose) afin qu’il soit priorisé.
### Disposition et mesure
Les éléments peuvent se mesurer eux-mêmes : l’hôte reflète la géométrie dans le bac à sable, de sorte que les lectures sont traitées localement mais peuvent avoir jusqu’à une image de retard, et la première lecture d’un élément jamais mesuré renvoie des zéros. Après l’écriture, relisez dans un callback `requestAnimationFrame` ou dans un effet.
| API | Ce qui se passe |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Fonctionne, depuis le miroir ; affecter `scrollTop` / `scrollLeft` est une opération nulle (no-op) |
| `getClientRects()`, `window.matchMedia()` | Lève une exception |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Fonctionnent, en rapportant le **viewport du navigateur** ; la taille propre de votre widget est `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Renvoie uniquement les styles inline de l’élément, jamais la cascade calculée de l’hôte |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (les gardes `typeof` fonctionnent) |
| `MutationObserver` | Fonctionne, y compris `subtree`, `attributeFilter`, les anciennes valeurs et `takeRecords()` |
Le positionnement via `getBoundingClientRect` fonctionne désormais, mais tout ce qui surveille les changements de taille via `ResizeObserver` (le `ResponsiveContainer` de recharts, `autoUpdate` de Floating UI) ne fonctionne toujours pas. Préférez de toute façon le CSS pour la mise en page : votre feuille de style atteint la vraie page, donc flexbox, grid, `aspect-ratio`, `clamp()` et `@container` se comportent normalement, sans latence de trame.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` et `queueMicrotask` fonctionnent sans le préfixe `window.`. Seuls `window.requestAnimationFrame(...)` et les fonctions similaires lèvent une exception.
</Note>
### Accès au DOM
Une `ref` vous donne un élément du bac à sable, pas un `HTMLElement`.
| Ce que vous écrivez | Ce qui se passe | À utiliser à la place |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Lève une exception | Composants contrôlés ; lisez les valeurs à partir de `event.target` |
| `element.classList.add(...)` | Lève une exception (`classList` est `undefined`) | Construisez vous-même la chaîne `className` |
| `document.createTreeWalker()` | Lève une exception | `querySelector()` / `querySelectorAll()` et `getElementById()` fonctionnent ; `getElementsByClassName()` fonctionne aussi, mais renvoie une collection statique, et non une `HTMLCollection` vivante |
| `document.activeElement` | Toujours `undefined` | Suivez le focus avec `onFocus` / `onBlur` |
| `canvas` | Ne rend rien, pas d’erreur | SVG, ou dessiner hors écran et afficher le résultat dans un élément `img` |
| `createPortal(node, document.body)` | Ne rend rien, tandis que `isConnected` signale un succès | Superpositions en ligne avec `position: absolute`, ou passez à la bibliothèque votre propre élément conteneur |
Cet écart lié au portail est la raison pour laquelle les popovers de Radix, Headless UI, MUI et react-select ne rendent rien par défaut. La plupart acceptent une prop de conteneur ; faites-la pointer vers un élément que vous avez rendu.
### Événements
La souris, le pointeur, le toucher, le glisser, le clavier, le focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` et `animationend`/`transitionend` sont transmis à l’hôte, plus quelques événements propres à chaque élément : `load`/`error` sur `img`, le presse-papiers et la composition sur `input`/`textarea`, les médias sur `video`/`audio`, `toggle` sur `details`/`dialog`. Tout le reste (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, la capture de pointeur, `onLoad` sur `img`) est ignoré sans avertissement.
`document.addEventListener()` et `window.addEventListener()` s’enregistrent sans erreur mais ne se déclenchent jamais, c’est pourquoi un glisser-déposer s’arrête dès que le pointeur quitte l’élément sur lequel il a commencé. `event.preventDefault()` ne traverse pas non plus ; l’envoi de formulaire, `dragover`/`drop` et les clics sur les liens sont déjà protégés pour vous.
### Attributs et styles
Chaque élément transfère ses propres propriétés au DOM hôte (`href` sur `a`, `src`/`alt` sur `img`, `value`/`placeholder`/`disabled` sur `input`, etc.), plus un ensemble commun sur chaque élément : `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` et tout attribut `aria-*` / `data-*` (avec tiret, donc `ariaLabel` est ignoré). Tout ce qui sort de ce cadre est silencieusement supprimé, donc exprimez l’état personnalisé sous forme de `data-*`.
Le CSS du composant, qu’il provienne de `import './styles.css'`, de CSS-in-JS ou d’un élément `style`, est injecté dans le `head` de la page hôte **sans portée**. Ainsi, les noms de classes entrent en collision avec ceux de Twenty (préfixez-les, et n’écrivez jamais de `div { ... }` sélecteurs), et `@media` correspond à la fenêtre du navigateur plutôt qu’à votre widget (utilisez `@container` avec votre propre `container-type`). Les props `style` en ligne ne sont pas affectées.
### Stockage et réseau
`localStorage` et `sessionStorage` sont fournis par Twenty plutôt que par le navigateur : le composant s’exécute dans un worker avec une origine opaque, donc l’hôte stocke les valeurs pour le compte de votre application. Voir [stockage](#storage) pour leur portée et leurs limites. IndexedDB, les cookies, l’API Cache et `BroadcastChannel` restent indisponibles. Pour conserver l’état entre les appareils, appelez une [fonction logique](/l/fr/developers/extend/apps/logic/logic-functions) et utilisez son [magasin clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store).
`fetch` fonctionne, avec quelques réserves :
* Les appels à l’API Twenty et aux routes de votre application sont proxifiés par l’hôte, donc privilégiez [`RestApiClient`](#calling-the-twenty-rest-api). Sur les appels proxifiés, `AbortSignal` et les autres options `RequestInit` sont ignorées, et seuls les corps `string` et `URLSearchParams` sont pris en charge.
* Les autres origines quittent le bac à sable avec `Origin: null`, donc une API tierce répond uniquement si elle envoie `Access-Control-Allow-Origin: *`. Appelez-la plutôt depuis une fonction logique.
* `fetch('/rest/people')` n’est jamais associé à l’API Twenty, car le bac à sable n’a pas d’URL de page pour résoudre un chemin relatif.
### Capture de médias
`navigator.mediaDevices.getUserMedia()` et `MediaRecorder` fonctionnent à l’intérieur des composants front-end grâce à des polyfills de sandbox, de sorte que le code d’enregistrement standard s’exécute sans modification et que `MediaRecorder.isTypeSupported` indique si les combinaisons courantes de conteneur/codec sont prises en charge. Des objets de contraintes `getUserMedia` détaillés sont acceptés mais non transmis — l’hôte capture avec ses valeurs par défaut pour les types demandés — et une seule capture peut être active à la fois entre les applications. Stockez un `Blob` enregistré avec la fonction hôte `uploadFile`.
### Autres lacunes
* **Contenu de fichier.** Un `input` de type `file` fournit uniquement à votre gestionnaire les métadonnées du fichier, pas les octets, donc `FileReader` n’est pas disponible. Pour téléverser un `Blob` dont votre code dispose déjà — par exemple celui produit par `MediaRecorder` — utilisez la fonction hôte `uploadFile`.
* **Charges utiles de glisser-déposer.** Les événements de glisser-déposer se déclenchent, mais `event.dataTransfer` est `undefined`.
* **Modules natifs Node.** `fs`, `path` et `node:crypto` font échouer la construction, donc déplacez ce travail dans une [fonction logique](/l/fr/developers/extend/apps/logic/logic-functions). Web Crypto, `fetch`, `TextEncoder` et `URL` sont disponibles.
* **`iframe`** est toujours à nouveau placé dans un bac à sable sans `allow-same-origin`, donc une intégration qui repose sur sa propre session s’affiche comme déconnectée. Il n’a pas non plus de `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Éléments de menu de navigation
description: Ajoutez des entrées personnalisées à la barre latérale de l’espace de travail — des liens vers des vues enregistrées ou des URL externes.
icon: bars
---
Un **élément de menu de navigation** est une entrée dans la barre latérale gauche. Utilisez `defineNavigationMenuItem()` pour ajouter des liens personnalisés dans la barre latérale — généralement un par [vue](/l/fr/developers/extend/apps/layout/views) — ou pour pointer vers des URL externes.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## Points clés
* `type` détermine la destination de l’élément de menu. Chaque type est associé à un champ d’identifiant spécifique :
| Type | Ce que cela fait | Champ obligatoire |
| ------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `NavigationMenuItemType.VIEW` | Ouvre une vue enregistrée | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Ouvre une URL externe | `link` |
| `NavigationMenuItemType.FOLDER` | Regroupe les éléments imbriqués sous une étiquette | `name` (et les éléments enfants font référence au dossier via `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Ouvre la page d’index par défaut d’un objet | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Ouvre une mise en page autonome | `pageLayoutUniversalIdentifier` |
* `position` contrôle l’ordre dans la barre latérale.
* L’énumération contient également `NavigationMenuItemType.RECORD`, utilisé en interne pour les favoris d’enregistrements créés par l’utilisateur — il n’est pas utilisable depuis un manifeste d’application (il n’existe aucun champ pour référencer un enregistrement).
* `icon` et `color` sont facultatifs et personnalisent l’apparence de l’entrée.
* `folderUniversalIdentifier` est également disponible sur n’importe quel élément pour l’imbriquer dans un parent de type `FOLDER`.
<Note>
**Piège courant :** créer un objet sans élément de menu de navigation associé rend cet objet inatteignable depuis la barre latérale. À moins qu’il ne s’agisse d’un objet technique/interne, chaque objet personnalisé devrait avoir une entrée dans la barre latérale qui renvoie vers cet objet.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Vue d'ensemble
description: Placez votre application à l’intérieur de l’interface de Twenty — entrées de barre latérale, vues enregistrées, onglets de page d’enregistrement et composants React isolés.
icon: table-columns
---
La **couche de mise en page** d’une application Twenty est tout ce que l’utilisateur voit : où l’application apparaît dans la barre latérale, quelles vues de liste elle fournit, comment ses pages de détails d’enregistrement sont organisées et quels composants React personnalisés sont rendus à l’intérieur de ces pages.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Vues" icon="list" href="/l/fr/developers/extend/apps/layout/views">
`defineView` — configurations de listes enregistrées : colonnes visibles, filtres, groupes.
</Card>
<Card title="Éléments de menu de navigation" icon="bars" href="/l/fr/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — entrées de barre latérale pointant vers des vues ou des URL externes.
</Card>
<Card title="Mises en page" icon="table-columns" href="/l/fr/developers/extend/apps/layout/page-layouts">
`definePageLayout` et `definePageLayoutTab` — onglets et widgets sur la page de détails d’un enregistrement.
</Card>
<Card title="Composants frontaux" icon="window-maximize" href="/l/fr/developers/extend/apps/layout/front-components">
`defineFrontComponent` — composants React isolés rendus à l’intérieur de Twenty.
</Card>
<Card title="Éléments du menu de commande" icon="terminal" href="/l/fr/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — enregistre des composants frontaux comme entrées Cmd+K et actions rapides.
</Card>
</CardGroup>
## Où l’application apparaît
| Surface | Ce qu'il contrôle | Entité |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Barre latérale** | Une entrée personnalisée pointant vers une vue enregistrée ou une URL externe | `defineNavigationMenuItem` |
| **Liste des enregistrements** | Une configuration enregistrée pour un objet — colonnes visibles, ordre, filtres, groupes | `defineView` |
| **Page de détails de l’enregistrement** | Les onglets et widgets sur une page d’enregistrement (celle de votre propre objet ou une page standard) | `definePageLayout`, `definePageLayoutTab` |
| **À l’intérieur de n’importe lequel des éléments ci-dessus** | Un widget React personnalisé — boutons, formulaires, tableaux de bord, intégrations | `defineFrontComponent` |
| **Paramètres de l’application** | Une section de configuration personnalisée dans l’onglet Paramètres de l’application, au lieu de l’interface utilisateur par défaut des variables | `defineSettingsFrontComponent` |
| **Menu de commande (Cmd+K)** | Une action rapide épinglée ou une commande masquée | `defineCommandMenuItem` |
Les composants frontaux s’exécutent à l’intérieur d’un Web Worker isolé en utilisant Remote DOM — ils sont rendus *nativement* dans la page (et non dans une iframe), mais ne peuvent pas accéder directement à la page hôte ou au DOM. La communication avec Twenty se fait via une API hôte de passage de messages.
Elles sont encore en cours de développement actif : le bac à sable implémente un DOM partiel, donc les usages avancés peuvent échouer. Voir [Limitations actuelles](/l/fr/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Mises en page de la page
description: Personnalisez les pages de détail des enregistrements — onglets, widgets et emplacement de rendu des composants front-end — à l'aide de `definePageLayout` et `definePageLayoutTab`.
icon: table-columns
---
Une mise en page contrôle la façon dont la page de détail d'un enregistrement est organisée : quels onglets apparaissent et quels widgets ils contiennent. Utilisez `definePageLayout()` pour déclarer une mise en page pour un objet que vous possédez, ou `definePageLayoutTab()` pour ajouter un seul onglet à une mise en page qui existe déjà (la vôtre ou une mise en page standard de Twenty).
| Cas d'utilisation | Entité |
| -------------------------------------------------------------------------------------------------------------- | --------------------- |
| Définir l'intégralité de la mise en page de la page de détail des enregistrements d'un objet que vous possédez | `definePageLayout` |
| Ajouter un onglet à une mise en page existante (votre propre objet ou un objet standard) | `definePageLayoutTab` |
## definePageLayout
Utilisez ceci lorsque vous possédez l'intégralité de la page de détail — généralement pour un objet personnalisé que vous avez défini vous-même.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### Points clés
* `type` est l'un de `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` ou `'STANDALONE_PAGE'`. Utilisez `'RECORD_PAGE'` pour personnaliser la vue détaillée d'un objet spécifique.
* `objectUniversalIdentifier` spécifie à quel objet cette mise en page s'applique.
* Chaque `tab` définit une section de la page avec un `title`, une `position` et un `layoutMode` : `VERTICAL_LIST` pour les pages d'enregistrement et les pages autonomes, `GRID` pour les tableaux de bord et `CANVAS` pour un seul widget qui doit remplir la zone d'affichage de l'onglet. Un onglet `VERTICAL_LIST` empile les widgets verticalement. Les widgets intégrés qui gèrent leur propre défilement, tels que les chronologies, les fichiers, les notes, les tâches et les workflows, remplissent une zone d'affichage ; les champs, les composants front, les graphiques et les autres widgets fit-content s'affichent à la hauteur de leur contenu ou à la hauteur configurée. Un onglet `GRID` dispose toujours ses widgets sous forme de cartes sur une grille de 12 colonnes. Un widget `CANVAS` n’a pas de position explicite ; si un onglet de canevas contient plusieurs widgets, ils s’affichent à la hauteur de leur contenu au lieu de remplir la fenêtre d’affichage.
* Définissez `layoutMode` explicitement. Si vous l’omettez, vous obtenez `VERTICAL_LIST` sur une `STANDALONE_PAGE` et `GRID` partout ailleurs, ce qui est rarement ce que vous voulez sur une page d’enregistrement.
* Chaque `widget` à l'intérieur d'un onglet peut afficher un [front component](/l/fr/developers/extend/apps/layout/front-components), une liste de relations ou d'autres types de widgets intégrés.
* Un widget `FRONT_COMPONENT` peut définir `headerCommandMenuItemUniversalIdentifiers` sur un tableau ordonné d’identifiants universels d’éléments du menu de commandes provenant de la même app. Ces actions apparaissent sous forme de boutons d’icône dans l’en-tête de la carte du widget et conservent leur disponibilité au niveau de la commande ainsi que leurs vérifications des autorisations. Les identifiants doivent être uniques et doivent pouvoir être résolus lors de l’installation de l’app.
* La `position` des onglets contrôle leur ordre. Utilisez des valeurs plus élevées (p. ex., 50) pour placer les onglets personnalisés après les onglets intégrés.
### Widgets des champs
Un widget `FIELD` affiche un champ de l’enregistrement. Pour les champs de relation, il peut également intégrer une liste d’enregistrements liés :
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` prend l’identifiant universel d’un champ sur l’objet de la mise en page.
* `fieldDisplayMode` est l’une des valeurs suivantes : `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` ou `'TABLE'`. `TABLE` intègre une vue listant les enregistrements d’un champ de relation un-à-plusieurs.
* `nestedRelationFieldMetadataId` est facultatif et prend l’identifiant universel d’un champ de relation un-à-plusieurs sur l’objet cible de la relation, afin de lister des enregistrements à deux sauts de relation (par exemple, une page Entreprise listant les opportunités des personnes de l’entreprise, ou une page Personne listant les opportunités de l’entreprise de la personne). Le premier saut peut être un champ de relation un-à-plusieurs ou plusieurs-à-un, le second doit être un-à-plusieurs (les relations de jonction ne sont pas prises en charge), et cela nécessite `fieldDisplayMode: 'TABLE'` — le combiner avec tout autre mode d’affichage constitue une erreur de validation, puisqu’un widget imbriqué est toujours rendu comme une vue intégrée.
## definePageLayoutTab
Utilisez ceci lorsque vous voulez uniquement ajouter un onglet à une mise en page existante — par exemple, un onglet d'analyse sur la page standard Company, ou un onglet de résumé d'IA rattaché à la mise en page de votre propre objet.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Points clés
* `pageLayoutUniversalIdentifier` est **obligatoire** et doit pointer vers une mise en page de page qui existe déjà au moment de l'installation — soit une mise en page standard de Twenty, soit une mise en page définie par votre propre application. Les références inter-applications aux mises en page appartenant à une autre application installée ne sont pas prises en charge aujourd'hui. Lorsque la mise en page parente est manquante, l'installation échoue avec une erreur de validation explicite.
* Pour les mises en page standard de Twenty, importez les identifiants depuis `twenty-sdk/define` :
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Chaque entrée de mise en page expose également ses `tabs` et leurs `widgets`, afin que vous puissiez faire référence à n’importe quel niveau :
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Un alias court `STANDARD_PAGE_LAYOUT` est également disponible :
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* Les `widgets` sont limités à cet onglet uniquement — ils font référence aux [front components](/l/fr/developers/extend/apps/layout/front-components), aux vues, etc., exactement comme les widgets définis en ligne dans `definePageLayout`.
* `position` contrôle l'ordre par rapport aux onglets existants sur la mise en page ciblée. Choisissez une valeur qui place votre onglet à l'endroit souhaité par rapport aux onglets intégrés.
* Utilisez ceci plutôt que `definePageLayout` lorsque vous voulez seulement ajouter à une mise en page existante. Utilisez `definePageLayout` lorsque vous possédez l'intégralité de la mise en page.

View File

@@ -0,0 +1,113 @@
---
title: Vues
description: Livrez des vues enregistrées préconfigurées — ordre des colonnes, filtres, groupes — pour les objets de votre application.
icon: list
---
Une **vue** est une configuration enregistrée pour la façon dont les enregistrements d’un objet sont affichés : quels champs apparaissent, leur ordre, s’ils sont visibles, ainsi que les filtres ou groupes appliqués. Utilisez `defineView()` pour fournir des vues préconfigurées avec votre application.
Chaque objet obtient automatiquement sa vue de liste principale (le tableau « All ... » ouvert par un élément de navigation `OBJECT`), provisionnée par le serveur — y compris les objets que votre application crée, avec une colonne par champ que votre application déclare. Les vues que vous définissez avec `defineView()` sont toujours des vues **supplémentaires** en plus de cette vue par défaut.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## Points clés
* `objectUniversalIdentifier` spécifie à quel objet cette vue s'applique. Il peut s’agir d’un objet personnalisé que vous avez défini ou d’un objet Twenty standard.
* La vue de liste principale de l’objet appartient au serveur : `key` est obsolète et ignorée, de sorte qu’une vue définie dans le manifeste ne peut jamais la revendiquer. Ajoutez un élément de navigation `VIEW` si vous voulez que votre vue soit à un clic dans la barre latérale.
* `fields` contrôle les colonnes affichées et leur ordre. Chaque champ référence un `fieldMetadataUniversalIdentifier`. Pour faire référence à un champ système créé automatiquement tel que `createdAt`, voir [Ciblage des champs système](/l/fr/developers/extend/apps/data/system-fields).
* Vous pouvez également définir `filters`, `filterGroups`, `sorts`, `groups` et `fieldGroups` pour des configurations plus avancées.
* `position` contrôle l’ordre lorsqu’il existe plusieurs vues pour le même objet.
## Propriétés optionnelles
| Propriété | Valeurs | Description |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (par défaut), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Comment les enregistrements sont disposés. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` et `CALENDAR_WIDGET` existent également mais sont utilisés en interne par les widgets de mise en page.) |
| `visibility` | `ViewVisibility.WORKSPACE` (par défaut), `ViewVisibility.UNLISTED` | Indique si la vue est affichée pour l’ensemble de l’espace de travail ou masquée des sélecteurs. |
| `openRecordIn` | obsolète | N’est plus pris en compte : l’endroit où s’ouvrent les enregistrements est désormais une propriété de l’[objet](/l/fr/developers/extend/apps/data/objects) (`openRecordIn` sur `defineObject()`), avec repli sur la préférence de chaque membre. |
| `tris` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordre de tri par défaut. |
| `isCompact` | `boolean` | Affichage compact des lignes. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Regrouper les enregistrements (par exemple, les colonnes kanban) par un champ. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agrégats et dimensionnement des colonnes Kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Vues de calendrier : disposition, champ de date qui positionne les enregistrements et champ de date de fin facultatif. |
Tous les enums ci-dessus sont exportés depuis `twenty-sdk/define`.
## Filtres
Une vue peut être livrée avec des filtres préappliqués. Chaque filtre possède trois coordonnées : le **champ** faisant l’objet du filtrage, l’**opérateur** (comment comparer) et la **valeur** (par rapport à quoi comparer). Les trois doivent être alignées : l’utilisation d’un opérateur qui ne s’applique pas à un type de champ sera rejetée au moment de la synchronisation.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Opérateurs pris en charge par type de champ
| Type de champ | Opérandes pris en charge |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Les types de champs avec des noms similaires peuvent utiliser des opérandes complètement différents — `SELECT` et `MULTI_SELECT` étant un cas courant.
### Forme de valeur par opérande
Le champ `value` est toujours une valeur sérialisable en JSON, mais sa forme attendue dépend de l’opérande :
| Famille d’opérandes | Forme de la valeur | Exemple |
| ----------------------------------------------------------- | ------------------------------------ | ------------------------ |
| `IS`, `IS_NOT` sur `SELECT` | tableau de clés d’option (chaînes) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` sur `MULTI_SELECT` | tableau de clés d’option (chaînes) | `['TAG_A']` |
| `IS`, `IS_NOT` sur `RELATION` | tableau d’ID d’enregistrement (UUID) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` sur les champs de type texte | chaîne | `'acme'` |
| `IS`, `IS_NOT` sur `NUMBER` | chaîne (la valeur) | `'5'` |
| `IS` sur `RATING` / `UUID` | chaîne (la valeur) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | chaîne (la borne) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` sur `DATE` / `DATE_TIME` | chaîne ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | chaîne vide | `''` |
| `IS` sur `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
## Comment les vues apparaissent dans l’interface utilisateur
Une vue seule n’est pas accessible à partir de la barre latérale. Pour la faire apparaître là, associez-la à un [élément de menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items) de type `VIEW` qui pointe vers le `universalIdentifier` de la vue. C’est le schéma canonique : chaque objet personnalisé obtient une vue d’index par défaut générée automatiquement lors de sa création ; il vous suffit donc d’inclure l’élément de la barre latérale qui l’ouvre.

View File

@@ -0,0 +1,197 @@
---
title: Tâches en arrière-plan
description: Confiez les travaux longs ou soumis à des limitations de débit aux workers de Twenty en mettant en file d’attente une autre exécution de fonction logique au lieu de tout faire en ligne.
icon: layer-group
---
Une exécution de fonction logique est limitée par son `timeoutSeconds` (900 secondes maximum). Tout ce qui ne peut pas se terminer dans cette fenêtre — une resynchronisation complète, une diffusion par enregistrement, une API tierce qui vous applique des limitations de débit — doit être découpé en exécutions plus petites.
`enqueueJobs` fait exactement cela : il demande aux workers de Twenty d’exécuter plus tard l’une des fonctions logiques de votre application, une fois par charge utile, chaque exécution dans son propre processus avec son propre budget de délai d’expiration. La fonction appelante retourne immédiatement.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Mettre des exécutions en file d’attente
Importez `enqueueJobs` depuis `twenty-sdk/logic-function`, pointez-le vers le `universalIdentifier` de la fonction logique à exécuter et transmettez une charge utile par exécution pour la mise en file d’attente.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Chaque exécution reçoit sa charge utile comme argument du gestionnaire, exactement comme pour tout autre déclencheur. La cible doit appartenir à la **même application** que l’appelant — la mise en file d’attente de la fonction d’une autre application est rejetée avec `Logic function not found` et rien n’est mis en file d’attente. Un seul appel accepte jusqu’à `200` charges utiles.
<Note>
`enqueueJobs` renvoie dès que les jobs sont acceptés, et non pas lorsqu’ils ont été exécutés. Il ne renvoie pas les résultats des cibles — faites en sorte que chaque cible écrive ce qu’elle produit dans le [key-value store](/l/fr/developers/extend/apps/logic/key-value-store) ou dans un enregistrement d’espace de travail si vous devez les lire à nouveau.
</Note>
<Note>
L’ancien assistant `enqueueJob`, qui met en file d’attente un seul job par appel, est obsolète. Utilisez plutôt `enqueueJobs` avec une liste `payloads` à un élément.
</Note>
## Options du job
Les options s’appliquent à chaque exécution du lot.
| Option | Par défaut | Plage | Ce que cela fait |
| ------------ | ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Nombre total de tentatives supplémentaires dans la file d'attente. Les nouvelles tentatives demandées par l'application sont limitées à `3`. N’augmentez cette valeur que pour les gestionnaires qui peuvent être exécutés deux fois en toute sécurité. |
| `delayMs` | `0` | `0`–`604800000` (7 jours) | Attendez ce délai avant que les exécutions deviennent éligibles. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**La priorité n’est pas encore configurable.** Les jobs mis en file d’attente s’exécutent toujours avec la priorité la plus basse, de sorte que le travail de la plateforme n’est jamais retardé derrière les jobs des applications. Le contrôle de la priorité arrive bientôt.
</Note>
L’exécution mise en file d’attente hérite de l’utilisateur exécutant la fonction qui l’a mise en file d’attente, elle agit donc avec les mêmes autorisations.
## Réessayer après un échec transitoire.
Twenty ne réessaie pas toutes les exceptions du code d'application. Une erreur ordinaire levée est traitée comme un échec permanent. En cas d'échec transitoire, une fonction logique mise en file d'attente peut demander jusqu'à trois nouvelles tentatives en levant `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Levez directement `RetryableLogicFunctionError` lorsque cela est possible. Si vous l'étendez, ne remplacez pas son `name` : Twenty reconnaît le nom sérialisé `RetryableLogicFunctionError` dans les environnements d'exécution.
`retryCount` est égal à `0` lors de l'exécution initiale et n'augmente que lorsque le code d'application demande une nouvelle tentative. `maxRetries` est au maximum de `3` et peut être inférieur lorsque la tâche mise en file d'attente a une limite globale de nouvelles tentatives plus faible. Les défaillances de la plateforme n'augmentent pas `retryCount`, bien qu'elles consomment toujours le budget de sécurité global de la file d'attente.
La file d'attente retarde les tentatives avec un délai exponentiel et une gigue. Le délai exact n'est intentionnellement pas garanti, le code d'application ne doit donc pas dépendre d'une nouvelle tentative à un moment précis. Une fois `maxRetries` atteint, un autre `RetryableLogicFunctionError` est enregistré comme échec final de l'application sans nouvelle exécution.
<Warning>
Les nouvelles tentatives réexécutent l'intégralité du gestionnaire et peuvent survenir après la réussite de certains effets secondaires. Rendez le gestionnaire idempotent avant de demander des nouvelles tentatives.
</Warning>
## Utilisation : paginer une longue synchronisation
La forme classique est une fonction qui met *elle-même* en file d’attente la prochaine exécution avec le curseur suivant. Chaque exécution traite une page de travail bien à l’intérieur de son propre délai d’expiration, et la chaîne s’arrête lorsqu’il ne reste plus rien.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Répartition par enregistrement
Quand le travail est naturellement par élément, mettez en file d’attente un job par élément dans un seul appel et laissez les workers les traiter en parallèle au lieu de boucler en ligne.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Bonnes pratiques pour les tâches de longue durée
Deux règles couvrent presque toutes les longues tâches : **utilisez la récursion au lieu de boucles** et **traitez un bloc borné par exécution**.
Une exécution qui essaie de tout faire est un mode d’échec — elle atteint le délai d’expiration, et avec une nouvelle tentative elle recommence tout depuis zéro. Au lieu de cela, dimensionnez un bloc de manière à ce qu’il se termine confortablement dans `timeoutSeconds`, conservez votre position et mettez en file d’attente l’exécution suivante.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
Ce qui rend cette approche robuste :
* **Dimensionnez le bloc à partir de l’élément le plus lent, pas de la moyenne.** `CHUNK_SIZE × worst-case item time` doit tenir dans `timeoutSeconds` avec une marge de sécurité, sinon la fin d’un bloc est perdue lorsque l’exécution est interrompue.
* **Rendez la condition de terminaison explicite.** Utilisez la récursion uniquement lorsqu’un bloc complet est revenu. Une chaîne qui s’arrête uniquement sur "no results" continuera indéfiniment si la source renvoie un jour une page courte en cours de route.
* **Conservez la progression avant de mettre en file d’attente l’exécution suivante,** afin qu’un maillon ayant échoué redémarre au dernier bloc terminé plutôt qu’au début.
* **Gardez chaque bloc idempotent.** Le retraitement d’un bloc après une nouvelle tentative ne doit pas provoquer une double écriture — indexez les écritures sur l’enregistrement ou l’identifiant externe que vous traitez.
* **Préférez une chaîne par blocs à un énorme déploiement parallèle** lorsque le travail touche un tiers soumis à des limitations de débit : une chaîne avec `delayMs` se régule elle-même, alors que des milliers de jobs mis en file d’attente d’un coup deviennent tous éligibles immédiatement.

View File

@@ -0,0 +1,269 @@
---
title: Connexions
description: Autorisez votre application à agir au nom d'un utilisateur auprès de services tiers via OAuth.
icon: plug
---
Les connexions sont des identifiants qu'un utilisateur détient pour un service externe (Linear, GitHub, Slack, ...). Votre application déclare **comment** ces identifiants sont obtenus — un **fournisseur de connexion** — et les consomme à l'exécution pour effectuer des appels authentifiés vers l'API tierce.
À ce jour, seul OAuth 2.0 est pris en charge. De futurs types d'identifiants (jetons d'accès personnels, clés d'API, authentification de base) s'intégreront à la même interface — les applications utilisant déjà `defineConnectionProvider({ type: 'oauth', ... `})\` n'auront pas besoin de migrer.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Déclarez comment les connexions de votre application sont obtenues">
Un fournisseur de connexion décrit le processus d'échange OAuth requis par votre application. L'utilisateur clique sur "Ajouter une connexion" dans les paramètres de votre application, complète l'écran de consentement du fournisseur, et une ligne `ConnectedAccount` est créée dans son espace de travail.
Une configuration fonctionnelle nécessite **deux fichiers** — le fournisseur de connexion, et une déclaration `serverVariables` correspondante sur `defineApplication` qui contient les identifiants client OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Points clés :
* `name` est l'identifiant unique utilisé dans `listConnections({ providerName })` (kebab-case, doit correspondre à `^[a-z][a-z0-9-]*$`).
* `displayName` s'affiche dans l'onglet des paramètres par application et dans la liste des outils d'IA.
* `clientIdVariable` / `clientSecretVariable` sont des **noms**, pas des valeurs — ils doivent correspondre aux clés déclarées dans `defineApplication.serverVariables`. Les `client_id` et `client_secret` réels sont saisis par l'administrateur du serveur via l'interface d'enregistrement de l'application, et ne sont jamais validés dans votre dépôt.
* Utilisez `serverVariables` (pas `applicationVariables`) — les identifiants OAuth sont au niveau du serveur et il n'y a qu'une application OAuth par serveur Twenty.
* Tant que les deux `serverVariables` ne sont pas renseignées, l'onglet des paramètres par application affiche une indication "nécessite un administrateur du serveur" et le bouton "Ajouter une connexion" est désactivé.
* `type: 'oauth'` est la seule valeur prise en charge à ce jour. Le discriminateur est compatible avec les évolutions : des types futurs (`'pat'`, `'api-key'`, ...) ajouteront de nouveaux blocs de sous-configuration à côté de `oauth`.
L'URL de rappel OAuth que votre fournisseur doit autoriser est :
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Exécuter une fonction logique lors de la connexion" description="Réagir au moment où une connexion est établie">
Certains fournisseurs vous transmettent, au moment de la connexion, des données que vous devez conserver avant que la connexion ne soit exploitable — l’exemple classique est Slack, où la réponse OAuth identifie le `team_id` de l’espace de travail sur lequel les événements entrants seront indexés. Définissez `onConnectLogicFunction` pour référencer une fonction logique dans la même application (par son `universalIdentifier`), et elle s’exécute juste après la création du `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
Le hook s’exécute **de manière asynchrone dans l’espace de travail en cours de connexion** (il est mis en file d’attente, non attendu), de sorte qu’un hook lent ou défaillant ne bloque ni n’interrompt jamais le callback OAuth — rendez-le idempotent et faites en sorte qu’il gère ses propres réessais. Le gestionnaire reçoit :
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
À partir de là, utilisez `getConnection(connectedAccountId)` pour lire le jeton d’accès actualisé et appeler l’API du fournisseur (par exemple Slack `auth.test`) ou persister un mappage dans le [stockage clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Exécuter une fonction logique lors de la déconnexion" description="Nettoyer lorsqu’une connexion est supprimée">
Tout ce qu’une application revendique au moment de la connexion doit être libéré lorsque la connexion est supprimée. Une intégration Slack qui revendique un `team_id` lors de la connexion, par exemple, doit libérer cette revendication afin qu’un autre espace de travail puisse connecter la même équipe Slack. Définissez `onDisconnectLogicFunction` pour référencer une fonction logique dans la même application, et elle s’exécute juste après la suppression du `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
Comme le hook on-connect, il s’exécute **de manière asynchrone dans l’espace de travail en cours de déconnexion** et ne bloque jamais la déconnexion. Le gestionnaire reçoit la même structure de payload :
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Le `ConnectedAccount` n’existe déjà plus au moment où le hook s’exécute, donc `getConnection(connectedAccountId)` ne se résout plus. Tout ce dont le nettoyage a besoin (un `team_id`, un identifiant d’abonnement externe) doit avoir été écrit dans le [key-value store](/l/fr/developers/extend/apps/logic/key-value-store) au moment de la connexion, indexé par `connectedAccountId`.
Le hook est déclenché lorsqu’une connexion est supprimée directement. La désinstallation de l’application supprime ses connexions via une cascade de base de données à la place, donc le hook ne s’exécute pas dans ce cas. Déclarez un `uninstallLogicFunction` sur `defineApplication` pour ce cas : il s’exécute avant que les métadonnées de l’application ne soient supprimées, ce qui lui permet encore d’appeler `listConnections` et de nettoyer tout ce qui reste.
</Accordion>
<Accordion title="listConnections / getConnection" description="Utilisez les connexions depuis une fonction logique">
Dans un gestionnaire de fonction logique, `listConnections({ providerName })` renvoie les lignes `ConnectedAccount` de cette application pour le fournisseur donné, avec des jetons d'accès actualisés.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Chaque connexion comporte :
| Champ | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID de ligne unique ; à passer à `getConnection(id)` pour en récupérer une seule |
| `visibility` | `'user'` (réservée à un membre de l'espace de travail) ou `'workspace'` (partagée avec tous les membres) |
| `scopes` | Autorisations OAuth accordées par le fournisseur en amont (distinctes de `visibility` — sans rapport) |
| `userWorkspaceId` | L'ID userWorkspace du propriétaire — utile pour sélectionner "la connexion de l'utilisateur de la requête" dans les déclencheurs de routes HTTP |
| `accessToken` | Jeton d'accès OAuth à jour (actualisé automatiquement s'il a expiré) |
| `nom` | Le nom d'affichage de la connexion (déduit automatiquement lors du callback OAuth, renommable par l'utilisateur) |
| `handle` | L'e-mail du compte amont connecté, actualisé à chaque reconnexion; résolu à partir du `id_token` OIDC a retourné lors de l'échange de jetons, tombant à l'email de 20 utilisateurs de connexion quand le fournisseur ne retourne pas un |
| `authFailedAt` | Défini lorsque le rafraîchissement le plus récent a échoué ; l'utilisateur doit se reconnecter |
Points clés :
* Passez `{ providerName }` pour filtrer par fournisseur ; omettez-le pour obtenir toutes les connexions que cette application possède sur l'ensemble des fournisseurs.
* Le serveur actualise de manière transparente le jeton d'accès avant de répondre. Votre gestionnaire voit toujours un jeton utilisable (ou `authFailedAt` défini).
* `getConnection(id)` est l'équivalent pour une seule ligne.
</Accordion>
<Accordion title="Visibilité par utilisateur vs partagée à l'échelle de l'espace de travail" description="Comment les utilisateurs choisissent entre des identifiants privés et partagés">
Lorsqu'un utilisateur clique sur "Ajouter une connexion", il lui est demandé de choisir une visibilité :
* **Uniquement pour moi** — les identifiants sont privés pour l'utilisateur qui se connecte. Toute fonction logique appelée en son nom (déclencheur de route HTTP avec `isAuthRequired: true`) y a accès ; les déclencheurs cron et les événements de base de données n'y ont pas accès.
* **Partagé au niveau de l'espace de travail** — tout membre de l'espace de travail peut utiliser les identifiants. Les déclencheurs cron / base de données y ont également accès, puisqu'ils n'ont pas d'utilisateur de requête.
Utilisez le bon pour chaque gestionnaire :
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Plusieurs connexions par (utilisateur, fournisseur) sont autorisées, ainsi le même utilisateur peut avoir "Linear personnel" et "Linear professionnel" côte à côte.
</Accordion>
<Accordion title="Configuration unique du fournisseur" description="Enregistrez votre application OAuth auprès du service tiers">
Pour chaque fournisseur de connexion, l'administrateur du serveur doit d'abord enregistrer une application OAuth auprès du service tiers.
1. Accédez aux paramètres développeur du fournisseur (par ex. https://linear.app/settings/api/applications/new).
2. Définissez l'**URI de redirection** sur `\<SERVER_URL>/auth/apps/callback`.
3. Copiez l'**ID client** et le **Secret client** générés.
4. Ouvrez l'application installée dans Twenty en tant qu'administrateur du serveur → définissez les valeurs sur les `serverVariables` correspondantes.
5. Les membres de l'espace de travail peuvent ensuite ajouter des connexions depuis la section **Connexions** par application.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Stockage clé-valeur
description: Conservez des résultats intermédiaires, mettez les données en cache et partagez l’état entre les exécutions de fonctions logiques avec le magasin clé-valeur intégré à l’application.
icon: database
---
Les fonctions logiques s'exécutent dans des processus Node.js isolés et de courte durée — une fois une exécution terminée, rien de ce qui était conservé en mémoire ne survit. Lorsque vous avez besoin de **vous souvenir de quelque chose entre les exécutions** (mettre en cache une réponse d’API coûteuse, stocker un curseur pour des synchronisations incrémentales, appliquer un anti-rebond, ou transférer l’état d’une fonction à une autre), conservez-le dans le magasin clé-valeur intégré.
Chaque application dispose de son propre espace de noms isolé : les entrées sont associées à l’application authentifiée, de sorte que vos clés ne peuvent jamais entrer en collision avec celles d’une autre application, ni être lues par celle-ci.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Obtenir, définir, supprimer
Importez `kv` depuis `twenty-sdk/logic-function`. Les valeurs peuvent être n’importe quelle charge utile sérialisable en JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Périmètres
Chaque entrée possède une portée, passée comme option à chaque appel. La valeur par défaut est `WORKSPACE`.
* **`WORKSPACE`** (par défaut) — l’entrée est réservée à l’installation actuelle de votre application dans l’espace de travail. Chaque espace de travail qui installe l’application obtient son propre ensemble indépendant de clés. C’est ce qu’il vous faut pour les caches, les curseurs et l’état par espace de travail.
* **`SERVER`** — l’entrée est partagée entre **toutes les installations** de votre application sur le serveur. Les entrées serveur se comportent comme des **revendications** : la valeur stockée est toujours le workspaceId qui a revendiqué la clé (omettre `value` lors de `set` pour revendiquer la clé pour l’espace de travail actuel), et seul cet espace de travail peut la remplacer ou la supprimer. N’importe quelle installation peut lire l’entrée.
Les revendications serveur existent pour le routage inter-espaces de travail. Un [résolveur de route serveur](/l/fr/developers/extend/apps/logic/logic-functions#server-route-trigger) s’exécute dans l’espace de travail propriétaire de l’enregistrement de l’application, mais un webhook entrant ne transporte généralement qu’un identifiant de compte externe — pas un workspaceId Twenty. Faites en sorte que chaque espace de travail revendique son identifiant externe au moment de la connexion, puis résolvez-le dans la route :
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Comme une clé serveur ne peut être revendiquée que pour l’espace de travail de l’appelant et jamais écrasée par un autre, un espace de travail ne peut pas détourner un mappage appartenant à quelqu’un d’autre. `kv.set` lève une exception lorsque la clé est déjà revendiquée par un autre espace de travail.
## Utilisez-le : mettez en cache un appel coûteux
Un cas d'utilisation typique consiste à mettre en cache une réponse tierce lente ou soumise à des limites de débit afin que les exécutions répétées la réutilisent au lieu d'en payer le coût à chaque fois.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Modèles et conseils
* **Espaces de noms.** Préfixez les clés pour séparer les différentes préoccupations — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Expiration (TTL).** Le store n'a pas d'expiration intégrée. Stockez un horodatage dans la valeur (comme dans l’exemple de cache) et vérifiez-le à la lecture, ou effacez les clés obsolètes à partir d’une [fonction déclenchée par cron](/l/fr/developers/extend/apps/logic/logic-functions).
* **Que stocker.** Toute valeur sérialisable en JSON — nombres, chaînes de caractères, tableaux, objets. Gardez les entrées petites ; ceci sert à la coordination et à la mise en cache, pas aux blobs volumineux ni aux fichiers. Pour les fichiers, utilisez un champ `FILES` et [`uploadFile`](/l/fr/developers/extend/apps/logic/logic-functions#uploading-files).
* **Visibilité.** Les entrées résident dans la base de données de l’instance, et non comme enregistrements d’espace de travail — elles n’apparaissent jamais dans l’interface utilisateur de l’espace de travail, ne font pas partie du modèle de données de votre application et ne nécessitent aucun droit ni aucune permission d’objet.
## Alternative : un objet de stockage interrogeable
Le magasin intégré est délibérément opaque : les entrées ne sont pas des enregistrements, vous ne pouvez donc pas les parcourir dans l’interface utilisateur, les relier à d’autres objets, ni les filtrer avec des requêtes sur les enregistrements. Lorsque vous avez besoin de tout cela — par exemple un journal de synchronisation visible, ou un état par enregistrement — définissez plutôt un petit **objet technique** avec un champ `key` unique et un champ `RAW_JSON` `value`, puis interrogez-le via le [client d’API typé](/l/fr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Consultez [Objets](/l/fr/developers/extend/apps/data/objects) pour la référence `defineObject` et [Données → Index uniques](/l/fr/developers/extend/apps/data/overview#unique-indexes) pour faire appliquer l’unicité des clés.
* **Délimiter la portée à un enregistrement.** Ajoutez une [relation](/l/fr/developers/extend/apps/data/relations) de l’objet de stockage vers l’objet cible plutôt que de coder l’identifiant dans la clé.
* **Visibilité et autorisations.** Les lignes résident dans la base de données de l'espace de travail comme n'importe quel autre enregistrement, elles sont donc interrogeables via l'API et respectent le [rôle](/l/fr/developers/extend/apps/config/roles) de votre application. Pour garder le store hors de l'interface principale, ne l'ajoutez pas à votre [menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items).
<Note>
Contrairement au magasin intégré, un objet personnalisé est toujours limité à un seul espace de travail — il ne peut pas partager des entrées entre installations comme le font les clés `SERVER`.
</Note>

View File

@@ -0,0 +1,909 @@
---
title: Fonctions logiques
description: Définissez des fonctions TypeScript côté serveur avec des déclencheurs HTTP, cron et d’événements de base de données.
icon: bolt
---
Les fonctions logiques sont des fonctions TypeScript côté serveur qui s'exécutent sur la plateforme Twenty. Elles peuvent être déclenchées par des requêtes HTTP, des programmations cron ou des événements de base de données — et peuvent également être exposées comme des outils pour des agents d'IA.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Définir des fonctions logiques et leurs déclencheurs">
Chaque fichier de fonction utilise `defineLogicFunction()` pour exporter une configuration avec un gestionnaire et des déclencheurs facultatifs.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Types de déclencheurs disponibles :
* **httpRoute** : Expose votre fonction sur un chemin et une méthode HTTP. Dans le code de l'application, préfixez le chemin de la route avec `/s/` lorsque vous utilisez `RestApiClient` ; l'URL déployée utilise la base injectée `TWENTY_FUNCTIONS_URL` (ou `\<server-url>/s` lorsqu'elle n'est pas définie).
<Note>
Pour appeler une fonction logique déclenchée par une route depuis un composant frontal (sans interface), consultez [Appeler une fonction logique](/l/fr/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron** : Exécute votre fonction selon une planification à l’aide d’une expression CRON.
* **databaseEvent**: S'exécute lors des événements du cycle de vie des objets de l'espace de travail. Lorsque l'opération de l'événement est `updated`, des champs spécifiques à surveiller peuvent être spécifiés dans le tableau `updatedFields`. S'il est laissé indéfini ou vide, toute mise à jour déclenchera la fonction.
> p. ex. `person.updated`, `*.created`, `company.*`
* **serverRoute** : expose une seule route HTTP à portée d’enregistrement. Une fonction de **résolution** (déclarée avec `serverRouteTriggerSettings`) s’exécute dans l’espace de travail propriétaire et renvoie soit une `Response` synchrone, soit l’espace de travail cible ET la fonction logique cible à mettre en file d’attente ; dans le cas de la mise en file d’attente, la plateforme accuse réception avec `202` et exécute cette **cible** dans la file d’attente du worker. Voir [déclencheur de route serveur](#server-route-trigger).
<Note>
Vous pouvez également exécuter manuellement une fonction à l'aide de la CLI :
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Vous pouvez consulter les journaux avec :
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Charge utile du déclencheur de route
Lorsqu'un déclencheur de route invoque votre fonction logique, elle reçoit un objet `RoutePayload` qui suit le
[format AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importez le type `RoutePayload` depuis `twenty-sdk/logic-function` :
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Le type `RoutePayload` a la structure suivante :
| Nom de la propriété | Type | Description | Exemple |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | En-têtes HTTP (uniquement ceux répertoriés dans `forwardedRequestHeaders`) | voir la section ci-dessous |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Paramètres de la chaîne de requête (plusieurs valeurs séparées par des virgules) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Paramètres de chemin extraits du modèle de route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corps de la requête analysé (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corps de la requête UTF-8 d'origine, avant l'analyse JSON. Utile pour vérifier les signatures de webhook de type HMAC (par exemple `X-Hub-Signature-256` de GitHub, Stripe). `undefined` lorsque l'environnement d'exécution ne l'a pas conservé. | |
| `isBase64Encoded` | `boolean` | Indique si le corps est encodé en base64 | |
| `requestContext.http.method` | `string` | Méthode HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Chemin de la requête brut | |
#### forwardedRequestHeaders
Par défaut, les en-têtes HTTP des requêtes entrantes ne sont pas transmis à votre fonction logique pour des raisons de sécurité.
Pour accéder à des en-têtes spécifiques, listez-les dans le tableau `forwardedRequestHeaders` :
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
Dans votre gestionnaire, accédez aux en-têtes transférés comme ceci :
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Les noms d'en-têtes sont normalisés en minuscules. Accédez-y en utilisant des clés en minuscules (p. ex., `event.headers['content-type']`).
</Note>
#### Réponse HTTP personnalisée
Par défaut, le retour d’une valeur simple depuis votre gestionnaire l’envoie en réponse `200` (JSON pour les objets, `text/plain` pour les chaînes). Pour contrôler le code d’état et les en-têtes de la réponse, retournez un objet `Response` depuis `twenty-sdk/logic-function` :
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
Pour des raisons de sécurité, les en-têtes de réponse sont restreints à une liste d’autorisation. Tout en-tête qui ne figure pas dans la liste (par exemple `Set-Cookie`, les en-têtes CORS tels que `Access-Control-Allow-Origin`, ou les en-têtes personnalisés `X-*`) est silencieusement supprimé avant l’envoi de la réponse. Les en-têtes de réponse autorisés sont :
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
Le code d’état doit être un code d’état HTTP valide (compris entre 100 et 599). Les noms des en-têtes de réponse sont comparés sans tenir compte de la casse.
</Note>
#### Réponses d’erreur de la plateforme
Au-delà des réponses propres à votre gestionnaire, la plateforme répond directement aux appels de route dans certaines situations : `404` lorsque la route ou la fonction n’existe pas, `403` lorsque l’application est arrêtée, `429` lorsque la limite de débit d’exécution est atteinte, et `422` lorsque les `dependencies` de production de l’application sont trop volumineuses pour être installées — voir [limites de taille des dépendances](/l/fr/developers/extend/apps/getting-started/project-structure#dependencies).
#### Déclencheur de route serveur
`httpRouteTriggerSettings` expose une fonction sous `/s/` et résout l’espace de travail à partir de l’hôte de la requête — ce qui fonctionne lorsque chaque espace de travail a son propre domaine. Les fournisseurs tiers, en revanche, envoient les événements de chaque locataire vers **une** URL. Dans ce cas, utilisez `serverRouteTriggerSettings`.
Le déclencheur comporte deux parties :
1. Une fonction de logique de **résolution** — déclarée avec `serverRouteTriggerSettings` — s’exécute dans votre **espace de travail propriétaire** (l’espace de travail qui possède l’enregistrement de l’application). Elle inspecte la requête entrante et renvoie soit :
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la plateforme met cette cible en file d’attente dans l’espace de travail résolu et accuse réception avec `202 { queued: true }`, ou
* une `Response` de `twenty-sdk/logic-function` — la plateforme renvoie cette réponse HTTP **de manière synchrone** et ne met **pas** de cible en file d’attente (utilisez ceci pour les échanges de vérification, comme la `url_verification` de Slack).
Le résolveur est le point d’autorisation unique — l’URL transporte uniquement l’identifiant du résolveur. **C’est l’endroit privilégié pour vérifier les signatures des requêtes** : le résolveur s’exécute avant tout effet de bord, a accès au `rawBody` original et aux en-têtes transmis, et peut rejeter la requête sans jamais toucher la cible.
2. Une fonction de logique **cible** — une fonction de logique classique par espace de travail — s’exécute ensuite dans l’espace de travail résolu avec la charge utile renvoyée par le résolveur (ou la charge utile originale de la requête si le résolveur ne l’a pas transformée). Sa valeur de retour **n’est pas** observée par l’appelant HTTP lorsque le résolveur a choisi le chemin de mise en file d’attente.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
Le point de terminaison est accessible à l’adresse :
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
L’identifiant est le `universalIdentifier` du résolveur issu de votre manifeste. Enregistrez cette URL auprès du fournisseur.
**Répondre à une vérification GET.** Certains fournisseurs vérifient un point de terminaison avant d'y effectuer une livraison, en envoyant une requête `GET` contenant une vérification à la même URL vers laquelle ils effectueront ensuite des événements `POST` — l'API WhatsApp Cloud de Meta en est un exemple. Une route de serveur répond uniquement à `POST`, sauf indication contraire. Déclarez donc les deux méthodes :
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
La vérification arrive dans `event.queryStringParameters`, et le renvoi d'une `Response` la renvoie au fournisseur dans la même requête. Un corps de chaîne est envoyé en tant que `text/plain`, ce que ces fournisseurs attendent :
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` remplace la valeur par défaut au lieu de s'y ajouter, donc `['GET']` seul fait que la route rejette `POST`. Seuls `GET` et `POST` sont pris en charge. Ne le définissez pas, sauf si le fournisseur a besoin du deuxième verbe : une route qui déclare `GET` verra son résolveur exécuté par tout appelant non authentifié, y compris les robots d'exploration et les outils de déploiement des aperçus de liens qui envoient des requêtes `GET` sans y être invités. Toute requête pour laquelle la plateforme ne dispose pas de méthode reçoit une réponse `405` sans que le résolveur ne soit jamais exécuté.
<Note>
**L’application doit être revendiquée et installée sur son espace de travail propriétaire.** Comme le résolveur s’exécute dans l’**espace de travail propriétaire** (l’espace de travail qui détient l’enregistrement de l’application), un déclencheur de route serveur ne fonctionne que lorsque l’application a été *revendiquée* — c’est‑à‑dire qu’elle possède un espace de travail propriétaire — **et** que cette application est **installée sur l’espace de travail propriétaire**. Tant que ces deux conditions ne sont pas remplies, le résolveur n’a nulle part où s’exécuter, donc la route ne peut pas être envoyée. Une application qui expose une fonction logique `serverRouteTriggerSettings` ne peut donc pas être répertoriée sur la place de marché tant qu’elle n’a pas été revendiquée et installée sur son espace de travail propriétaire.
</Note>
**Contrat du résolveur.** Le type `LogicFunctionConfig` du SDK impose cela à la compilation : dès que vous définissez `serverRouteTriggerSettings`, votre gestionnaire est contraint de retourner soit un `Response`, soit `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou une `Promise` de l’un ou l’autre). Sur le chemin d’acheminement, le `workspaceId` doit être celui d’un espace de travail où la fonction cible est installée, sinon la requête est rejetée avec un `404`. Un résultat qui ne correspond à aucune de ces formes — y compris un résultat dont les identifiants ne sont pas des UUID — est rejeté avec un `502`.
| Champ | Type | Notes |
| ---------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | UUID de l’espace de travail dans lequel la cible sera exécutée. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` de la fonction de logique à invoquer dans cet espace de travail. |
| `payload` | `object` (facultatif) | S’il est défini, il remplace le corps de la requête envoyé à la cible. |
<Warning>
**La vérification de la signature est de votre responsabilité — effectuez-la dans le résolveur.** La plateforme ne vérifie pas les signatures des requêtes. Le résolveur est l’endroit recommandé pour le faire : il s’exécute en premier, avec accès à `event.rawBody` et aux en-têtes que vous avez listés dans `forwardedRequestHeaders`, et une erreur levée (ou tout `workspaceId` ne correspondant pas) interrompt la distribution avant que la cible ne soit invoquée. Si, à la place, vous repoussez la vérification vers la cible, celle-ci doit faire attention à ne pas perdre `rawBody` et les en-têtes — c’est-à-dire que le résolveur ne doit pas retourner de `payload`. Vérifiez toujours **avant** tout effet de bord et utilisez une comparaison en temps constant.
</Warning>
Pour les signatures de requêtes, la plupart des fournisseurs signent avec HMAC-SHA256 ; les éléments qui diffèrent sont le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée. Quelques exemples :
| Fournisseur | En-têtes à transférer | Chaîne signée | Empreinte |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (le secret est en base64 après suppression de `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hexadécimal |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hexadécimal (préfixé par `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hexadécimal (préfixé par `v0=`) |
L’exemple de résolveur ci-dessus montre déjà le flux GitHub HMAC-SHA256 — adaptez le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée en fonction du fournisseur avec lequel vous vous intégrez.
<Note>
Lorsque le résolveur retourne un objet d’acheminement, la route répond `202 { queued: true }` et la cible s’exécute dans la file d’attente du worker — l’appelant n’observe jamais la latence, le résultat ou les échecs de la cible (ceux-ci sont enregistrés dans les journaux d’exécution). Cela évite que les nouvelles tentatives d’envoi de l’émetteur n’amplifient les ralentissements de traitement, ce qui est souhaitable pour l’ingestion de webhook.
Lorsque l’appelant doit lire le corps de la réponse sur la même requête (handshakes de challenge, accusés de réception interactifs), retournez plutôt un `Response` depuis le **résolveur**. La plateforme le renvoie de manière synchrone et ignore la file d’attente ; ses en-têtes passent par la même liste d’autorisation que les réponses des routes HTTP. Gardez le résolveur rapide — certains fournisseurs (par ex. Slack) ont un délai d’attente de seulement quelques secondes. Comme le résolveur est accessible en tant que point de terminaison public, protégez-le avec une limitation de débit à votre périphérie.
</Note>
#### Charge utile du déclencheur d'événement de base de données
Lorsqu’un déclencheur d’événement de base de données appelle votre fonction logique, celle-ci reçoit un `DatabaseEventPayload` par enregistrement modifié. La charge utile combine les métadonnées concernant l'espace de travail et l'objet source avec l'événement au niveau de l'enregistrement.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
La charge utile inclut :
| Propriété | Description |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `name` | Nom de l’événement, comme `person.updated`. |
| `workspaceId` | Espace de travail où l’événement s’est produit. |
| `objectMetadata` | Métadonnées pour l’objet qui a été modifié. |
| `recordId` | ID de l’enregistrement modifié. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Champs de l’acteur lorsque l’événement a été provoqué par un utilisateur de l’espace de travail. |
| `properties` | Données d’enregistrement pour l’événement, avec `before`, `after`, `diff` et `updatedFields` selon l’opération. |
| Événement | Données d’enregistrement |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Pour les suppressions logiques (soft deletes), `.deleted` suit la structure de type mise à jour, car le champ `deletedAt` de l’enregistrement change.
Pour les suppressions permanentes, utilisez `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtre les événements de mise à jour qui déclenchent la fonction.
`event.properties.updatedFields` indique quels champs ont réellement changé pour l’événement actuel.
</Note>
Exemple d’événement de création :
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Exemple d’événement de mise à jour :
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
Déclencher uniquement lors des mises à jour de l’adresse e-mail :
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Exemple d’événement de destruction :
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Contexte d'exécution
Chaque gestionnaire reçoit un deuxième argument décrivant l'exécution elle-même, quel que soit ce qui l'a déclenchée. Alors que la forme du premier argument varie selon le déclencheur, celle-ci ne varie pas :
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Champ | Type | Description |
| ------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | L'espace de travail auquel appartient l'exécution. |
| `userWorkspaceId` | `string \| null` | La personne qui a déclenché l'exécution, en tant que membre de cet espace de travail. |
| `workspaceMemberId` | `string \| null` | La même personne sous la forme d'un enregistrement de membre de l'espace de travail, auquel vous liez les enregistrements. |
| `retryCount` | `number` | `0` lors de la première tentative. Voir [Tâches en arrière-plan](/l/fr/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limite supérieure des nouvelles tentatives demandées par l'application. |
`userWorkspaceId` et `workspaceMemberId` sont `null` lorsque personne n'a déclenché l'exécution : les planifications cron, les hooks d'installation et les webhooks non authentifiés n'ont aucune personne derrière eux. Ils sont également `null` lorsque la personne ne possède aucun enregistrement de membre de l'espace de travail, ou que le sien a été supprimé.
<Note>
**Le contexte vous indique qui a déclenché l'exécution.** Ce que l'exécution peut *faire* est une déclaration distincte, ci-dessous.
</Note>
#### Quels accès un appel utilise
Chaque client — `CoreApiClient`, `MetadataApiClient` et `RestApiClient` — agit en tant que personne ayant déclenché l'exécution : son rôle est combiné à celui de votre application, de sorte que l'appel ne peut jamais faire plus que l'un ou l'autre. C'est le comportement par défaut, et cela signifie qu'une personne ne peut jamais utiliser votre application pour dépasser ses propres autorisations.
Lorsque personne n'a déclenché l'exécution, il n'y a aucune personne dont il faut adopter l'identité, le même client utilise donc les propres accès de votre application : les planifications cron, les hooks d'installation et les webhooks non authentifiés ne nécessitent aucun traitement particulier.
Certains appels nécessitent légitimement les propres accès de l'application même lorsqu'une personne *est* à l'origine de l'exécution — pour lire les enregistrements de configuration de votre application, ou effectuer un travail que cette personne ne pourrait pas faire elle-même. Créez un deuxième client pour ceux-ci :
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Créez les deux une seule fois, à la portée du module, et chaque site d'appel indique alors les accès qu'il utilise par le client qu'il appelle. `RestApiClient` accepte la même option :
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**Une exécution que personne n'a déclenchée agit en tant que votre application.** Les planifications cron, les hooks d'installation et les webhooks non authentifiés n'ont aucune personne derrière eux, le client par défaut utilise donc les propres accès de votre application et continue de fonctionner. `runAs: 'application'` n'est nécessaire que lorsque vous souhaitez utiliser ces accès dans une exécution qu'une personne *a* déclenchée.
Vérifiez `context.workspaceMemberId` lorsqu'une fonction se comporte différemment selon qu'une personne est derrière elle, par exemple pour attribuer un enregistrement.
</Note>
Les assistants du SDK qui accèdent aux propres ressources de votre application utilisent toujours ses accès et ignorent `runAs` : le [magasin clé-valeur](/l/fr/developers/extend/apps/logic/key-value-store), les [connexions](/l/fr/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` et la facturation de crédits.
#### Exposer une fonction en tant qu'outil d'IA ou en tant qu'action de workflow
Les fonctions logiques peuvent être exposées sur deux surfaces, chacune avec son propre déclencheur :
* **`toolTriggerSettings`** — rend la fonction découvrable par les fonctionnalités d'IA de Twenty (chat, MCP, appel de fonctions). Utilise le schéma JSON standard, le format que les LLM comprennent nativement.
* **`workflowActionTriggerSettings`** — fait apparaître la fonction comme une étape dans le concepteur visuel de workflows. Utilise le `InputSchema` riche de Twenty afin que le concepteur puisse afficher des éditeurs de champs appropriés, des sélecteurs de variables et des libellés.
Une fonction peut opter pour l'un, l'autre ou les deux. Elles côtoient `cronTriggerSettings`, `databaseEventTriggerSettings` et `httpRouteTriggerSettings` — même modèle, même structure.
<Note>
**Lien avec l’action Code du workflow.** L’action **Code** intégrée dans le générateur de workflows est elle-même une fonction logique — Twenty en crée une pour chaque étape Code et affiche son éditeur en ligne. `workflowActionTriggerSettings` est la manière de transformer ce code ponctuel en ligne en une action **réutilisable** : définissez la fonction une fois dans votre application et elle devient sélectionnable dans n’importe quel workflow, au lieu d’être copiée-collée dans chaque étape Code. Voir l’[action Code](/l/fr/user-guide/workflows/capabilities/workflow-actions#code) dans le guide utilisateur pour la vue côté utilisateur final.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Points clés :
* Une fonction peut mélanger les surfaces — déclarez à la fois `toolTriggerSettings` et `workflowActionTriggerSettings` pour l'exposer à la fois dans le chat ET dans le concepteur de workflows.
* `toolTriggerSettings.inputSchema` et `workflowActionTriggerSettings.inputSchema` sont tous deux facultatifs. Lorsqu'ils sont omis, le générateur de manifeste les déduit à partir du code source du gestionnaire (schéma JSON pour l'outil d'IA, `InputSchema` de Twenty pour l'action de workflow). Fournissez-en un explicitement lorsque vous souhaitez un typage plus riche — par exemple, avec des champs compatibles avec `FieldMetadataType` comme `CURRENCY` ou `RELATION` pour le concepteur de workflows, ou avec des champs `description` que l'agent d'IA peut lire :
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
Pour déclarer vos paramètres **une seule fois** et les utiliser sur les deux surfaces, définissez un seul schéma JSON (`InputJsonSchema`) et convertissez-le pour l’action de flux de travail avec `jsonSchemaToInputSchema` depuis `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` prend directement le schéma JSON, tandis que `workflowActionTriggerSettings.inputSchema` attend le `InputSchema` de Twenty :
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Un exemple complet d’action de workflow
`workflowActionTriggerSettings` accepte quatre champs :
| Champ | Objectif |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Nom affiché pour l’action dans le sélecteur d’étapes du générateur de workflows. Utilise par défaut le `name` de la fonction. |
| `icon` | Icône affichée à côté de l’action (un nom `tabler-icons`, par exemple `IconBuilding`). |
| `inputSchema` | Le riche `InputSchema` de Twenty — ce que le générateur affiche sous forme de champs configurables (avec des sélecteurs de variables). Optionnel ; déduit du gestionnaire lorsqu’il est omis. |
| `outputSchema` | Déclare la structure renvoyée par le gestionnaire, afin que **les étapes suivantes puissent établir une correspondance avec ses champs de sortie**. Optionnel ; sans cela, la sortie est exposée comme une valeur opaque unique. |
Assembler le tout — une fonction exposée comme action de workflow, avec une sortie déclarée pour que les étapes ultérieures puissent référencer `taskId` :
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Une fois l’application installée, **Enrich Company** apparaît dans le sélecteur d’actions du générateur de workflows. Le générateur affiche `companyName` et `domain` sous forme de champs de saisie (chacun pouvant récupérer des valeurs à partir des étapes précédentes), et les étapes en aval peuvent référencer les sorties `taskId` et `enriched` de l’étape.
<Note>
**Rédigez une bonne `description`.** Les agents IA s'appuient sur le champ `description` de la fonction pour décider quand utiliser l'outil. Soyez précis sur ce que fait l'outil et quand il doit être appelé.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Aides à l’exécution.** `twenty-sdk/utils` réexporte de petites aides à l’exécution afin que les gestionnaires n’importent jamais directement depuis `twenty-shared`. Par exemple, `isDefined(value)` renvoie `false` à la fois pour `null` et `undefined` — utilisez-le pour restreindre en toute sécurité les entrées de gestionnaire optionnelles, qui peuvent arriver sous forme de `null` à l’exécution même lorsqu’elles sont typées `T | undefined` :
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Hooks d'installation** — les gestionnaires de pré-installation, de post-installation et de désinstallation — partagent ce runtime mais sont déclarés avec leurs propres fonctions define et ne prennent pas de paramètres de déclenchement. Voir [hooks d'installation](/l/fr/developers/extend/apps/config/install-hooks) pour `definePreInstallLogicFunction`, `definePostInstallLogicFunction` et `defineUninstallLogicFunction`.
</Note>
## Créer une activité de chronologie.
Utilisez `createTimelineActivity()` pour publier un événement de domaine explicite depuis une fonction logique. Définissez d'abord l'événement comme un [type d'activité de chronologie](/l/fr/developers/extend/apps/data/timeline-activity-types), puis référencez le type et les objets à l'aide de leurs identifiants universels stables :
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty résout les identifiants universels en identifiants de métadonnées spécifiques à l'installation, valide que le type d'activité de chronologie appartient à l'application appelante et capture un instantané de ses métadonnées de présentation dans la nouvelle activité.
Les entrées requises sont `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` et `targetRecordId`. Vous pouvez également fournir `happensAt`, `properties` et `workspaceMemberId`. `happensAt` contrôle la position et l'heure affichée de l'événement dans la chronologie ; par défaut, il correspond à l'heure de création.
Pour associer un autre enregistrement à l'événement, fournissez ensemble `linkedRecordId` et `linkedObjectMetadataUniversalIdentifier`. Vous pouvez également fournir `linkedRecordCachedName` comme solution de repli d'affichage historique :
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Le rôle de la fonction logique doit disposer de l'autorisation d'écriture sur l'objet standard `timelineActivity`. Gardez les types d'événements explicites non liés à une `action` ; un type lié à une action reçoit déjà des événements d'audit automatiques et produirait sinon des lignes en double.
## Clients d'API typés (twenty-client-sdk)
Le package `twenty-client-sdk` fournit deux clients GraphQL typés pour interagir avec l'API Twenty depuis vos fonctions logiques et vos composants frontaux.
| Client | Importer | Point de terminaison | Généré ? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------------ | --------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — données de l'espace de travail (enregistrements, objets) | Oui, au moment du dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuration de l'espace de travail, téléversements de fichiers | Non, livré prêt à l'emploi |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Interroger et modifier les données de l'espace de travail (enregistrements, objets)">
`CoreApiClient` est le client principal pour interroger et modifier les données de l'espace de travail. Il est **généré à partir du schéma de votre espace de travail** lors de l'exécution de `yarn twenty dev` ou `yarn twenty dev:build`, il est donc entièrement typé pour correspondre à vos objets et champs.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Le client utilise une syntaxe d'ensemble de sélection : passez `true` pour inclure un champ, utilisez `__args` pour les arguments et imbriquez des objets pour les relations. Vous bénéficiez d'une autocomplétion complète et d'une vérification de types basée sur le schéma de votre espace de travail.
<Note>
**CoreApiClient est généré au moment du dev/build.** Si vous l'utilisez sans exécuter d'abord `yarn twenty dev` ou `yarn twenty dev:build`, une erreur est levée. La génération se fait automatiquement — la CLI inspecte le schéma GraphQL de votre espace de travail et génère un client typé à l'aide de `@genql/cli`.
</Note>
#### Utiliser CoreSchema pour les annotations de type
`CoreSchema` fournit des types TypeScript correspondant à vos objets d'espace de travail — utile pour typer l'état des composants ou les paramètres de fonction :
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configuration de l'espace de travail, applications et téléversements de fichiers">
`MetadataApiClient` est livré prêt à l'emploi avec le SDK (aucune génération requise). Il interroge le point de terminaison `/metadata` pour la configuration de l'espace de travail, les applications et les téléversements de fichiers. Il accepte la même option `runAs` que `CoreApiClient` — voir [Quels accès un appel utilise](#whose-access-a-call-uses).
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Téléverser des fichiers
Le `MetadataApiClient` inclut une méthode `uploadFile` pour joindre des fichiers aux champs de type fichier :
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Paramètre | Type | Description |
| ---------------------------------- | -------- | ---------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Le contenu brut du fichier |
| `filename` | `string` | Le nom du fichier (utilisé pour le stockage et l'affichage) |
| `contentType` | `string` | Type MIME (par défaut `application/octet-stream` s'il est omis) |
| `fieldMetadataUniversalIdentifier` | `string` | Le `universalIdentifier` du champ de type fichier de votre objet |
Points clés :
* Utilise le `universalIdentifier` du champ (et non son ID propre à l'espace de travail), de sorte que votre code de téléversement fonctionne dans tout espace de travail où votre application est installée.
* L'`url` renvoyée est une URL signée que vous pouvez utiliser pour accéder au fichier téléversé.
</Accordion>
</AccordionGroup>
<Note>
Lorsque votre code s'exécute sur Twenty (fonctions logiques ou composants frontaux), la plateforme injecte des identifiants sous forme de variables d'environnement :
* `TWENTY_API_URL` — URL de base de l'API Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Clé de courte durée pour l'accès par défaut : le rôle d'une personne combiné à celui de votre application lorsqu'une personne est à l'origine de l'exécution, le propre rôle de votre application lorsque personne ne l'est. La personne est celle qui a déclenché l'exécution dans une fonction logique, ou celle qui consulte la page dans un composant frontal.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Clé de courte durée limitée au seul rôle de votre application. Uniquement pour les fonctions logiques, toujours injectée dans celles-ci, et utilisée par `runAs: 'application'`.
Vous n'avez **pas** besoin de les transmettre aux clients — ils lisent automatiquement depuis `process.env`, et [Quels accès un appel utilise](#whose-access-a-call-uses) explique comment choisir entre eux. Les propres autorisations de votre application sont déterminées par le rôle déclaré avec `defineApplicationRole()` (ou référencé via `defaultRoleUniversalIdentifier` dans `application-config.ts`) ; une exécution agissant en tant que personne ne peut jamais dépasser ce rôle, ni le sien.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Vue d'ensemble
description: TypeScript côté serveur qui s’exécute dans Twenty — déclenché par des routes HTTP, des planifications CRON, des événements de base de données, des outils d’IA ou des actions de flux de travail.
icon: bolt
---
La **couche logique** d’une application Twenty est le code qui *s’exécute* — des gestionnaires TypeScript côté serveur qui réagissent aux requêtes HTTP, aux planifications CRON et aux modifications d’enregistrements ; des compétences et des agents d’IA qui résident dans l’espace de travail ; et des connexions OAuth qui permettent à vos fonctions d’agir au nom d’un utilisateur dans des services tiers.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Fonctions logiques" icon="bolt" href="/l/fr/developers/extend/apps/logic/logic-functions">
Le bloc de construction principal — types de déclencheurs, charges utiles et client API typé.
</Card>
<Card title="Compétences et agents" icon="robot" href="/l/fr/developers/extend/apps/logic/skills-and-agents">
Instructions d’agent IA réutilisables et assistants avec invites système personnalisées.
</Card>
<Card title="Connexions" icon="plug" href="/l/fr/developers/extend/apps/logic/connections">
Identifiants OAuth détenus par votre application pour des services tiers — Linear, GitHub, Slack, et plus encore.
</Card>
<Card title="Stockage clé-valeur" icon="database" href="/l/fr/developers/extend/apps/logic/key-value-store">
Conservez l’état entre les exécutions de fonctions logiques — caches, curseurs et revendications inter-espaces de travail.
</Card>
<Card title="Tâches en arrière-plan" icon="layer-group" href="/l/fr/developers/extend/apps/logic/background-jobs">
Mettez en file d’attente l’exécution d’une fonction logique sur les workers afin de contourner la limite de temps par exécution.
</Card>
</CardGroup>
## Aperçu des types de déclencheurs
Une fonction logique choisit un ou plusieurs déclencheurs — chaque entrée ci-dessous est un champ distinct sur `defineLogicFunction()`:
| Déclencheur | Moment d’exécution | Paramètre |
| -------------------------------- | ---------------------------------------------------------------------------- | ------------------------------- |
| **Route HTTP** | Une requête atteint votre point de terminaison `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | Une expression CRON correspond | `cronTriggerSettings` |
| **Événement de base de données** | Un enregistrement de l’espace de travail est créé, mis à jour ou supprimé | `databaseEventTriggerSettings` |
| **Outil IA** | Une fonctionnalité IA de Twenty décide d’appeler votre fonction | `toolTriggerSettings` |
| **Action de flux de travail** | Une étape de flux de travail invoque votre fonction | `workflowActionTriggerSettings` |
Les fonctions s’exécutent dans un environnement isolé dans des processus Node.js sandboxés et accèdent à l’espace de travail via un client API typé, limité au rôle déclaré sur [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
<Note>
Les **hooks au moment de l’installation** — du code qui s’exécute avant ou après l’installation — partagent ce runtime mais utilisent leurs propres fonctions define et se trouvent sous [Config → Install Hooks](/l/fr/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,190 @@
---
title: Compétences et agents
description: Définir les compétences et les agents d'IA pour votre application.
icon: robot
---
<Warning>
Les compétences et les agents sont actuellement en phase alpha. Cette fonctionnalité fonctionne mais est encore en évolution.
</Warning>
Les applications peuvent définir des fonctionnalités d'IA au sein de l'espace de travail — des instructions de compétences réutilisables et des agents avec des invites système personnalisées.
<AccordionGroup>
<Accordion title="defineSkill" description="Définir des compétences pour les agents IA">
Les compétences définissent des instructions et des capacités réutilisables que les agents IA peuvent utiliser dans votre espace de travail. Utilisez `defineSkill()` pour définir des compétences avec validation intégrée :
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Points clés :
* `name` est une chaîne d'identification unique pour la compétence (kebab-case recommandé).
* `label` est le nom d'affichage lisible par l'utilisateur dans l'UI.
* `content` contient les instructions de la compétence — c'est le texte que l'agent IA utilise.
* `icon` (optionnel) définit l'icône affichée dans l'UI.
* `description` (optionnel) fournit un contexte supplémentaire sur l'objectif de la compétence.
</Accordion>
<Accordion title="defineAgent" description="Définir des agents d'IA avec des prompts personnalisés">
Les agents sont des assistants IA qui vivent dans votre espace de travail. Utilisez `defineAgent()` pour créer des agents avec un prompt système personnalisé :
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Points clés :
* `name` est un identifiant unique pour l'agent (kebab-case recommandé).
* `label` est le nom d'affichage montré dans l'UI.
* `prompt` est le prompt système qui définit le comportement de l'agent.
* `description` (optionnel) fournit un contexte sur ce que fait l'agent.
* `icon` (optionnel) définit l'icône affichée dans l'UI.
* `modelId` (optionnel) remplace le modèle d'IA par défaut utilisé par l'agent.
* `responseFormat` (facultatif) contrôle la forme de la sortie de l'agent. Par défaut, la valeur est `{ type: 'text' }` pour le texte libre. Utilisez `{ type: 'json', schema }` pour forcer une sortie JSON structurée.
Par défaut, un agent renvoie du texte libre. Pour obtenir une sortie structurée, définissez `responseFormat` sur `{ type: 'json' }` et fournissez un `schema` :
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Remarques sur le schéma :
* Le schéma est un objet plat : le `type` de chaque propriété doit être un type primitif (`string`, `number` ou `boolean`). Les objets imbriqués et les tableaux ne sont pas pris en charge.
* `description` (facultatif) sur chaque propriété guide le modèle sur ce qu'il doit y mettre.
* `required` (facultatif) répertorie les propriétés que le modèle doit toujours renvoyer.
* `additionalProperties: false` (facultatif) interdit toute propriété non déclarée dans `properties`.
</Accordion>
<Accordion title="runAgent" description="Exécuter un agent depuis une fonction logique">
`runAgent()` permet à une fonction logique d'exécuter l'un des agents de votre application (avec ses compétences et ses outils). Identifiez l'agent à l'aide du `universalIdentifier` que vous avez passé
à `defineAgent()`. Passez soit une chaîne de caractères `prompt`, soit un historique de conversation `messages`,
mais pas les deux :
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Pour les bots multi-tours (Slack, Discord, Teams, …), passez l'historique du fil de discussion en
`messages` au lieu d'un seul `prompt` :
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Points clés :
* Fournissez **exactement l'un des deux** : `prompt` (string) ou `messages` (1 à 100 entrées
de `{ role: 'user' | 'assistant', content: string }`).
* L'agent s'exécute **de manière synchrone** et peut lire/mettre à jour lui-même des enregistrements via ses propres outils — `runAgent()` se résout une fois l'exécution terminée.
* Une application ne peut exécuter que ses propres agents.
* Le [rôle par défaut](/l/fr/developers/extend/apps/config/roles) de l'application doit accorder l'indicateur d'autorisation `AI` — ajoutez `SystemPermissionFlag.AI` à ses `permissionFlagUniversalIdentifiers` (ou définissez `canAccessAllTools: true`).
Sans cela, `runAgent()` échoue avec une erreur d'autorisation.
* Définissez une valeur généreuse pour `timeoutSeconds` sur la fonction logique — les exécutions d'agent peuvent prendre plusieurs secondes.
* `success` est `true` et `result` est non nul lorsque l'exécution se termine ; en cas d'échec, `success` est `false`, `result` est `null`, et `error` contient la raison (par exemple, lorsque l'espace de travail épuise ses crédits AI en cours d'exécution).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Évitez les boucles :** si vous appelez `runAgent()` à partir d'un déclencheur d'événement de base de données `*.updated` et que l'agent met à jour le même enregistrement, limitez le déclencheur avec `updatedFields` à un champ que l'agent n'écrit jamais (par exemple l'URL source), ou vérifiez si un champ cible est encore vide avant d'appeler `runAgent()`.
</Warning>
### Exécution au nom d’un membre de l’espace de travail
Passez `runAsWorkspaceMemberId` lorsque l’exécution est déclenchée par une personne — par exemple un chatbot répondant à un message — afin que l’agent agisse comme ce membre plutôt qu’en tant qu’application :
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
L’exécution agit alors avec **le propre rôle du membre** : elle peut faire tout ce que ce membre peut faire et rien de plus, les enregistrements qu’elle crée lui sont attribués, et ses permissions au niveau des lignes s’appliquent. Le rôle de l’agent ne participe pas — il s’agit de la valeur par défaut de l’application pour les exécutions sans personne derrière. Omettez ce champ pour les exécutions autonomes (tâches planifiées, déclencheurs d’événements de base de données) : celles-ci conservent le rôle propre de l’agent. L’application qui agit reste rattachée au contexte de l’exécution à des fins de traçabilité, sans restreindre les permissions du membre.
Votre application est responsable de faire correspondre la personne qui a déclenché l’exécution à un membre de l’espace de travail. Nommer un membre nécessite un jeton d’accès de l’application, et ce qu’un jeton peut
nommer dépend du fait qu’il soit ou non associé à un utilisateur :
* Un jeton **sans utilisateur associé** peut nommer n’importe quel membre. Une fonction logique s’exécute
avec un tel jeton, tout comme les jetons créés via `client_credentials` ou à partir d’une clé
d’API.
* Un jeton émis **au nom d’un utilisateur**, comme en reçoit un composant frontal, ne peut
nommer que le membre de cet utilisateur lui-même.
Tout autre type — une simple session utilisateur, une clé d’API sans jeton d’application — ne peut pas
nommer de membre du tout.
<Warning>
`runAgent()` lève une exception lorsque le membre de l’espace de travail ne peut pas être résolu — un
membre inconnu ou supprimé, ou un membre sans rôle. Elle ne revient jamais au rôle
propre de l’agent, car cela accorderait plus d’accès que ce que l’appelant a demandé.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: Les commandes `yarn twenty` pour exécuter des fonctions, diffuser en continu les journaux, gérer les installations d'applications et basculer entre des remotes.
icon: terminal
---
Le CLI `yarn twenty` est votre interface pour tout ce qui concerne les applications. Liste complète des commandes :
| Commande | Ce que cela fait | Documenté dans |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `dev` | Surveille les fichiers sources et synchronise en direct les modifications | [Prise en main rapide](/l/fr/developers/extend/apps/getting-started/quick-start) |
| `plan` | Prévisualiser les modifications de métadonnées sans les appliquer | [Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `appliquer` | Appliquer les modifications de métadonnées après avoir affiché le plan | [Synchronisation et récupération](/l/fr/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Compiler l’application et générer le client d’API (`--tarball` pour empaqueter un `.tgz`) | [Publication](/l/fr/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | Exécuter la vérification des types TypeScript | [Tests](/l/fr/developers/extend/apps/operations/testing) |
| `dev:add` | Générer la structure d’une nouvelle entité | [Génération de structure](/l/fr/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Régénérer le client d’API typé | cette page |
| `dev:function:exec` / `dev:function:logs` | Exécuter des fonctions et afficher leurs journaux en continu | cette page |
| `dev:translations-extract` | Extraire les chaînes traduisibles dans les catalogues `locales/` | [Traductions](/l/fr/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Déclencher la synchronisation du catalogue de la place de marché | [Publication](/l/fr/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Cycle de vie de publication | [Publication](/l/fr/developers/extend/apps/operations/publishing) et cette page |
| `docker:*` | Gérer le conteneur du serveur Twenty local | [Serveur local](/l/fr/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Gérer les connexions serveur | cette page |
Chaque commande accepte `-r, --remote \<name>` pour cibler un serveur distant spécifique au lieu de celui par défaut.
## Exécuter des fonctions (`yarn twenty dev:function:exec`)
Exécutez une fonction logique manuellement sans la déclencher via HTTP, cron ou un événement de base de données :
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## Afficher les journaux des fonctions (`yarn twenty dev:function:logs`)
Diffuser en continu les journaux d'exécution des fonctions logiques de votre application :
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Ceci est différent de `yarn twenty docker:logs`, qui affiche les journaux du conteneur Docker. `yarn twenty dev:function:logs` affiche les journaux d'exécution des fonctions de votre application depuis le serveur Twenty.
</Note>
## Génération du client typé (`yarn twenty dev:generate-client`)
Régénérez le client API typé (`twenty-client-sdk`) à partir du schéma du remote actif, sans compiler ni synchroniser une application. Utilisez-le pour obtenir un client typé dans n’importe quel projet — comme un service back-end hébergé dans un dépôt distinct — qui communique avec votre instance Twenty :
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Puis importez le client dans votre code :
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Relancez la commande chaque fois que votre modèle de données change afin d’actualiser les types générés.
<Note>
Le client est généré dans `node_modules`, il n’est donc pas versionné avec votre code. Exécutez `yarn twenty dev:generate-client` après chaque installation (par exemple dans un script `postinstall` ou dans un pipeline CI).
</Note>
## Désinstaller une application (`yarn twenty app:uninstall`)
Supprimez votre application de l'espace de travail actif :
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## Gestion des remotes
Un remote est un serveur Twenty auquel votre application se connecte. Lors de la configuration, le générateur en crée un automatiquement pour vous. Vous pouvez ajouter d’autres remotes ou basculer entre eux à tout moment.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Vos identifiants sont stockés dans `~/.twenty/config.json`.

View File

@@ -0,0 +1,32 @@
---
title: Vue d'ensemble
description: Créez, testez et livrez votre application — commandes CLI, tests d’intégration, CI et publication sur un serveur ou sur npm.
icon: rocket
---
La **couche d’opérations** regroupe tout ce que vous faites *à* votre application plutôt qu’*avec* elle : lancer des commandes CLI, exécuter des tests d’intégration sur un véritable serveur Twenty, configurer la CI et livrer des versions — soit sous la forme d’une archive tar déployée sur un seul serveur, soit sous la forme d’un package npm répertorié sur la place de marché.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## Dans cette section
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/fr/developers/extend/apps/operations/cli">
Référence `yarn twenty` — exec, logs, uninstall, remotes.
</Card>
<Card title="Synchronisation et récupération" icon="compass" href="/l/fr/developers/extend/apps/operations/sync-and-recovery">
Quelle commande utiliser et quand, comment lire le diff de synchronisation, et une procédure de récupération.
</Card>
<Card title="Tests" icon="flask" href="/l/fr/developers/extend/apps/operations/testing">
Configuration de Vitest, tests d’intégration, vérification des types, flux de travail CI.
</Card>
<Card title="Publication" icon="upload" href="/l/fr/developers/extend/apps/operations/publishing">
Générer, déployer une archive tar, publier sur npm, installer.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: Publication
icon: upload
description: Distribuez votre application Twenty sur la marketplace ou déployez-la en interne.
---
## Vue d'ensemble
Une fois que votre application est [créée et testée en local](/l/fr/developers/extend/apps/getting-started/concepts), vous avez deux options pour sa distribution :
* **Déployer une archive tar** — téléversez votre application directement sur un serveur Twenty spécifique pour un usage interne ou privé.
* **Publier sur npm** — répertoriez votre application dans la marketplace Twenty pour que tout espace de travail puisse la découvrir et l'installer.
Les deux parcours commencent à la même étape de **build**.
## Compilation de votre application
Exécutez la commande `build` pour compiler votre application et générer un `manifest.json` prêt pour la distribution :
```bash filename="Terminal"
yarn twenty dev:build
```
Cela compile les sources TypeScript, transpile les fonctions de logique et les composants front-end, et écrit le tout dans `.twenty/output/`. Ajoutez `--tarball` pour produire également un paquet `.tgz` pour la distribution manuelle ou la commande `publish`.
## Déploiement sur un serveur (archive tar)
Pour les applications que vous ne souhaitez pas rendre publiques — outils propriétaires, intégrations réservées aux entreprises ou versions expérimentales — vous pouvez déployer une archive tar directement sur un serveur Twenty.
### Prérequis
Avant le déploiement, vous avez besoin d'un remote configuré pointant vers le serveur cible. Les remotes stockent localement l'URL du serveur et les informations d'authentification dans `~/.twenty/config.json`.
Ajouter un remote :
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Déploiement
Compilez et téléversez votre application sur le serveur en une seule étape :
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Partage d'une application déployée
Les applications au format tarball ne sont pas répertoriées dans la place de marché publique, donc les autres espaces de travail sur le même serveur ne les découvriront pas en parcourant. Pour partager une application déployée :
1. Allez dans **Paramètres > Applications > Inscriptions** et ouvrez votre application
2. Dans l'onglet **Distribution**, cliquez sur **Copier le lien de partage**
3. Partagez ce lien avec des utilisateurs sur d'autres espaces de travail — il les amène directement à la page d'installation de l'application
Le lien de partage utilise l'URL de base du serveur (sans sous-domaine d'espace de travail) afin qu'il fonctionne pour tout espace de travail sur le serveur.
### Gestion des versions
Lors de la mise à jour d'une application au format tarball déjà déployée, le serveur exige que la `version` dans `package.json` soit **strictement supérieure** (selon l'ordre [SemVer](https://semver.org)) à la version actuellement déployée. Le redéploiement de la même version, ou la tentative d’en pousser une inférieure, est refusé avant que l’archive tar ne soit stockée — vous verrez une erreur `VERSION_ALREADY_EXISTS` émise par la CLI.
Pour publier une mise à jour :
1. Incrémentez le champ `version` dans votre `package.json` (par ex. `1.2.3` → `1.2.4`, `1.3.0` ou `2.0.0`)
2. Exécutez `yarn twenty app:publish --private` (ou `yarn twenty app:publish --private --remote production`)
3. Les espaces de travail qui ont installé l’application et y ont activé la mise à niveau automatique (dans l’onglet Paramètres de l’application) sont mis à niveau automatiquement en arrière-plan ; les autres verront la mise à niveau disponible dans leurs paramètres
<Note>
Les tags de préversion fonctionnent comme prévu : incrémenter `1.0.0-rc.1` → `1.0.0-rc.2` est autorisé, et une version finale telle que `1.0.0` est correctement reconnue comme supérieure à `1.0.0-rc.5`. La version dans `package.json` doit elle-même être une chaîne SemVer valide.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilité des versions du serveur
Si votre application utilise une fonctionnalité introduite dans une version spécifique du serveur Twenty (par exemple, des fournisseurs OAuth ajoutés en v2.3.0), vous devez déclarer la version minimale du serveur requise par votre application en utilisant le champ `engines.twenty` dans `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
La valeur est une [plage semver](https://github.com/npm/node-semver#ranges) standard. Modèles courants:
| Plage | Signification |
| ---------------------------------- | ------------------------------------------------------------------- |
| `>=2.3.0` | Tout serveur à partir de 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 ou ultérieure, mais inférieure à la prochaine version majeure |
| `^2.3.0` | Identique à `>=2.3.0 \<3.0.0` |
**Ce qui se passe lors du déploiement et de l'installation:**
* Si `engines.twenty` est défini et que la version du serveur cible ne satisfait pas la plage, le déploiement (chargement du tarball) ou l'installation est refusé avec une erreur `SERVER_VERSION_INCOMPATIBLE` et un message indiquant à la fois la plage requise et la version réelle du serveur.
* Si `engines.twenty` est **non défini**, l'application est acceptée sur n'importe quelle version du serveur (rétrocompatible avec les applications existantes).
* Si le serveur n'a pas de `APP_VERSION` configuré, la vérification est ignorée.
<Note>
Le serveur est l'autorité de référence pour la vérification — il valide `engines.twenty` tant lors du chargement du tarball que lors de l'installation dans l'espace de travail. Si vous déployez un tarball hors bande ou installez depuis la place de marché, le serveur applique toujours la compatibilité.
</Note>
## CI/CD automatisés (workflows préconfigurés)
Les applications générées avec `create-twenty-app` sont livrées avec trois workflows GitHub Actions prêts à l’emploi, sous `.github/workflows/`. L’intégration continue (CI) s’exécute sans configuration, le déploiement continu (CD) ne nécessite qu’un seul secret, et la publication sur npm requiert une configuration unique de npm trusted-publisher.
### CI — `ci.yml`
Il exécute automatiquement vos tests d’intégration à chaque push sur `main` et sur les pull requests.
**Ce que cela fait:**
1. Récupère le code source de votre application.
2. Lance une instance de test Twenty isolée à l’aide de l’action composite `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (l’équivalent CI de `yarn twenty docker:start --test`).
3. Active Corepack, configure Node.js à partir de votre `.nvmrc` et installe les dépendances avec `yarn install --immutable`.
4. Exécute `yarn test`, en passant `TWENTY_API_URL` et `TWENTY_API_KEY` depuis l’instance lancée pour que vos tests puissent communiquer avec un serveur réel.
**Paramètres de configuration:**
* `TWENTY_VERSION` (variable d’environnement, par défaut `latest`) — figez la version du serveur Twenty utilisée en CI en la modifiant dans `ci.yml`.
* La concurrence est regroupée par `github.ref` et annule les exécutions en cours lors de nouveaux push.
Aucun secret n’est requis — l’instance de test est éphémère et n’existe que pendant la durée du job.
### CD — `cd.yml`
Déploie votre application vers un serveur Twenty configuré à chaque push sur `main`, et éventuellement depuis une pull request lorsque le label `deploy` est appliqué.
**Ce que cela fait:**
1. Récupère la tête de la PR (pour les PR étiquetées) ou le commit poussé.
2. Exécute `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — l’équivalent CI de `yarn twenty app:publish --private`.
3. Exécute `twentyhq/twenty/.github/actions/install-twenty-app@main` afin que la version nouvellement déployée soit installée dans l’espace de travail cible.
**Configuration requise:**
| Paramètre | Où | Objectif |
| ----------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` dans `cd.yml` (par défaut `http://localhost:3000`) | Le serveur Twenty vers lequel déployer. Remplacez ceci par l’URL réelle de votre serveur avant la première utilisation. |
| `TWENTY_DEPLOY_API_KEY` | Dépôt GitHub **Settings → Secrets and variables → Actions** | Clé API avec la permission de déployer sur le serveur cible. |
<Note>
La valeur par défaut de `TWENTY_DEPLOY_URL`, `http://localhost:3000`, est un espace réservé — elle n’atteindra rien depuis un runner hébergé par GitHub. Remplacez-la par l’URL publique de votre serveur (ou utilisez un runner auto-hébergé avec accès réseau) avant d’activer la CD.
</Note>
**Déclencher un déploiement de prévisualisation depuis une PR:**
Ajoutez le label `deploy` à une pull request. La condition `if:` dans `cd.yml` exécutera le job pour cette PR en utilisant le commit de tête de la PR, vous permettant de valider une modification sur le serveur cible avant la fusion.
### Publication — `publish.yml`
Publie votre application sur npm avec provenance lorsque vous poussez une étiquette de version (par exemple `v1.0.0`), ou lorsque vous exécutez manuellement le workflow depuis l’onglet Actions.
**Ce que cela fait:**
1. Récupère votre application, configure Node.js et met à jour npm (la publication de confiance nécessite npm 11.5.1 ou une version ultérieure).
2. Exécute `yarn twenty app:publish`, qui génère l’application et publie `.twenty/output` sur npm. En CI, il ajoute automatiquement `--provenance` et `--access public`, de sorte qu’aucun indicateur n’est nécessaire dans le workflow.
**Configuration unique :**
Sur npmjs.com, ouvrez votre package > **Settings → Trusted Publisher** et enregistrez ce dépôt avec le workflow `publish.yml` (voir la [documentation npm sur la publication de confiance](https://docs.npmjs.com/trusted-publishers)). La publication avec provenance certifie quel dépôt GitHub a construit le package, ce qui est aussi la façon de revendiquer la propriété de votre application dans une marketplace Twenty.
<Note>
npm n’accepte la provenance que depuis des dépôts sources **publics**. Si vous publiez depuis un dépôt privé, npm rejette le bundle de provenance OIDC avec une erreur `E422 ... Erreur « Unsupported GitHub Actions source repository visibility: "private"` ». Pour publier depuis un dépôt privé, désactivez la provenance en définissant `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` dans la variable `env` de l’étape de publication (un indice commenté est inclus dans le fichier `publish.yml` généré automatiquement) :
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Épingler les actions réutilisables
Les workflows `ci.yml` et `cd.yml` référencent des actions réutilisables sur `@main`, de sorte que les mises à jour des actions dans le dépôt `twentyhq/twenty` sont récupérées automatiquement. Si vous souhaitez des builds déterministes, remplacez `@main` par un SHA de commit ou un tag de version sur chaque ligne `uses:`.
## Publication sur npm
Publier sur npm rend votre application découvrable dans la marketplace Twenty. N'importe quel espace de travail Twenty peut parcourir, installer et mettre à niveau des applications de la marketplace directement depuis l'interface utilisateur.
### Prérequis
* Un compte [npm](https://www.npmjs.com)
* Le mot-clé `twenty-app` dans le tableau `keywords` de votre `package.json` (à ajouter manuellement — il n'est pas inclus par défaut dans le modèle `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Métadonnées de la marketplace
La configuration `defineApplication()` prend en charge des champs optionnels qui contrôlent la façon dont votre application apparaît sur la place de marché. Utilisez `logo` et `galleryImages` pour référencer des images depuis le dossier `public/` :
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Voir l'[accordéon defineApplication](/l/fr/developers/extend/apps/config/application#marketplace-metadata) dans la page Building Apps pour la liste complète des champs de la place de marché (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensions recommandées pour les images de la galerie
La place de marché affiche `galleryImages` dans un conteneur fixe `8:5` (par exemple, `1600×1000 px`).
<Note>
Les images de la galerie, quel que soit leur ratio d'aspect, sont affichées en entier et ne sont jamais recadrées, mais tout ce qui est nettement plus haut ou plus étroit que `8:5` affichera des bandes vides sur les côtés.
</Note>
#### Limite de taille d’image
Le `logo` et chaque fichier `galleryImages` ne doivent pas dépasser **10 Mo**. Les fichiers plus volumineux sont ignorés lorsque la place de marché réhéberge vos ressources publiées, ils ne seront donc pas affichés.
### Publier
```bash filename="Terminal"
yarn twenty app:publish
```
Pour publier sous un dist-tag spécifique (par exemple, `beta` ou `next`) :
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### Comment fonctionne la découverte de la place de marché
Le serveur Twenty synchronise son catalogue de la place de marché à partir du registre npm **toutes les heures**.
Vous pouvez déclencher la synchronisation immédiatement au lieu d'attendre :
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
Les métadonnées affichées dans la marketplace proviennent de votre configuration `defineApplication()` — voir [Métadonnées de la marketplace](#marketplace-metadata) ci-dessus.
<Note>
Si votre application ne définit pas de `aboutDescription` dans `defineApplication()`, la place de marché utilisera automatiquement le `README.md` de votre package depuis npm comme contenu de la page À propos. Cela signifie que vous pouvez maintenir un seul README à la fois pour npm et pour la place de marché Twenty. Si vous souhaitez une description différente dans la place de marché, définissez explicitement `aboutDescription`.
</Note>
### Publication via CI
Le workflow `publish.yml` préconfiguré décrit ci-dessus publie automatiquement sur npm lors des tags de version, avec provenance. Comme `yarn twenty app:publish` ajoute `--provenance` et `--access public` pour vous lorsqu’il s’exécute en CI, le workflow n’a besoin d’aucune option npm — seulement de la configuration unique de Trusted Publisher.
Pour d’autres systèmes de CI (GitLab CI, CircleCI, etc.), exécutez `yarn install` puis `yarn twenty app:publish`. La provenance est émise lorsque l’environnement peut générer un jeton OIDC et est ignorée automatiquement dans le cas contraire.
<Note>
La **provenance npm** ajoute un badge de confiance à votre fiche npm, permettant aux utilisateurs de vérifier que le package a été construit à partir d’un commit spécifique dans un pipeline CI public. C’est également ce qui vous permet de revendiquer la propriété de votre application dans une marketplace Twenty. Voir la [documentation npm sur la provenance](https://docs.npmjs.com/generating-provenance-statements) pour plus de détails.
</Note>
## Installation des applications
Une fois qu'une application est publiée (npm) ou déployée (tarball), les espaces de travail peuvent l'installer via l'interface utilisateur.
Accédez à la page **Paramètres > Applications** dans Twenty, où les applications de la place de marché et celles déployées en tarball peuvent être parcourues et installées.
{/* TODO: add screenshot of the UI when the app is registered */}
Vous pouvez également installer des applications depuis la ligne de commande :
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
Le serveur applique le versionnement SemVer à l’installation, en reprenant les mêmes règles que lors du déploiement :
* L’installation de la même version déjà installée dans votre espace de travail est refusée avec une erreur `APP_ALREADY_INSTALLED`.
* L’installation d’une version inférieure à celle actuellement installée est refusée avec une erreur `CANNOT_DOWNGRADE_APPLICATION`.
Pour installer une version plus récente, déployez-la ou publiez-la d’abord, puis relancez `yarn twenty app:install`.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: Synchronisation et récupération
description: Quelle commande utiliser et quand, comment lire la sortie de synchronisation, et une échelle de récupération pour les cas où les métadonnées locales dérivent — avant d'en arriver à une réinitialisation complète.
icon: compass
---
Le développement local d'applications tourne autour de la **synchronisation** : le CLI reconstruit votre manifeste et le serveur applique uniquement la différence entre celui-ci et les métadonnées déjà présentes dans votre espace de travail. Cette page explique vers quelle commande se tourner, comment lire ce qu'une synchronisation a modifié, et quoi faire — dans l'ordre — lorsque l'état local semble incohérent.
## Quelle commande, quand
<Note>
Pour l'itération locale au quotidien, vous voudrez presque toujours `yarn twenty dev`. Le déploiement et la publication servent à livrer des versions, **pas** pour la boucle locale.
</Note>
| Vous souhaitez… | Commande | Notes |
| ------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Itérer localement avec la synchronisation en direct | `yarn twenty dev` | Surveille vos fichiers et synchronise à chaque modification. |
| Synchroniser une fois puis quitter (CI, scripts, hooks) | `yarn twenty apply` | Effectue une compilation + synchronisation, puis quitte. Ajoutez `--force` pour ignorer la confirmation des changements destructifs. |
| Prévisualiser les changements **sans les appliquer** | `yarn twenty plan` | Calcule et affiche le diff ; n'écrit rien. |
| Retirer l'application de l'espace de travail | `yarn twenty app:uninstall` | Ajoutez `--yes` pour ignorer la confirmation. |
| Envoyer une archive tarball vers un serveur | `yarn twenty app:publish --private` | Nécessite une version de `package.json` **strictement supérieure** — voir [Publication](/l/fr/developers/extend/apps/operations/publishing). |
| Publier sur la place de marché (npm) | `yarn twenty app:publish` | — |
| Installer / mettre à niveau une version déployée | `yarn twenty app:install` | Installe la version actuellement déployée. |
| Effacer le serveur local et repartir de zéro | `yarn twenty docker:reset` | Supprime **toutes** les données locales — en dernier recours. |
<Note>
`yarn twenty dev --once` et `yarn twenty dev --once --dry-run` fonctionnent toujours comme alias obsolètes de `yarn twenty apply` et `yarn twenty plan`.
</Note>
### La synchronisation locale n'a pas besoin d'un incrément de version
La règle de `version` strictement croissante (`VERSION_ALREADY_EXISTS` lors du déploiement, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` lors de l'installation) s'applique à **`app:publish` / `app:install`** — le chemin de mise en production. `yarn twenty dev` synchronise votre manifeste sur place et ne nécessite jamais de changement de version, vous n'avez donc pas besoin de toucher à `package.json` pour itérer. Si vous vous surprenez à incrémenter la version pour tester un changement local, c'est que vous utilisez le chemin de mise en production alors que vous voulez la boucle de développement.
## Lire la sortie de synchronisation
Chaque synchronisation affiche les changements de métadonnées qu'elle a appliqués (ou qu'elle appliquerait, avec `plan`), à la manière de Terraform — un bloc par entité avec ses attributs, puis une ligne récapitulative :
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
C'est votre premier diagnostic : il vous indique exactement quels objets, champs et mises en page ont changé, afin que vous puissiez confirmer qu'une synchronisation a fait ce que vous attendiez avant de vérifier l'interface utilisateur.
Les changements destructifs (`to destroy`) sont listés avec ce qu'ils suppriment (par ex. `objectMetadata "auditNote" — drops the table and all its rows`) et nécessitent une confirmation interactive, ou `--force` dans les scripts.
Lorsqu'une synchronisation échoue sur une seule entité, l'erreur nomme l'entité en cause et son `universalIdentifier`, par exemple :
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Utilisez cet identifiant pour trouver l'entité dans votre manifeste (et, si nécessaire, dans l'espace de travail) plutôt que de deviner laquelle est en conflit.
## Prévisualiser les changements (plan)
`yarn twenty plan` construit votre manifeste, demande au serveur le plan de migration et l'affiche — **sans rien appliquer**. C'est le moyen sûr de répondre « que changerait cette synchronisation ? » avant de s'y engager.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Un plan :
* **N'écrit rien** — aucune migration de métadonnées, aucune mise à jour de l'enregistrement d'application, aucun changement de rôle/onglet par défaut, et aucune génération de client d'API.
* Renvoie le **même diff** qu'une synchronisation réelle appliquerait, afin que vous puissiez examiner à l'avance les entités créées/mises à jour/supprimées.
* Est utile avant un changement risqué, lors de la révision d'un changement généré par une IA, ou dans un script qui doit échouer si un changement inattendu est sur le point d'être appliqué.
<Note>
Un plan ne prévisualise que les changements de **métadonnées**, et il nécessite que l'application ait été synchronisée au moins une fois (pour que l'espace de travail la connaisse). Si vous l'exécutez sur une application qui n'a jamais été synchronisée, le serveur indique que l'application n'est pas installée — exécutez d'abord une fois `yarn twenty dev`.
</Note>
## Échelle de récupération
Lorsque les métadonnées locales semblent incorrectes, augmentez le niveau de manière progressive dans cet ordre et arrêtez-vous dès que vous êtes débloqué. Chaque étape est plus perturbatrice que la précédente.
1. **Resynchroniser.** Exécutez à nouveau `yarn twenty apply`. Les synchronisations sont idempotentes — réexécuter un manifeste propre est sûr et résout souvent un incident passager.
2. **Prévisualiser le plan.** Exécutez `yarn twenty plan` pour voir exactement ce que la prochaine synchronisation compte changer, sans l'appliquer.
3. **Lire l'erreur nommée.** Si une synchronisation échoue, relevez le type de métadonnées et l'`universalIdentifier` dans le message (voir ci-dessus) et localisez cette entité dans votre manifeste. Un conflit pointe généralement vers un identifiant dupliqué ou réutilisé.
4. **Désinstaller et réinstaller.** `yarn twenty app:uninstall`, puis synchronisez à nouveau (`yarn twenty dev`). Cette opération reconstruit les métadonnées de l'application à partir d'une base saine tout en gardant le reste de votre espace de travail intact.
5. **Réinitialisation complète (en dernier recours).** `yarn twenty docker:reset`, puis réinjectez des données et resynchronisez.
<Warning>
`yarn twenty docker:reset` supprime **toutes** les données de votre instance locale — chaque espace de travail, enregistrement et application. Ne l'utilisez que lorsque les étapes précédentes ont échoué.
</Warning>
<Note>
Vous avez rencontré une erreur de métadonnées ? Veuillez [ouvrir un ticket](https://github.com/twentyhq/twenty/issues/new/choose) et inclure le message d'échec de migration (avec son type de métadonnées et son `universalIdentifier`), la sortie `Metadata changes` de la synchronisation, ainsi que les commandes que vous avez exécutées.
</Note>
## Éviter les synchronisations concurrentes sur un même espace de travail
La synchronisation applique des migrations de métadonnées. Exécuter plusieurs opérations de synchronisation, de déploiement ou d'installation contre le **même espace de travail au même moment** — par exemple, plusieurs terminaux ou agents IA qui itèrent en parallèle — peut entrelacer ces migrations et laisser les métadonnées dans un état partiellement appliqué.
Le serveur sérialise les synchronisations par espace de travail pour éviter cela, mais vous devriez tout de même faire transiter les opérations sensibles sur les métadonnées par un **processus unique** plutôt que de les lancer en concurrence. Si vous orchestrez le développement avec plusieurs agents, faites passer leurs appels de synchronisation/déploiement/installation par une file unique afin qu'un seul s'exécute à la fois.
## Distinguer les types d'échecs
Lorsque quelque chose se passe mal, le diff de métadonnées et les erreurs nommées vous permettent de localiser l'échec :
* **Erreur de construction du manifeste** — le CLI échoue avant la synchronisation (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`) ; corrigez le code source de votre application.
* **Erreur de synchronisation / migration** — la construction réussit mais l'application du diff échoue, en nommant l'entité et son `universalIdentifier` ; corrigez les métadonnées en conflit.
* **Erreur de taille des dépendances** — la synchronisation ou l’installation échoue parce que les `dependencies` de production de l’application sont trop volumineuses pour être installées en tant que couche d’exécution (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`) ; déplacez vers `devDependencies` les packages que vos fonctions logiques n’importent pas au moment de l’exécution (bibliothèques d’interface utilisateur, outils de développement).
* **Erreur d’exécution du code de l’application** — la synchronisation réussit, mais vos fonctions logiques ou composants se comportent mal à l’exécution ; consultez les [journaux de fonctions](/l/fr/developers/extend/apps/operations/cli).
* **État local de l’instance** — aucun des cas ci-dessus et l’espace de travail semble toujours incorrect ; descendez l’échelle de récupération.

View File

@@ -0,0 +1,264 @@
---
title: Tests
description: Configuration de Vitest, tests d’intégration sur un serveur Twenty réel, vérification des types et CI avec GitHub Actions.
icon: flask
---
Le SDK fournit des API programmatiques qui vous permettent de construire, déployer, installer et désinstaller votre application depuis le code de test. Combiné avec [Vitest](https://vitest.dev/) et les clients d'API typés, vous pouvez écrire des tests d'intégration qui vérifient que votre application fonctionne de bout en bout sur un serveur Twenty réel.
## Utilisation de packages npm
Vous pouvez installer et utiliser n'importe quel package npm dans votre application. Les fonctions logiques et les composants frontaux sont tous deux empaquetés avec [esbuild](https://esbuild.github.io/), qui intègre leurs dépendances dans la sortie — aucun `node_modules` n'est nécessaire à l'exécution.
### Installation d'un package
```bash filename="Terminal"
yarn add axios
```
Puis importez-le dans votre code :
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Il en va de même pour les composants frontaux :
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Comment fonctionne le bundling
L'étape de build utilise esbuild pour produire un seul fichier autonome par fonction logique et par composant frontal. Les paquets importés sont intégrés dans le bundle, à l’exception de ceux listés ci-dessous.
**Les fonctions logiques** s'exécutent dans un environnement Node.js. Les modules intégrés de Node (`fs`, `path`, `crypto`, `http`, etc.) sont disponibles et n'ont pas besoin d'être installés.
**Les composants frontaux** s'exécutent dans un Web Worker. Les modules intégrés de Node ne sont **pas** disponibles — seuls les packages npm qui fonctionnent dans un environnement navigateur sont pris en charge. Notez que le bac à sable implémente un DOM *partiel*, de sorte qu’un package peut être compilé sans erreur tout en échouant à l’exécution ; voir [Limitations actuelles](/l/fr/developers/extend/apps/layout/front-components#current-limitations).
Les deux environnements disposent de `twenty-client-sdk/core` et `twenty-client-sdk/metadata` en tant que modules pré-fournis — ils ne sont pas intégrés au bundle mais résolus à l'exécution par le serveur.
Les composants frontaux peuvent également partager des dépendances via un [bundle de dépendances partagées](/l/fr/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). Les paquets listés dans le champ `frontComponentSharedDependencies` du fichier package.json de l'application sont construits une fois dans un fichier séparé et chargés à partir de celui-ci à l'exécution au lieu d'être intégrés à chaque composant, de sorte qu'un composant qui n'importe que des paquets partagés n'embarque quasiment aucun code de dépendance propre.
## Installation
L'application générée inclut déjà Vitest. Si vous le configurez manuellement, installez les dépendances :
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Créez un `vitest.config.ts` à la racine de votre application :
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Créez un fichier de configuration global qui vérifie que le serveur est joignable, écrit une configuration de test pour le SDK (`~/.twenty/config.test.json`) et synchronise l’application avant l’exécution des tests :
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## APIs programmatiques du SDK
Le sous-chemin `twenty-sdk/cli` exporte des fonctions que vous pouvez appeler directement depuis le code de test :
| Fonction | Description |
| -------------- | ----------------------------------------------------------------------------------- |
| `appBuild` | Construire l'application et éventuellement créer une archive tarball |
| `appDeploy` | Téléverser une archive tarball vers le serveur |
| `appDevOnce` | Construire et synchroniser l’application une fois (identique à `yarn twenty apply`) |
| `appInstall` | Installer l'application sur l'espace de travail actif |
| `appUninstall` | Désinstaller l'application de l'espace de travail actif |
Chaque fonction retourne un objet résultat avec `success: boolean` et soit `data` soit `error`.
## Écrire un test d'intégration
Voici un exemple complet qui construit, déploie et installe l'application, puis vérifie qu'elle apparaît dans l'espace de travail :
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## Exécuter les tests
Assurez-vous que votre serveur Twenty local est en cours d'exécution, puis :
```bash filename="Terminal"
yarn test
```
Ou en mode surveillance (watch) pendant le développement :
```bash filename="Terminal"
yarn test:watch
```
## Vérification des types
Vous pouvez également exécuter une vérification des types sur votre application sans exécuter les tests :
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Cela exécute `tsc --noEmit` sur le `tsconfig.json` de votre application et signale toute erreur de type. Les applications générées contiennent également un script `yarn typecheck` qui couvre aussi les fichiers de test (`tsconfig.spec.json`).
## CI avec GitHub Actions
Le générateur crée un workflow prêt à l’emploi dans `.github/workflows/ci.yml`. À chaque push sur `main` et à chaque pull request, il lance un serveur Twenty éphémère dans le runner (via l’action `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`), puis exécute `yarn lint`, `yarn typecheck`, `yarn test:unit` et `yarn test` avec `TWENTY_API_URL` / `TWENTY_API_KEY` pointant vers ce serveur. Aucun secret n’est requis, et vous pouvez fixer la version du serveur via la variable d’environnement `TWENTY_VERSION` en haut du workflow.
Voir [Publication → CI/CD automatisé](/l/fr/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) pour un guide complet des trois workflows générés (`ci.yml`, le pipeline de déploiement `cd.yml` et `publish.yml` pour la publication sur npm).

View File

@@ -0,0 +1,143 @@
---
title: Traductions
description: Publiez votre application en plusieurs langues — traduisez les libellés du manifeste et les chaînes d’interface utilisateur via un seul catalogue de locales.
icon: language
---
Les applications Twenty sont rédigées en **anglais** : les chaînes de vos sources et de votre manifeste
sont le texte source en anglais, `en` est la locale source à partir de laquelle vous traduisez, et
toute locale laissée non traduite revient à celle-ci par défaut.
Votre application comporte deux types de textes traduisibles, et tous deux transitent par le même
catalogue `locales/` :
* **Libellés du manifeste** — noms d’objets et de champs, titres de vues, éléments de menu et
autres chaînes déclarées dans les métadonnées de votre application.
* **Chaînes de front-component** — le texte d’interface utilisateur que vos composants React front affichent.
Vous marquez les chaînes traduisibles, les extrayez dans des catalogues par locale,
traduisez ces catalogues, et le build sert la bonne langue pour
l’utilisateur actuel — sans configuration supplémentaire.
## Marquer les chaînes de front-component
Importez les helpers de traduction depuis `twenty-sdk/front-component` :
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Quand utiliser quoi
* **`Trans`** — texte statique dans JSX. Utilisez les props `message` et `values` pour
l’interpolation, comme dans `Trans message="Hi {name}" values={{ name }}`;
l’interpolation directement dans les enfants n’est pas extractible statiquement.
* **`useTranslate().t`** — chaînes dynamiques à l’intérieur d’un composant. Se réaffiche lorsque
l’utilisateur change de langue. À privilégier à l’intérieur du rendu.
* **`t(...)`** (importé directement) — traduction immédiate utilisable **partout**,
y compris dans les gestionnaires d’événements, helpers et dans la portée du module — pas seulement dans le rendu.
* **`msg(...)`** — un descripteur paresseux pour les chaînes déclarées comme données (constantes,
configuration). Résolvez-le plus tard avec `t(descriptor)`.
### Contexte
Passez `context` pour lever l’ambiguïté entre des chaînes source identiques qui se traduisent
différemment :
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Les chaînes contextuelles apparaissent dans les catalogues de paramètres régionaux comme un groupe indexé par leur
contexte : `"\<context>": { "\<message>": "\<translation>" }`. Les libellés de manifeste sont
contextuels par construction — chacun porte le rôle de métadonnée qu’il joue
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), de sorte que
le même mot peut être traduit différemment en tant que nom d’objet et en tant que nom de vue.
## Extraction et traduction
Exécutez la commande d’extraction depuis le répertoire de votre application :
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
L’extraction collecte à la fois les libellés de votre manifeste et les chaînes `t()`/`msg()`/`Trans`
provenant de votre source front-component dans `locales/\<locale>.json`, indexées par
chaîne source. Renseignez les traductions :
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Les chaînes sans contexte sont des entrées plates ; les chaînes contextuelles — y compris chaque libellé de manifeste — sont imbriquées sous leur clé de contexte.
Les espaces réservés comme `{name}` sont substitués à l’exécution — conservez-les dans la
traduction. Toute chaîne laissée vide revient au texte source.
## Comment ça marche
`twenty dev:build` compile les catalogues et sert la bonne langue pour
l’utilisateur actuel : les libellés du manifeste sont résolus côté serveur, et les catalogues
front-component sont intégrés à chaque bundle de composant. À l’exécution, un composant lit la
locale depuis son contexte d’exécution (la langue actuelle de l’hôte) et résout
chaque chaîne à partir de son catalogue, en revenant à la source lorsqu’une traduction
manque. Changer la langue dans l’hôte réaffiche en direct les chaînes `Trans` et
`useTranslate().t`.
Les libellés du manifeste sont compilés à chaque synchronisation — `twenty dev:build`, `twenty apply`
et la surveillance continue de `twenty dev` —, donc modifier un fichier de paramètres régionaux et
le synchroniser suffit pour voir le nouveau texte. Les catalogues de composants front-end sont intégrés à
chaque bundle de composants lors de la compilation, donc modifier l'un d'eux implique toujours
de relancer `twenty dev:build` (et de redéployer).
Les enfants textuels de `Trans` peuvent s’étendre sur plusieurs lignes — les espaces sont réduits
de la même façon que JSX les réduit, donc ces deux exemples sont extraits avec la clé `Welcome back` :
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,81 @@
---
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 discussion 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
l’expé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>

View File

@@ -0,0 +1,303 @@
---
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 d’un 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. Le serveur provisionne déjà automatiquement, pour chaque objet, la vue par défaut « Tous ... » ; les vues définies par une application sont donc des vues supplémentaires, sélectionnées.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
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 { RestApiClient } from 'twenty-client-sdk/rest';
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 res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
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 n’est pas seulement destiné aux menus de commande — vous pouvez en monter un comme **onglet sur une page d’enregistrement**. 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 { RestApiClient } from 'twenty-client-sdk/rest';
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 = new RestApiClient().resolveUrl('/s/documents/view', {
query: { 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 disposition `RECORD_PAGE` ajoute des onglets à la vue d’enregistrement d’un objet ; un onglet avec un seul widget `FRONT_COMPONENT` héberge le composant en pleine largeur :
```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.VERTICAL_LIST,
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',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, 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).
Les widgets `FIELD` prennent également en charge d'autres modes d'affichage et options, notamment l’énumération des enregistrements d’une relation imbriquée à deux sauts — voir la [référence des widgets de champ](/l/fr/developers/extend/apps/layout/page-layouts#field-widgets).
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>

View File

@@ -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>

View File

@@ -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 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"
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 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"
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
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"
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>

View File

@@ -0,0 +1,150 @@
---
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 de l'application sont accessibles via le chemin `/s/` ; sur les instances locales
et auto-hébergées, ce chemin est géré par votre serveur Twenty
(par exemple, `http://localhost:2020/s/documents/generate`). Sur Twenty Cloud, `RestApiClient`
résout le chemin via le domaine des fonctions de l'espace de travail.
## 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>

View File

@@ -0,0 +1,70 @@
---
title: "Tutoriel : Document Generator"
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 n’importe 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 l’interface 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) | Publiez-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 d’exé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é ? L’application 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 l’application s’assemble
<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, l’agent IA,
l’étape de workflow, le lien partageable — n’est qu’une façon différente de déclencher ce
même générateur.
## Gardez cette boucle en marche
Laissez `yarn twenty dev` s’exé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 l’interface 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>

View File

@@ -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
. Placez un logo dans `public/` et référencez-le avec `logo`.
```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.',
logo: '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 PNG dans
`public/gallery/` et référencez-les avec `galleryImages` — ils sont affichés comme une
galerie sur la page de l’annonce, dans l’ordre du tableau.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'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 plan # preview the metadata diff
```
Le plan affiche exactement ce qui changerait sur le serveur sans les appliquer —
un bon dernier contrôle de cohérence. 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>