Salt la conținutul principal
Funcțiile de logică sunt funcții TypeScript pe partea de server care rulează pe platforma Twenty. Acestea pot fi declanșate de solicitări HTTP, programări cron sau evenimente din baza de date — și pot fi, de asemenea, expuse ca instrumente pentru agenți AI.
Fiecare fișier de funcție folosește defineLogicFunction() pentru a exporta o configurație cu un handler și declanșatoare opționale.
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 *',
  },*/
});
Tipuri de declanșatoare disponibile:
  • httpRoute: Expune funcția pe o cale și metodă HTTP sub endpoint-ul /s/:
de ex. path: '/post-card/create' este apelabil la https://your-twenty-server.com/s/post-card/create
Pentru a apela o funcție logică declanșată de o rută dintr-o componentă front-end (headless), consultă Apelarea unei funcții logice.
  • cron: Rulează funcția pe un program folosind o expresie CRON.
  • databaseEvent: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este updated, câmpurile specifice de urmărit pot fi specificate în array-ul updatedFields. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
de ex. person.updated, *.created, company.*
  • serverWebhook: Primește webhook-uri de intrare de la un serviciu terț (Stripe, GitHub, Svix, …) la un singur endpoint la nivelul înregistrării și determină spațiul de lucru țintă din payload. Consultați declanșatorul de webhook de server.
Puteți, de asemenea, să executați manual o funcție folosind CLI:
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Puteți urmări jurnalele cu:
yarn twenty dev:function:logs

Payload-ul declanșatorului de rută

Când un declanșator de rută invocă funcția logică, aceasta primește un obiect RoutePayload care urmează AWS HTTP API v2 format. Importați tipul RoutePayload din twenty-sdk/logic-function:
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' };
};
Tipul RoutePayload are următoarea structură:
ProprietateTipDescriereExemplu
headersRecord\<string, string | undefined>Anteturi HTTP (doar cele listate în forwardedRequestHeaders)consultați secțiunea de mai jos
queryStringParametersRecord\<string, string | undefined>Parametri query string (valorile multiple unite cu virgule)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord\<string, string | undefined>Parametri de cale extrași din modelul rutei/users/:id, /users/123 -> { id: '123' }
bodyobject | nullCorpul cererii analizat (JSON){ id: 1 } -> { id: 1 }
rawBodystring | undefinedCorpul original al cererii în UTF-8, înainte de parsarea JSON. Util pentru verificarea semnăturilor de tip HMAC pentru webhook-uri (de exemplu, X-Hub-Signature-256 de la GitHub, Stripe). undefined atunci când mediul de execuție nu a păstrat-o.
isBase64EncodedbooleanIndică dacă corpul este codificat în base64
requestContext.http.methodstringMetoda HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringCalea brută a cererii

forwardedRequestHeaders

În mod implicit, anteturile HTTP din cererile de intrare nu sunt transmise funcției dvs. de logică din motive de securitate. Pentru a accesa anumite anteturi, listați-le explicit în array-ul forwardedRequestHeaders:
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'],
  },
});
În handler, accesați anteturile transmise mai departe astfel:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, event.headers['content-type']).

Răspuns HTTP personalizat

În mod implicit, returnarea unei valori simple din handler trimite înapoi un răspuns 200 (JSON pentru obiecte, text/plain pentru șiruri). Pentru a controla codul de stare și antetele răspunsului, returnează un Response din twenty-sdk/logic-function:
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' },
  });
};
Din motive de securitate, anteturile de răspuns sunt limitate la o listă de antete permise. Orice antet care nu se află pe listă (de exemplu, Set-Cookie, anteturi CORS precum Access-Control-Allow-Origin sau anteturi personalizate X-*) este eliminat în mod silențios înainte ca răspunsul să fie trimis. Anteturile de răspuns permise sunt:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
Codul de stare trebuie să fie un cod de stare HTTP valid (între 100 și 599). Numele anteturilor de răspuns sunt comparate fără a ține cont de majuscule și minuscule.

Declanșator webhook de server

httpRouteTriggerSettings expune o funcție sub /s/ și rezolvă spațiul de lucru din gazda cererii — ceea ce funcționează atunci când fiecare spațiu de lucru are propriul domeniu. Furnizorii terți, însă, livrează evenimentele fiecărui tenant către un singur URL de webhook. Pentru acest caz, folosiți serverWebhookTriggerSettings: funcția este accesibilă la un endpoint la nivelul înregistrării, iar spațiul de lucru este determinat din payload.
src/logic-functions/handle-provider-webhook.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { Response } from 'twenty-sdk/logic-function';

const handler = async (event: RoutePayload) => {
  // Verify the signature yourself before doing anything (see below).
  // Return a non-2xx Response to make the provider retry.
  return { received: true };
};

export default defineLogicFunction({
  universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
  name: 'handle-provider-webhook',
  handler,
  serverWebhookTriggerSettings: {
    workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
    forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
  },
});
Funcția este accesibilă la:
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
Ambii identificatori sunt universalIdentifier din manifestul dvs. — cel al înregistrării aplicației și al acestei funcții logice. Înregistrați acel URL la furnizor.Rezolvarea spațiului de lucru. Deoarece un singur endpoint deservește fiecare spațiu de lucru, integrarea dvs. trebuie să plaseze workspaceId țintă undeva în livrare, iar workspaceIdResolver.{ source, path } îi indică platformei de unde să îl citească:
CâmpValoriNotițe
sourcebody | query | headerbody citește JSON-ul deja parsificat. query este cel mai universal — de obicei controlați URL-ul de callback pe care îl înregistrați, așa că adăugați ?twentyWorkspaceId=….
pathdot-path, de ex. metadata.twentyWorkspaceIdRestricționat la segmente alfanumerice / _ / -; cheile prototype sunt respinse.
Valoarea rezolvată trebuie să fie un UUID de spațiu de lucru valid și aplicația dvs. trebuie să fie instalată în acel spațiu de lucru, altfel cererea este respinsă înainte ca funcția să ruleze.
Verificarea semnăturii este responsabilitatea dvs. Platforma nu verifică semnăturile webhook pentru acest declanșator — doar rezolvă spațiul de lucru și rulează funcția. Handlerul dvs. trebuie să verifice singur semnătura folosind event.rawBody și headerele pe care le-ați enumerat în forwardedRequestHeaders, comparând cu un secret stocat ca variabilă de server/aplicație. Verificați întotdeauna înainte de orice efect secundar și folosiți o comparație în timp constant.
Majoritatea furnizorilor semnează cu HMAC-SHA256; părțile care diferă sunt numele headerului, codificarea digestului și șirul de payload semnat. Câteva exemple:
FurnizorHeadere de redirecționatȘir semnatDigest
Svix (Recall, Resend, Clerk)webhook-id, webhook-timestamp, webhook-signature{id}.{timestamp}.{rawBody}base64 (secretul este în base64 după eliminarea prefixului whsec_)
Stripestripe-signature{timestamp}.{rawBody}hex
GitHubx-hub-signature-256{rawBody}hex (cu prefixul sha256=)
Shopifyx-shopify-hmac-sha256{rawBody}base64
Slackx-slack-signature, x-slack-request-timestampv0:{timestamp}:{rawBody}hex (cu prefixul v0=)
import { createHmac, timingSafeEqual } from 'crypto';

const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-hub-signature-256'] ?? '';
  const expected =
    'sha256=' +
    createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
      .update(event.rawBody ?? '')
      .digest('hex');

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);

  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response({ error: 'invalid signature' }, { status: 401 });
  }

  // ...handle the verified event
  return { received: true };
};
Funcția rulează sincron, iar valoarea returnată devine răspunsul HTTP, astfel încât furnizorii văd codul de stare și pot reîncerca pentru coduri non-2xx. Mențineți handler-ele rapide — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece funcția rulează înainte ca semnătura să fie verificată, protejați acest endpoint cu limitare de rată la edge.

Payload-ul declanșatorului de eveniment al bazei de date

Când un declanșator de eveniment al bazei de date apelează funcția dvs. logică, aceasta primește un DatabaseEventPayload pentru fiecare înregistrare modificată. Payload-ul combină metadatele despre spațiul de lucru și obiectul sursă cu evenimentul la nivel de înregistrare.
import type {
  DatabaseEventPayload,
  ObjectRecordCreateEvent,
  ObjectRecordDestroyEvent,
  ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';

type Person = {
  id: string;
  emails?: { primaryEmail?: string };
};
Payload-ul include:
ProprietateDescriere
nameNumele evenimentului, cum ar fi person.updated.
workspaceIdSpațiul de lucru în care a avut loc evenimentul.
objectMetadataMetadate pentru obiectul care s-a modificat.
recordIdID-ul înregistrării modificate.
userId, userWorkspaceId, workspaceMemberIdCâmpurile actorului atunci când evenimentul a fost cauzat de un utilizator al spațiului de lucru.
propertiesDatele înregistrării pentru eveniment, cu before, after, diff și updatedFields în funcție de operație.
EvenimentDatele înregistrării
person.createdevent.properties.after
person.updatedevent.properties.before, event.properties.after, event.properties.diff, event.properties.updatedFields
person.destroyedevent.properties.before
Pentru ștergeri logice (soft delete), .deleted urmează structura de tip update deoarece câmpul deletedAt al înregistrării se modifică. Pentru ștergeri permanente, folosiți .destroyed.
databaseEventTriggerSettings.updatedFields filtrează ce evenimente de actualizare declanșează funcția. event.properties.updatedFields vă indică ce câmpuri s-au modificat efectiv în evenimentul curent.
Exemplu de eveniment de creare:
type PersonCreatedEvent = DatabaseEventPayload<
  ObjectRecordCreateEvent<Person>
>;

const handler = async (event: PersonCreatedEvent) => {
  const person = event.properties.after;

  return {
    personId: event.recordId,
    email: person.emails?.primaryEmail,
  };
};
Exemplu de eveniment de actualizare:
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,
  };
};
Declanșare doar la actualizări ale e-mailului:
export default defineLogicFunction({
  ...,
  databaseEventTriggerSettings: {
    eventName: 'person.updated',
    updatedFields: ['emails'],
  },
});
Exemplu de eveniment de ștergere:
type PersonDestroyedEvent = DatabaseEventPayload<
  ObjectRecordDestroyEvent<Person>
>;

const handler = async (event: PersonDestroyedEvent) => {
  const personBeforeDestroy = event.properties.before;

  return {
    personId: event.recordId,
    email: personBeforeDestroy.emails?.primaryEmail,
  };
};

Expunerea unei funcții ca instrument AI sau ca acțiune în fluxul de lucru

Funcțiile logice pot fi expuse în două locuri, fiecare cu propriul declanșator:
  • toolTriggerSettings — face funcția descoperibilă de către funcționalitățile AI ale Twenty (chat, MCP, apelarea de funcții). Folosește JSON Schema standard, formatul pe care LLM-urile îl înțeleg nativ.
  • workflowActionTriggerSettings — determină ca funcția să apară ca un pas în constructorul vizual de fluxuri de lucru. Folosește InputSchema bogat al Twenty, astfel încât constructorul să poată afișa editori de câmp adecvați, selectoare de variabile și etichete.
O funcție poate opta pentru una, cealaltă sau ambele. Acestea stau alături de cronTriggerSettings, databaseEventTriggerSettings și httpRouteTriggerSettings — același tipar, aceeași formă.
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: {},
});
Puncte cheie:
  • O funcție poate combina suprafețele — declară atât toolTriggerSettings, cât și workflowActionTriggerSettings pentru a o expune atât în chat, cât și în constructorul de fluxuri de lucru.
  • toolTriggerSettings.inputSchema și workflowActionTriggerSettings.inputSchema sunt ambele opționale. Când sunt omise, generatorul de manifest le deduce din codul sursă al handlerului (JSON Schema pentru instrumentul AI, InputSchema al Twenty pentru acțiunea de flux de lucru). Furnizează unul în mod explicit atunci când dorești o tipizare mai bogată — de exemplu, cu câmpuri compatibile cu FieldMetadataType, precum CURRENCY sau RELATION pentru constructorul de fluxuri de lucru, sau cu câmpuri description pe care agentul AI le poate citi:
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'],
    },
  },
});
Pentru a declara parametrii o singură dată și a deservi ambele suprafețe, definește o singură schemă JSON (InputJsonSchema) și convertește-o pentru acțiunea din fluxul de lucru cu jsonSchemaToInputSchema din twenty-sdk/logic-function. toolTriggerSettings.inputSchema primește direct schema JSON, în timp ce workflowActionTriggerSettings.inputSchema necesită InputSchema al Twenty:
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),
  },
});
Scrieți o description bună. Agenții AI se bazează pe câmpul description al funcției pentru a decide când să folosească instrumentul. Fiți specifici cu privire la ceea ce face instrumentul și când ar trebui apelat.
Ajutoare la rulare (runtime helpers). twenty-sdk/utils re-exportă mici ajutoare la rulare, astfel încât handlerii să nu importe niciodată direct din twenty-shared. De exemplu, isDefined(value) returnează false atât pentru null, cât și pentru undefined — folosește-l pentru a restrânge în siguranță intrările opționale ale handlerilor, care pot ajunge drept null la rulare, chiar și atunci când sunt tipate T | undefined:
import { isDefined } from 'twenty-sdk/utils';

const handler = async (params: { parentMessageId?: string }) => {
  if (isDefined(params.parentMessageId)) {
    // params.parentMessageId is narrowed to string here
  }
};
Hook-uri de instalare — handleri pre-instalare și post-instalare — partajează acest runtime, dar sunt declarați cu propriile lor funcții define și nu folosesc setări de declanșare. Consultați Hook-uri de instalare pentru definePreInstallLogicFunction și definePostInstallLogicFunction.

Clienți API tipizați (twenty-client-sdk)

Pachetul twenty-client-sdk oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front.
ClientImportațiEndpointGenerat?
CoreApiClienttwenty-client-sdk/core/graphql — date ale spațiului de lucru (înregistrări, obiecte)Da, în timpul dev/build
MetadataApiClienttwenty-client-sdk/metadata/metadata — configurarea spațiului de lucru, încărcări de fișiereNu, este livrat preconstruit
CoreApiClient este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este generat din schema spațiului de lucru în timpul yarn twenty dev sau yarn twenty dev:build, astfel încât este complet tipizat pentru a corespunde obiectelor și câmpurilor dvs.
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,
  },
});
Clientul folosește o sintaxă de tip selection-set: transmiteți true pentru a include un câmp, folosiți __args pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare completă a tipurilor, pe baza schemei spațiului dvs. de lucru.
CoreApiClient este generat în timpul dev/build. Dacă îl utilizați fără a rula mai întâi yarn twenty dev sau yarn twenty dev:build, va arunca o eroare. Generarea are loc automat — CLI inspectează schema GraphQL a spațiului dvs. de lucru și generează un client tipizat folosind @genql/cli.

Folosirea CoreSchema pentru adnotări de tip

CoreSchema oferă tipuri TypeScript care corespund obiectelor din spațiul dvs. de lucru — utile pentru tiparea stării componentelor sau a parametrilor funcțiilor:
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);
MetadataApiClient este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul /metadata pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere.
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 },
    },
  },
});

Încărcarea fișierelor

MetadataApiClient include o metodă uploadFile pentru atașarea fișierelor la câmpuri de tip fișier:
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://...' }
ParametruTipDescriere
fileBufferBufferConținutul brut al fișierului
filenamestringNumele fișierului (folosit pentru stocare și afișare)
contentTypestringTipul MIME (implicit application/octet-stream dacă este omis)
fieldMetadataUniversalIdentifierstringuniversalIdentifier al câmpului de tip fișier de pe obiectul dvs.
Puncte cheie:
  • Folosește universalIdentifier al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată.
  • url returnat este un URL semnat pe care îl puteți folosi pentru a accesa fișierul încărcat.
Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu:
  • TWENTY_API_URL — URL-ul de bază al API-ului Twenty
  • TWENTY_APP_ACCESS_TOKEN — Cheie cu durată scurtă, limitată la rolul implicit de funcție al aplicației
Nu trebuie să le transmiteți clienților — aceștia citesc automat din process.env. Permisiunile cheii API sunt determinate de rolul declarat cu defineApplicationRole() (sau referențiat prin defaultRoleUniversalIdentifier în application-config.ts).