Vai al contenuto principale
Le funzioni logiche sono funzioni TypeScript lato server che vengono eseguite sulla piattaforma Twenty. Possono essere attivate da richieste HTTP, pianificazioni cron o eventi del database — e possono anche essere esposte come strumenti per agenti di IA.
Ogni file di funzione usa defineLogicFunction() per esportare una configurazione con un handler e trigger opzionali.
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 *',
  },*/
});
Tipi di trigger disponibili:
  • httpRoute: Espone la tua funzione su un percorso HTTP e un metodo al URL di base delle funzioni del tuo workspace — il valore Twenty inietta come TWENTY_FUNCTIONS_URL (su Twenty Cloud, un dominio dedicato per workspace
ad es. path: '/post-card/create' è invocabile su https://your-workspace.withtwenty.com/post-card/create
Il prefisso tradizionale /s/ (https://your-twenty-server.com/s/post-card/create) è deprecato su Twenty Cloud e sarà disattivato il 2026-07-24. Rimane disponibile per le istanze locali e self-hosted che non configurano un dominio di funzioni isolate — usa TWENTY_FUNCTIONS_URL quando è impostato, e torna a \<server-url>/s/\<path> altrimenti.
Per richiamare, da un componente front-end (headless), una funzione logica attivata da una rotta, vedi Chiamare una funzione logica.
  • cron: Esegue la tua funzione secondo una pianificazione utilizzando un’espressione CRON.
  • databaseEvent: Viene eseguito sugli eventi del ciclo di vita degli oggetti dello spazio di lavoro. Quando l’operazione dell’evento è updated, è possibile specificare campi specifici da monitorare nell’array updatedFields. Se lasciato non definito o vuoto, qualsiasi aggiornamento attiverà la funzione.
ad es. person.updated, *.created, company.*
  • serverRoute: espone una singola route HTTP con ambito di registrazione. Una funzione di resolver (dichiarata con serverRouteTriggerSettings) viene eseguita nel workspace proprietario e restituisce sia il workspace di destinazione SIA la funzione logica di destinazione a cui indirizzare la richiesta; la piattaforma quindi esegue tale funzione di destinazione e ne restituisce la risposta. Vedi Trigger route del server.
Puoi anche eseguire manualmente una funzione utilizzando la 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
Puoi osservare i log con:
yarn twenty dev:function:logs

Payload del trigger di route

Quando un trigger di tipo route invoca la tua funzione logica, questa riceve un oggetto RoutePayload che segue il formato AWS HTTP API v2. Importa il tipo RoutePayload da 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' };
};
Il tipo RoutePayload ha la seguente struttura:
ProprietàTipoDescrizioneEsempio
headersRecord\<string, string | undefined>Intestazioni HTTP (solo quelle elencate in forwardedRequestHeaders)vedi la sezione sotto
queryStringParametersRecord\<string, string | undefined>Parametri della query string (valori multipli uniti da virgole)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord\<string, string | undefined>Parametri di percorso estratti dal pattern della route/users/:id, /users/123 -> { id: '123' }
bodyobject | nullCorpo della richiesta analizzato (JSON){ id: 1 } -> { id: 1 }
rawBodystring | undefinedCorpo della richiesta UTF-8 originale, prima dell’analisi JSON. Utile per verificare le firme dei webhook in stile HMAC (ad es. X-Hub-Signature-256 di GitHub, Stripe). undefined quando il runtime non lo ha conservato.
isBase64EncodedbooleanIndica se il corpo è codificato in base64
requestContext.http.methodstringMetodo HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringPercorso della richiesta non elaborato

forwardedRequestHeaders

Per impostazione predefinita, le intestazioni HTTP delle richieste in ingresso non vengono passate alla tua funzione logica per motivi di sicurezza. Per accedere a intestazioni specifiche, elencale nell’array 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'],
  },
});
Nel tuo handler, accedi alle intestazioni inoltrate in questo modo:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
I nomi delle intestazioni vengono normalizzati in minuscolo. Accedile usando chiavi in minuscolo (ad es., event.headers['content-type']).

Risposta HTTP personalizzata

Per impostazione predefinita, restituire un valore semplice dal tuo handler lo invia come risposta 200 (JSON per gli oggetti, text/plain per le stringhe). Per controllare il codice di stato e le intestazioni della risposta, restituisci un oggetto Response da 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' },
  });
};
Per motivi di sicurezza, le intestazioni di risposta sono limitate a un elenco consentito. Qualsiasi intestazione che non è presente nell’elenco (ad esempio Set-Cookie, intestazioni CORS come Access-Control-Allow-Origin o intestazioni personalizzate X-*) viene ignorata senza segnalazione prima che la risposta venga inviata. Le intestazioni di risposta consentite sono:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
Il codice di stato deve essere un codice di stato HTTP valido (compreso tra 100 e 599). I nomi delle intestazioni di risposta vengono confrontati senza distinzione tra maiuscole e minuscole.

Trigger route del server

httpRouteTriggerSettings espone una funzione sotto /s/ e risolve lo spazio di lavoro dall’host della richiesta — il che funziona quando ogni spazio di lavoro ha il proprio dominio. I provider di terze parti, tuttavia, inviano gli eventi di ogni tenant a un URL. Per questo caso, usa serverRouteTriggerSettings.Il trigger ha due parti:
  1. Una funzione logica di resolver — dichiarata con serverRouteTriggerSettings — viene eseguita nel tuo workspace proprietario (il workspace che possiede la registrazione dell’applicazione). Ispeziona la richiesta in ingresso e restituisce { workspaceId, targetLogicFunctionUniversalIdentifier, payload? }, scegliendo sia il workspace di destinazione che la funzione di destinazione. Il resolver è l’unico punto di autorizzazione: l’URL contiene solo l’identificatore del resolver. Questo è il punto preferenziale per verificare le firme delle richieste: il resolver viene eseguito prima di qualsiasi effetto collaterale, ha accesso al rawBody originale e agli header inoltrati, e può rifiutare senza toccare la destinazione.
  2. Una funzione logica di destinazione — una normale funzione logica per-workspace — viene quindi eseguita nel workspace risolto con il payload restituito dal resolver (o il payload originale della richiesta se il resolver non lo ha trasformato). Il suo valore di ritorno diventa la risposta HTTP.
src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import 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 {
    metadata?: { twentyWorkspaceId?: string };
    type?: string;
  };

  return {
    workspaceId: body.metadata?.twentyWorkspaceId ?? '',
    // 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'],
  },
});
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,
});
L’endpoint è raggiungibile all’indirizzo:
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
L’identificatore è il universalIdentifier del resolver dal tuo manifest. Registra quell’URL presso il provider.
L’applicazione deve essere rivendicata e installata nel workspace del proprietario. Poiché il resolver viene eseguito nel workspace del proprietario (il workspace che possiede la registrazione dell’applicazione), un server route trigger funziona solo quando l’applicazione è stata rivendicata, cioè ha un workspace del proprietario, e quell’applicazione è installata nel workspace del proprietario. Finché entrambe non sono vere, il resolver non ha dove essere eseguito, quindi la route non può essere gestita. Un’applicazione che espone una funzione logica serverRouteTriggerSettings quindi non può essere elencata nel marketplace finché non è stata rivendicata e installata nel workspace del proprietario.
Contratto del resolver. Il tipo LogicFunctionConfig dell’SDK impone questo a tempo di compilazione: non appena imposti serverRouteTriggerSettings, il tuo handler è vincolato a restituire { workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object } (o una Promise di questo valore). Il workspaceId deve essere un workspace in cui la funzione di destinazione è installata, altrimenti la richiesta viene rifiutata con 404.
CampoTipoNote
workspaceIdstringUUID del workspace in cui verrà eseguita la destinazione.
targetLogicFunctionUniversalIdentifierstringuniversalIdentifier della funzione logica da invocare in quel workspace.
payloadobject (opzionale)Se impostato, sostituisce il body della richiesta inviato alla destinazione.
La verifica della firma è una tua responsabilità — eseguila nel resolver. La piattaforma non verifica le firme delle richieste. Il resolver è il punto consigliato in cui farlo: viene eseguito per primo, con accesso a event.rawBody e agli header che hai elencato in forwardedRequestHeaders, e un errore lanciato (o qualsiasi workspaceId non corrispondente) interrompe l’inoltro prima che la destinazione venga invocata. Se invece spingi la verifica verso la destinazione, la destinazione deve fare attenzione a non perdere rawBody e gli header — cioè il resolver non deve restituire un payload. Verifica sempre prima di qualsiasi effetto collaterale e usa un confronto a tempo costante.
Per le firme delle richieste, la maggior parte dei provider firma con HMAC-SHA256; le parti che differiscono sono il nome dell’header, la codifica del digest e la stringa del payload firmato. Alcuni esempi:
ProviderHeader da inoltrareStringa firmataDigest
Svix (Recall, Resend, Clerk)webhook-id, webhook-timestamp, webhook-signature{id}.{timestamp}.{rawBody}base64 (il segreto è in base64 dopo aver rimosso whsec_)
Stripestripe-signature{timestamp}.{rawBody}esadecimale
GitHubx-hub-signature-256{rawBody}esadecimale (prefissato con sha256=)
Shopifyx-shopify-hmac-sha256{rawBody}base64
Slackx-slack-signature, x-slack-request-timestampv0:{timestamp}:{rawBody}esadecimale (prefissato con v0=)
L’esempio di resolver sopra mostra già il flusso HMAC-SHA256 di GitHub — adatta il nome dell’header, la codifica del digest e la stringa del payload firmato in base al provider con cui ti stai integrando.
La destinazione viene eseguita in modo sincrono e il valore restituito diventa la risposta HTTP, quindi i chiamanti vedono il tuo codice di stato e possono ritentare in caso di codice non 2xx. Mantieni entrambi gli handler veloci — alcuni provider (ad es. Slack) vanno in timeout in pochi secondi. Poiché il resolver è raggiungibile come endpoint pubblico, proteggilo con rate limiting al tuo edge.

Payload del trigger di evento del database

Quando un trigger di evento del database invoca la tua funzione logica, questa riceve un DatabaseEventPayload per ogni record modificato. Il payload combina i metadati sull’area di lavoro e sull’oggetto di origine con l’evento a livello di record.
import type {
  DatabaseEventPayload,
  ObjectRecordCreateEvent,
  ObjectRecordDestroyEvent,
  ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';

type Person = {
  id: string;
  emails?: { primaryEmail?: string };
};
Il payload include:
ProprietàDescrizione
nameNome dell’evento, ad esempio person.updated.
workspaceIdArea di lavoro in cui si è verificato l’evento.
objectMetadataMetadati per l’oggetto che è cambiato.
recordIdID del record modificato.
userId, userWorkspaceId, workspaceMemberIdCampi dell’attore quando l’evento è stato causato da un utente dell’area di lavoro.
propertiesDati del record per l’evento, con before, after, diff e updatedFields a seconda dell’operazione.
EventoDati del record
person.createdevent.properties.after
person.updatedevent.properties.before, event.properties.after, event.properties.diff, event.properties.updatedFields
person.destroyedevent.properties.before
Per le eliminazioni logiche, .deleted segue la struttura in stile aggiornamento perché il campo deletedAt del record cambia. Per le eliminazioni permanenti, usa .destroyed.
databaseEventTriggerSettings.updatedFields filtra quali eventi di aggiornamento attivano la funzione. event.properties.updatedFields indica quali campi sono effettivamente cambiati nell’evento corrente.
Esempio di evento “created”:
type PersonCreatedEvent = DatabaseEventPayload<
  ObjectRecordCreateEvent<Person>
>;

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

  return {
    personId: event.recordId,
    email: person.emails?.primaryEmail,
  };
};
Esempio di evento “updated”:
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,
  };
};
Attiva solo sugli aggiornamenti dell’email:
export default defineLogicFunction({
  ...,
  databaseEventTriggerSettings: {
    eventName: 'person.updated',
    updatedFields: ['emails'],
  },
});
Esempio di evento “destroyed”:
type PersonDestroyedEvent = DatabaseEventPayload<
  ObjectRecordDestroyEvent<Person>
>;

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

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

Esporre una funzione come strumento di IA o come azione del flusso di lavoro

Le funzioni logiche possono essere esposte su due superfici, ciascuna con il proprio trigger:
  • toolTriggerSettings — rende la funzione individuabile dalle funzionalità di IA di Twenty (chat, MCP, function calling). Usa lo standard JSON Schema, il formato che gli LLM comprendono nativamente.
  • workflowActionTriggerSettings — fa apparire la funzione come un passaggio nel builder visivo dei flussi di lavoro. Usa il ricco InputSchema di Twenty affinché il builder possa visualizzare correttamente editor di campi, selettori di variabili ed etichette.
Una funzione può optare per uno, l’altro o entrambi. Si affiancano a cronTriggerSettings, databaseEventTriggerSettings e httpRouteTriggerSettings — stesso schema, stessa struttura.
Relazione con l’azione Code del workflow. L’azione Code incorporata nel generatore di workflow è essa stessa una funzione logica — Twenty ne crea una per ogni passaggio Code e ne espone l’editor inline. workflowActionTriggerSettings è il modo in cui trasformi quel codice inline una tantum in un’azione riutilizzabile: definisci la funzione una volta nella tua app e diventa selezionabile in qualsiasi workflow, invece di essere copiata e incollata in ogni passaggio Code. Consulta l’azione Code nella guida utente per la vista dell’utente finale.
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: {},
});
Punti chiave:
  • Una funzione può combinare le superfici — dichiara sia toolTriggerSettings sia workflowActionTriggerSettings per esporla in chat E nel builder dei flussi di lavoro.
  • toolTriggerSettings.inputSchema e workflowActionTriggerSettings.inputSchema sono entrambi opzionali. Se omessi, il builder del manifest li deduce dal codice sorgente dell’handler (JSON Schema per lo strumento di IA, InputSchema di Twenty per l’azione del flusso di lavoro). Forniscine uno esplicitamente quando desideri una tipizzazione più ricca — ad esempio, con campi compatibili con FieldMetadataType come CURRENCY o RELATION per il builder dei flussi di lavoro, oppure con campi description che l’agente di IA può leggere:
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'],
    },
  },
});
Per dichiarare i parametri una sola volta e servire entrambi i contesti, definisci un unico JSON Schema (InputJsonSchema) e convertilo per l’azione del flusso di lavoro con jsonSchemaToInputSchema da twenty-sdk/logic-function. toolTriggerSettings.inputSchema accetta direttamente il JSON Schema, mentre workflowActionTriggerSettings.inputSchema si aspetta l’InputSchema di 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),
  },
});
Un esempio completo di azione di workflow
workflowActionTriggerSettings accetta quattro campi:
CampoScopo
labelNome mostrato per l’azione nel selettore di passaggi del generatore di workflow. Il valore predefinito è il name della funzione.
iconIcona mostrata accanto all’azione (un nome tabler-icons, ad esempio IconBuilding).
inputSchemaIl ricco InputSchema di Twenty: ciò che il generatore visualizza come campi configurabili (con selettori di variabili). Opzionale; quando omesso viene ricavato dall’handler.
outputSchemaDichiara la struttura restituita dall’handler, così i passaggi successivi possono mappare ai suoi campi di output. Opzionale; senza di esso, l’output è esposto come singolo valore opaco.
Mettendo tutto insieme: una funzione esposta come azione di workflow, con un output dichiarato così che i passaggi successivi possano fare riferimento a taskId:
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' },
        },
      },
    ],
  },
});
Una volta che l’app è installata, Enrich Company appare nel selettore di azioni del generatore di workflow. Il generatore visualizza companyName e domain come campi di input (ognuno in grado di prelevare valori dai passaggi precedenti), e i passaggi a valle possono fare riferimento ai risultati taskId ed enriched del passaggio.
Scrivi una buona description. Gli agenti IA fanno affidamento sul campo description della funzione per decidere quando usare lo strumento. Sii specifico su cosa fa lo strumento e quando dovrebbe essere invocato.
Helper di runtime. twenty-sdk/utils riesporta piccoli helper di runtime in modo che gli handler non importino mai direttamente da twenty-shared. Per esempio, isDefined(value) restituisce false sia per null che per undefined — usalo per restringere in modo sicuro gli input opzionali degli handler, che possono arrivare come null a runtime anche quando sono tipizzati come 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 di installazione — i gestori di pre-installazione e post-installazione — condividono questo runtime, ma sono dichiarati con le proprie funzioni di definizione e non accettano impostazioni dei trigger. Consulta Hook di installazione per definePreInstallLogicFunction e definePostInstallLogicFunction.

Client API tipizzati (twenty-client-sdk)

Il pacchetto twenty-client-sdk fornisce due client GraphQL tipizzati per interagire con l’API di Twenty dalle tue funzioni logiche e dai componenti front-end.
ClientImportaEndpointGenerato?
CoreApiClienttwenty-client-sdk/core/graphql — dati dello spazio di lavoro (record, oggetti)Sì, in fase di dev/build
MetadataApiClienttwenty-client-sdk/metadata/metadata — configurazione dello spazio di lavoro, caricamenti di fileNo, fornito pronto all’uso
CoreApiClient è il client principale per interrogare e modificare i dati dello spazio di lavoro. Viene generato dallo schema del tuo spazio di lavoro durante yarn twenty dev o yarn twenty dev:build, quindi è completamente tipizzato per corrispondere ai tuoi oggetti e campi.
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,
  },
});
Il client utilizza una sintassi a selection-set: passa true per includere un campo, usa __args per gli argomenti e annida oggetti per le relazioni. Ottieni completamento automatico e controllo dei tipi completi basati sullo schema del tuo spazio di lavoro.
CoreApiClient viene generato in fase di dev/build. Se lo usi senza eseguire prima yarn twenty dev o yarn twenty dev:build, genera un errore. La generazione avviene automaticamente — la CLI esegue l’introspezione dello schema GraphQL del tuo spazio di lavoro e genera un client tipizzato usando @genql/cli.

Utilizzo di CoreSchema per le annotazioni di tipo

CoreSchema fornisce tipi TypeScript corrispondenti agli oggetti del tuo spazio di lavoro — utile per tipizzare lo stato dei componenti o i parametri delle funzioni:
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 è fornito pronto all’uso con l’SDK (nessuna generazione richiesta). Interroga l’endpoint /metadata per la configurazione dello spazio di lavoro, le applicazioni e i caricamenti di file.
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 },
    },
  },
});

Caricamento dei file

MetadataApiClient include un metodo uploadFile per allegare file ai campi di tipo file:
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://...' }
ParametroTipoDescrizione
fileBufferBufferIl contenuto grezzo del file
filenamestringIl nome del file (utilizzato per l’archiviazione e la visualizzazione)
contentTypestringTipo MIME (predefinito su application/octet-stream se omesso)
fieldMetadataUniversalIdentifierstringL’universalIdentifier del campo di tipo file nel tuo oggetto
Punti chiave:
  • Usa l’universalIdentifier del campo (non il suo ID specifico dello spazio di lavoro), quindi il tuo codice di upload funziona in qualsiasi spazio di lavoro in cui la tua app è installata.
  • L’url restituito è un URL firmato che puoi usare per accedere al file caricato.
Quando il tuo codice viene eseguito su Twenty (funzioni logiche o componenti front-end), la piattaforma inietta le credenziali come variabili d’ambiente:
  • TWENTY_API_URL — URL di base dell’API di Twenty
  • TWENTY_APP_ACCESS_TOKEN — Chiave a breve durata con ambito al ruolo funzione predefinito della tua applicazione
Non è necessario passarle ai client — vengono lette automaticamente da process.env. I permessi della chiave API sono determinati dal ruolo dichiarato con defineApplicationRole() (o referenziato tramite defaultRoleUniversalIdentifier in application-config.ts).