Skip to main content
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.
Chaque fichier de fonction utilise defineLogicFunction() pour exporter une configuration avec un gestionnaire et des déclencheurs facultatifs.
src/logic-functions/createPostCard.logic-function.ts
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).
Pour appeler une fonction logique déclenchée par une route depuis un composant frontal (sans interface), consultez Appeler une fonction logique.
  • 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.
Vous pouvez également exécuter manuellement une fonction à l’aide de la CLI :
Vous pouvez consulter les journaux avec :

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. Importez le type RoutePayload depuis twenty-sdk/logic-function :
Le type RoutePayload a la structure suivante :

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 :
Dans votre gestionnaire, accédez aux en-têtes transférés comme ceci :
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']).

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 :
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
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.

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.

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.
src/logic-functions/resolve-server-route.logic-function.ts
src/logic-functions/handle-invoice-paid.logic-function.ts
Le point de terminaison est accessible à l’adresse :
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 :
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 :
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é.
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.
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.
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.
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 :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.
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.

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.
La charge utile inclut :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.
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.
Exemple d’événement de création :
Exemple d’événement de mise à jour :
Déclencher uniquement lors des mises à jour de l’adresse e-mail :
Exemple d’événement de destruction :

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 :
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é.
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.

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 :
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 :
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.
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, les connexions, 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.
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 dans le guide utilisateur pour la vue côté utilisateur final.
src/logic-functions/enrich-company.logic-function.ts
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 :
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 :
Un exemple complet d’action de workflow
workflowActionTriggerSettings accepte quatre champs :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 :
src/logic-functions/enrich-company.logic-function.ts
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.
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é.
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 :
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 pour definePreInstallLogicFunction, definePostInstallLogicFunction et defineUninstallLogicFunction.

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, puis référencez le type et les objets à l’aide de leurs identifiants universels stables :
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 :
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.
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.
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.
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.

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 :
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.

Téléverser des fichiers

Le MetadataApiClient inclut une méthode uploadFile pour joindre des fichiers aux champs de type fichier :
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é.
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 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.