> ## Documentation Index
> Fetch the complete documentation index at: https://twenty-claude-cool-pascal-5ay683.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Funzioni logiche

> Definisci funzioni TypeScript lato server con trigger HTTP, cron e trigger di eventi del database.

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.

<AccordionGroup>
  <Accordion title="defineLogicFunction" description="Definisci funzioni logiche e i relativi trigger">
    Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazione con un handler e trigger opzionali.

    ```ts src/logic-functions/createPostCard.logic-function.ts theme={null}
    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 e metodo HTTP. Nel codice dell'app, anteponi al percorso della route `/s/` quando utilizzi `RestApiClient`; l'URL distribuito utilizza la base `TWENTY_FUNCTIONS_URL` iniettata (o `\<server-url>/s` quando non è impostata).

    <Note>
      Per richiamare, da un componente front-end (headless), una funzione logica attivata da una rotta, vedi [Chiamare una funzione logica](/l/it/developers/extend/apps/layout/front-components#calling-a-logic-function).
    </Note>

    * **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 una `Response` sincrona, oppure il workspace di destinazione e la funzione logica di destinazione da mettere in coda; nel percorso di messa in coda la piattaforma conferma con `202` ed esegue tale **destinazione** nella coda dei worker. Vedi [Trigger route del server](#server-route-trigger).

    <Note>
      Puoi anche eseguire manualmente una funzione utilizzando la CLI:

      ```bash filename="Terminal" theme={null}
      yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
      ```

      ```bash filename="Terminal" theme={null}
      yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
      ```

      Puoi osservare i log con:

      ```bash filename="Terminal" theme={null}
      yarn twenty dev:function:logs
      ```
    </Note>

    #### 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](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
    Importa il tipo `RoutePayload` da `twenty-sdk/logic-function`:

    ```ts theme={null}
    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à                    | Tipo                                   | Descrizione                                                                                                                                                                                                                   | Esempio                                                                    |
    | ---------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
    | `headers`                    | `Record\<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`)                                                                                                                                                         | vedi la sezione sotto                                                      |
    | `queryStringParameters`      | `Record\<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' }` |
    | `pathParameters`             | `Record\<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route                                                                                                                                                                        | `/users/:id`, `/users/123` -> `{ id: '123' }`                              |
    | `body`                       | `object \| null`                       | Corpo della richiesta analizzato (JSON)                                                                                                                                                                                       | `{ id: 1 }` -> `{ id: 1 }`                                                 |
    | `rawBody`                    | `string \| undefined`                  | Corpo 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. |                                                                            |
    | `isBase64Encoded`            | `boolean`                              | Indica se il corpo è codificato in base64                                                                                                                                                                                     |                                                                            |
    | `requestContext.http.method` | `string`                               | Metodo HTTP (GET, POST, PUT, PATCH, DELETE)                                                                                                                                                                                   |                                                                            |
    | `requestContext.http.path`   | `string`                               | Percorso 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`:

    ```ts theme={null}
    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:

    ```ts theme={null}
    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>
      I nomi delle intestazioni vengono normalizzati in minuscolo. Accedile usando chiavi in minuscolo (ad es., `event.headers['content-type']`).
    </Note>

    #### 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`:

    ```ts theme={null}
    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`

    <Note>
      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.
    </Note>

    #### Risposte di errore della piattaforma

    Oltre alle risposte specifiche del tuo handler, la piattaforma risponde direttamente alle chiamate di route in alcune situazioni: `404` quando la route o la funzione non esiste, `403` quando l'applicazione è arrestata, `429` quando viene raggiunto il rate limit di esecuzione e `422` quando le `dependencies` dell'applicazione in produzione sono troppo grandi per essere installate — vedi [limiti di dimensione delle dipendenze](/l/it/developers/extend/apps/getting-started/project-structure#dependencies).

    #### 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). Analizza la richiesta in ingresso e restituisce:

       * `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la piattaforma mette in coda tale destinazione nel workspace risolto e conferma con `202 { queued: true }`, oppure
       * una `Response` da `twenty-sdk/logic-function` — la piattaforma restituisce tale risposta HTTP **sincronicamente** e **non** mette in coda alcuna destinazione (usa questo per challenge handshake come Slack `url_verification`).

       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 **non** viene osservato dal chiamante HTTP quando il resolver ha scelto il percorso di messa in coda.

    ```ts src/logic-functions/resolve-server-route.logic-function.ts theme={null}
    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 theme={null}
    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.

    **Risposta a una verifica GET.** Alcuni provider verificano un endpoint prima di inviarvi dati, inviando un `GET` contenente una verifica allo stesso URL a cui in seguito invieranno eventi tramite `POST` — WhatsApp Cloud API di Meta è uno di questi. Una route del server risponde solo a `POST`, a meno che non venga specificato diversamente, quindi dichiara entrambi i metodi:

    ```ts theme={null}
    serverRouteTriggerSettings: {
      forwardedRequestHeaders: ['x-hub-signature-256'],
      httpMethods: ['GET', 'POST'],
    }
    ```

    La verifica arriva in `event.queryStringParameters` e la restituzione di un `Response` la rimanda al provider nella stessa richiesta. Un corpo stringa viene inviato come `text/plain`, che è ciò che questi provider si aspettano:

    ```ts theme={null}
    const handler = async (event: RoutePayload) => {
      if (event.requestContext.http.method === 'GET') {
        return new Response(event.queryStringParameters['hub.challenge']);
      }
      // …handle the delivered event
    };
    ```

    `httpMethods` sostituisce l'impostazione predefinita anziché aggiungersi a essa, quindi `['GET']` da solo fa sì che la route rifiuti `POST`. Sono supportati solo `GET` e `POST`. Lascialo non impostato a meno che il provider non richieda il secondo verbo: una route che dichiara `GET` avrà il proprio resolver eseguito da qualsiasi chiamante non autenticato, inclusi i crawler e gli strumenti di anteprima dei link che inviano `GET` senza richiesta. A tutto ciò per cui la piattaforma non dispone di un metodo viene restituito `405` senza che il resolver venga mai eseguito.

    <Note>
      **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.
    </Note>

    **Contratto del resolver.** Il tipo `LogicFunctionConfig` dell'SDK impone questo a tempo di compilazione: non appena imposti `serverRouteTriggerSettings`, il tuo handler è vincolato a restituire una `Response`, oppure `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o una `Promise` di uno dei due). Nel percorso di dispatch, il `workspaceId` deve essere un workspace in cui la funzione di destinazione è installata, altrimenti la richiesta viene rifiutata con `404`. Un risultato che non corrisponde a nessuna delle due forme — incluso uno i cui identificatori non sono UUID — viene rifiutato con `502`.

    | Campo                                    | Tipo                 | Note                                                                         |
    | ---------------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
    | `workspaceId`                            | `string`             | UUID del workspace in cui verrà eseguita la destinazione.                    |
    | `targetLogicFunctionUniversalIdentifier` | `string`             | `universalIdentifier` della funzione logica da invocare in quel workspace.   |
    | `payload`                                | `object` (opzionale) | Se impostato, sostituisce il body della richiesta inviato alla destinazione. |

    <Warning>
      **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.
    </Warning>

    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:

    | Provider                     | Header da inoltrare                                    | Stringa firmata              | Digest                                                     |
    | ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------------------------------- |
    | Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (il segreto è in base64 dopo aver rimosso `whsec_`) |
    | Stripe                       | `stripe-signature`                                     | `{timestamp}.{rawBody}`      | esadecimale                                                |
    | GitHub                       | `x-hub-signature-256`                                  | `{rawBody}`                  | esadecimale (prefissato con `sha256=`)                     |
    | Shopify                      | `x-shopify-hmac-sha256`                                | `{rawBody}`                  | base64                                                     |
    | Slack                        | `x-slack-signature`, `x-slack-request-timestamp`       | `v0:{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.

    <Note>
      Quando il resolver restituisce un oggetto di dispatch, la route risponde con `202 { queued: true }` e la funzione di destinazione viene eseguita nella coda dei worker — il chiamante non osserva mai la latenza, il risultato o gli errori della funzione di destinazione (che vengono registrati nei log di esecuzione). Questo impedisce che le nuove consegne da parte del mittente amplifichino i rallentamenti nell'elaborazione, che è esattamente ciò che si desidera per l'acquisizione dei webhook.

      Quando il chiamante deve leggere il corpo della risposta sulla stessa richiesta (challenge handshake, acknowledgement interattivi), restituisci invece una `Response` dal **resolver**. La piattaforma lo riecheggia in modo sincrono e salta la coda; i suoi header passano attraverso la stessa allow-list delle risposte delle route HTTP. Mantieni il resolver veloce — 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.
    </Note>

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

    ```ts theme={null}
    import type {
      DatabaseEventPayload,
      ObjectRecordCreateEvent,
      ObjectRecordDestroyEvent,
      ObjectRecordUpdateEvent,
    } from 'twenty-sdk/logic-function';

    type Person = {
      id: string;
      emails?: { primaryEmail?: string };
    };
    ```

    Il payload include:

    | Proprietà                                        | Descrizione                                                                                              |
    | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
    | `name`                                           | Nome dell'evento, ad esempio `person.updated`.                                                           |
    | `workspaceId`                                    | Area di lavoro in cui si è verificato l'evento.                                                          |
    | `objectMetadata`                                 | Metadati per l'oggetto che è cambiato.                                                                   |
    | `recordId`                                       | ID del record modificato.                                                                                |
    | `userId`, `userWorkspaceId`, `workspaceMemberId` | Campi dell'attore quando l'evento è stato causato da un utente dell'area di lavoro.                      |
    | `properties`                                     | Dati del record per l'evento, con `before`, `after`, `diff` e `updatedFields` a seconda dell'operazione. |

    | Evento             | Dati del record                                                                                                |
    | ------------------ | -------------------------------------------------------------------------------------------------------------- |
    | `person.created`   | `event.properties.after`                                                                                       |
    | `person.updated`   | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
    | `person.destroyed` | `event.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`.

    <Note>
      `databaseEventTriggerSettings.updatedFields` filtra quali eventi di aggiornamento attivano la funzione.
      `event.properties.updatedFields` indica quali campi sono effettivamente cambiati nell'evento corrente.
    </Note>

    Esempio di evento "created":

    ```ts theme={null}
    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":

    ```ts theme={null}
    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:

    ```ts theme={null}
    export default defineLogicFunction({
      ...,
      databaseEventTriggerSettings: {
        eventName: 'person.updated',
        updatedFields: ['emails'],
      },
    });
    ```

    Esempio di evento "destroyed":

    ```ts theme={null}
    type PersonDestroyedEvent = DatabaseEventPayload<
      ObjectRecordDestroyEvent<Person>
    >;

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

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

    #### Contesto di esecuzione

    Ogni gestore riceve un secondo argomento che descrive l'esecuzione stessa, indipendentemente da cosa l'abbia attivata. Mentre il primo argomento cambia struttura in base all'attivatore, questo no:

    ```ts theme={null}
    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 };
    };
    ```

    | Campo               | Tipo             | Descrizione                                                                                                |
    | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
    | `workspaceId`       | `string`         | Lo spazio di lavoro a cui appartiene l'esecuzione.                                                         |
    | `userWorkspaceId`   | `string \| null` | La persona che ha attivato l'esecuzione, in quanto membro di questo spazio di lavoro.                      |
    | `workspaceMemberId` | `string \| null` | La stessa persona come record del membro dello spazio di lavoro, a cui si collegano i record.              |
    | `retryCount`        | `number`         | `0` al primo tentativo. Vedi [Processi in background](/l/it/developers/extend/apps/logic/background-jobs). |
    | `maxRetries`        | `number`         | Limite massimo per i tentativi richiesti dall'applicazione.                                                |

    `userWorkspaceId` e `workspaceMemberId` sono `null` quando nessuno ha attivato l'esecuzione: le pianificazioni cron, gli hook di installazione e i webhook non autenticati non hanno una persona alle spalle. Sono inoltre `null` quando la persona non ha un record del membro dello spazio di lavoro o quando il suo è stato eliminato.

    <Note>
      **Il contesto indica chi ha attivato l'esecuzione.** Ciò che l'esecuzione può *fare* è una dichiarazione separata, riportata di seguito.
    </Note>

    #### Quale accesso utilizza una chiamata

    Ogni client — `CoreApiClient`, `MetadataApiClient` e `RestApiClient` — agisce come la persona che ha attivato l'esecuzione: il suo ruolo intersecato con quello della tua applicazione, quindi la chiamata non può mai fare più di quanto possa fare ciascuno dei due. Questo è il comportamento predefinito e significa che una persona non può mai usare la tua app per superare le proprie autorizzazioni.

    Quando nessuno ha attivato l'esecuzione, non c'è nessuna persona per conto della quale agire, quindi lo stesso client usa come fallback l'accesso della tua applicazione: le pianificazioni cron, gli hook di installazione e i webhook non autenticati non richiedono una gestione speciale.

    Alcune chiamate necessitano legittimamente dell'accesso della tua applicazione anche quando una persona *è* alle spalle dell'esecuzione — per leggere i record di configurazione della tua app o per eseguire un'attività che quella persona non potrebbe svolgere autonomamente. Crea un secondo client per questi casi:

    ```ts theme={null}
    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: { ... } });
    };
    ```

    Crea entrambi una sola volta, nell'ambito del modulo, e ogni punto di chiamata indica quindi quale accesso utilizza in base al client che chiama. `RestApiClient` accetta la stessa opzione:

    ```ts theme={null}
    import { RestApiClient } from 'twenty-client-sdk/rest';

    const asApplication = new RestApiClient({ runAs: 'application' });
    ```

    <Note>
      **Un'esecuzione che nessuno ha attivato agisce come la tua applicazione.** Le pianificazioni cron, gli hook di installazione e i webhook non autenticati non hanno una persona alle spalle, quindi il client predefinito usa come fallback l'accesso della tua applicazione e continua a funzionare. `runAs: 'application'` è necessario solo quando vuoi tale accesso in un'esecuzione che una persona *ha* attivato.

      Controlla `context.workspaceMemberId` quando una funzione si comporta diversamente a seconda che ci sia qualcuno alle sue spalle, ad esempio per attribuire un record.
    </Note>

    Gli helper dell'SDK che raggiungono le risorse della tua applicazione utilizzano sempre il suo accesso e ignorano `runAs`: l'[archivio chiave-valore](/l/it/developers/extend/apps/logic/key-value-store), le [connessioni](/l/it/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` e l'addebito dei crediti.

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

    <Note>
      **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](/l/it/user-guide/workflows/capabilities/workflow-actions#code) nella guida utente per la vista dell’utente finale.
    </Note>

    ```ts src/logic-functions/enrich-company.logic-function.ts theme={null}
    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:

    ```ts theme={null}
    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:

    ```ts theme={null}
    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:

    | Campo          | Scopo                                                                                                                                                                                          |
    | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `label`        | Nome mostrato per l’azione nel selettore di passaggi del generatore di workflow. Il valore predefinito è il `name` della funzione.                                                             |
    | `icon`         | Icona mostrata accanto all’azione (un nome `tabler-icons`, ad esempio `IconBuilding`).                                                                                                         |
    | `inputSchema`  | Il ricco `InputSchema` di Twenty: ciò che il generatore visualizza come campi configurabili (con selettori di variabili). Opzionale; quando omesso viene ricavato dall’handler.                |
    | `outputSchema` | Dichiara 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`:

    ```ts src/logic-functions/enrich-company.logic-function.ts theme={null}
    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.

    <Note>
      **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.
    </Note>
  </Accordion>
</AccordionGroup>

<Note>
  **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`:

  ```ts theme={null}
  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>
  **Hook di installazione** — i gestori di pre-installazione, post-installazione e disinstallazione — condividono questo runtime, ma sono dichiarati con le proprie funzioni di definizione e non accettano impostazioni dei trigger. Consulta [Hook di installazione](/l/it/developers/extend/apps/config/install-hooks) per `definePreInstallLogicFunction`, `definePostInstallLogicFunction` e `defineUninstallLogicFunction`.
</Note>

## Crea un'attività della cronologia di eventi.

Usa `createTimelineActivity()` per pubblicare un evento di dominio esplicito da una funzione logica. Definisci prima l'evento come [tipo di attività della cronologia](/l/it/developers/extend/apps/data/timeline-activity-types), quindi fai riferimento al tipo e agli oggetti tramite i relativi identificatori universali stabili:

```ts theme={null}
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 risolve gli identificatori universali negli ID dei metadati specifici dell'installazione, verifica che il tipo di attività della cronologia appartenga all'app chiamante e acquisisce un'istantanea dei relativi metadati di presentazione nella nuova attività.

Gli input obbligatori sono `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` e `targetRecordId`. Puoi anche fornire `happensAt`, `properties` e `workspaceMemberId`. `happensAt` controlla la posizione e l'ora visualizzata dell'evento nella cronologia; per impostazione predefinita corrisponde all'ora di creazione.

Per associare un altro record all'evento, fornisci insieme `linkedRecordId` e `linkedObjectMetadataUniversalIdentifier`. Puoi inoltre fornire `linkedRecordCachedName` come alternativa di visualizzazione storica:

```ts theme={null}
await createTimelineActivity({
  timelineActivityTypeUniversalIdentifier:
    'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
  targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  targetRecordId: postCardId,
  linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
  linkedRecordId: recipientId,
  linkedRecordCachedName: recipientName,
});
```

Il ruolo della funzione logica deve disporre dell'autorizzazione di scrittura sull'oggetto standard `timelineActivity`. Mantieni i tipi di evento espliciti non associati a un'`action`; un tipo associato a un'azione riceve già eventi di audit automatici e altrimenti produrrebbe righe duplicate.

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

| Client              | Importa                      | Endpoint                                                                 | Generato?                  |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------ | -------------------------- |
| `CoreApiClient`     | `twenty-client-sdk/core`     | `/graphql` — dati dello spazio di lavoro (record, oggetti)               | Sì, in fase di dev/build   |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurazione dello spazio di lavoro, caricamenti di file | No, fornito pronto all'uso |

<AccordionGroup>
  <Accordion title="CoreApiClient" description="Esegui query e modifica i dati dello spazio di lavoro (record, oggetti)">
    `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.

    ```ts theme={null}
    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.

    <Note>
      **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`.
    </Note>

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

    ```ts theme={null}
    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="Configurazione dello spazio di lavoro, applicazioni e caricamenti di file">
    `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. Accetta la stessa opzione `runAs` di `CoreApiClient` — vedi [Quale accesso utilizza una chiamata](#whose-access-a-call-uses).

    ```ts theme={null}
    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:

    ```ts theme={null}
    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://...' }
    ```

    | Parametro                          | Tipo     | Descrizione                                                            |
    | ---------------------------------- | -------- | ---------------------------------------------------------------------- |
    | `fileBuffer`                       | `Buffer` | Il contenuto grezzo del file                                           |
    | `filename`                         | `string` | Il nome del file (utilizzato per l'archiviazione e la visualizzazione) |
    | `contentType`                      | `string` | Tipo MIME (predefinito su `application/octet-stream` se omesso)        |
    | `fieldMetadataUniversalIdentifier` | `string` | L'`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.
  </Accordion>
</AccordionGroup>

<Note>
  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 per l'accesso predefinito: il ruolo di una persona intersecato con quello della tua applicazione quando qualcuno è alle spalle dell'esecuzione, il ruolo della tua applicazione quando non c'è nessuno. La persona è chiunque abbia attivato l'esecuzione in una funzione logica, oppure chiunque stia visualizzando la pagina in un componente front-end.
  * `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Chiave a breve durata con ambito esclusivamente al ruolo della tua applicazione. Solo funzioni logiche, sempre inserita lì, ed è ciò che utilizza `runAs: 'application'`.

  **Non** devi passare questi elementi ai client — vengono letti automaticamente da `process.env` e [Quale accesso utilizza una chiamata](#whose-access-a-call-uses) spiega come scegliere tra di essi. Le autorizzazioni della tua applicazione sono determinate dal ruolo dichiarato con `defineApplicationRole()` (o referenziato tramite `defaultRoleUniversalIdentifier` in `application-config.ts`); un'esecuzione che agisce come una persona non può mai superare né tale ruolo né il proprio.
</Note>
