Files
twenty/packages/twenty-docs/l/it/developers/extend/apps/building.mdx
T
107914b437 i18n - docs translations (#19119)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 14:39:38 +02:00

962 lines
45 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Creazione di app
description: Definisci oggetti, funzioni logiche, componenti front-end e molto altro con il Twenty SDK.
---
<Warning>
Le app sono attualmente in fase alfa. La funzionalità è funzionante ma ancora in evoluzione.
</Warning>
## Usa le risorse dell'SDK (tipi e configurazione)
Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare nella tua app. Di seguito gli elementi principali con cui interagirai più spesso.
### Funzioni helper
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](/l/it/developers/extend/apps/getting-started#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
| Funzione | Scopo |
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
| `defineApplication` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject` | Definisci oggetti personalizzati con campi |
| `defineField` | Estendi gli oggetti esistenti con campi aggiuntivi oppure definisci campi di relazione autonomi |
| `defineLogicFunction` | Definisci funzioni logiche con handler |
| `definePreInstallLogicFunction` | Definisci una funzione logica di pre-installazione (una per app) |
| `definePostInstallLogicFunction` | Definisci una funzione logica di post-installazione (una per app) |
| `defineFrontComponent` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineView` | Definisci viste salvate per gli oggetti |
| `defineNavigationMenuItem` | Definisci i link di navigazione della barra laterale |
| `defineSkill` | Definisci le competenze dell'agente IA |
| `defineAgent` | Definisci gli agenti IA |
| `definePageLayout` | Definisci layout di pagina personalizzati |
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
### Definizione degli oggetti
Gli oggetti personalizzati descrivono sia lo schema sia il comportamento per i record nel tuo spazio di lavoro. Usa `defineObject()` per definire oggetti con convalida integrata:
```typescript
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
Punti chiave:
* Usa `defineObject()` per una convalida integrata e un migliore supporto IDE.
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
* Puoi generare nuovi oggetti con `yarn twenty add`, che ti guida nella denominazione, nei campi e nelle relazioni.
<Note>
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard
come `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`.
Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati.
Puoi sovrascrivere i campi predefiniti definendo un campo con lo stesso nome nel tuo array `fields`,
ma non è consigliato.
</Note>
### Definire campi sugli oggetti esistenti
Usa `defineField()` per aggiungere campi a oggetti che non possiedi — come gli oggetti standard di Twenty (Person, Company, ecc.) o oggetti di altre app. A differenza dei campi inline in `defineObject()`, i campi autonomi richiedono un `objectUniversalIdentifier` per specificare quale oggetto estendono:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Punti chiave:
* `objectUniversalIdentifier` identifica l'oggetto di destinazione. Per gli oggetti standard, usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` esportati da `twenty-sdk`.
* Quando definisci campi inline in `defineObject()`, **non** hai bisogno di `objectUniversalIdentifier` — viene ereditato dall'oggetto padre.
* `defineField()` è l'unico modo per aggiungere campi a oggetti che non hai creato con `defineObject()`.
### Relazioni
Le relazioni collegano gli oggetti tra loro. In Twenty, le relazioni sono sempre **bidirezionali** — definisci entrambi i lati e ciascun lato fa riferimento all'altro.
Esistono due tipi di relazione:
| Tipo di relazione | Descrizione | Ha una chiave esterna? |
| ----------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Molti record di questo oggetto puntano a un record della destinazione | Sì (`joinColumnName`) |
| `ONE_TO_MANY` | Un record di questo oggetto ha molti record della destinazione | No (lato inverso) |
#### Come funzionano le relazioni
Ogni relazione richiede **due campi** che fanno riferimento l'uno all'altro:
1. Il lato **MANY_TO_ONE** — risiede sull'oggetto che detiene la chiave esterna
2. Il lato **ONE_TO_MANY** — risiede sull'oggetto che possiede la collezione
Entrambi i campi usano `FieldType.RELATION` e si riferiscono reciprocamente tramite `relationTargetFieldMetadataUniversalIdentifier`.
#### Esempio: Post Card ha molti destinatari
Supponiamo che un `PostCard` possa essere inviato a molti record `PostCardRecipient`. Ogni destinatario appartiene esattamente a una sola cartolina.
**Passaggio 1: definisci il lato ONE_TO_MANY su PostCard** (il lato "uno"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Passaggio 2: definisci il lato MANY_TO_ONE su PostCardRecipient** (il lato "molti" — contiene la chiave esterna):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Importazioni circolari:** Entrambi i campi di relazione fanno riferimento all'`universalIdentifier` dell'altro. Per evitare problemi di importazioni circolari, esporta gli ID dei campi come costanti denominate da ciascun file e importale nell'altro file. Il sistema di build le risolve in fase di compilazione.
</Note>
#### Relazioni con gli oggetti standard
Per creare una relazione con un oggetto Twenty integrato (Person, Company, ecc.), usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Proprietà dei campi di relazione
| Proprietà | Obbligatorio | Descrizione |
| ------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `tipo` | Sì | Deve essere `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` dell'oggetto di destinazione |
| `relationTargetFieldMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` del campo corrispondente sull'oggetto di destinazione |
| `universalSettings.relationType` | Sì | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Solo MANY_TO_ONE | Cosa accade quando il record referenziato viene eliminato: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
| `universalSettings.joinColumnName` | Solo MANY_TO_ONE | Nome della colonna del database per la chiave esterna (ad es., `postCardId`) |
#### Campi di relazione inline in defineObject
Puoi anche definire i campi di relazione direttamente all'interno di `defineObject()`. In tal caso, ometti `objectUniversalIdentifier` — viene ereditato dall'oggetto padre:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Configurazione dell'applicazione (application-config.ts)
Ogni app ha un singolo file `application-config.ts` che descrive:
* **Identità dell'app**: identificatori, nome visualizzato e descrizione.
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
* **(Opzionale) funzione di pre-installazione**: una funzione logica che viene eseguita prima che l'app venga installata.
* **(Opzionale) funzione post-installazione**: una funzione logica che viene eseguita dopo l'installazione dell'app.
Usa `defineApplication()` per definire la configurazione della tua applicazione:
```typescript
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
Note:
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
* Le funzioni di pre-installazione e post-installazione vengono rilevate automaticamente durante la build del manifesto. Vedi [Funzioni di pre-installazione](#pre-install-functions) e [Funzioni di post-installazione](#post-install-functions).
#### Metadati del marketplace
Se prevedi di [pubblicare la tua app](/l/it/developers/extend/apps/publishing), questi campi opzionali controllano come la tua app appare nel marketplace:
| Campo | Descrizione |
| ------------------ | ----------------------------------------------------------- |
| `autore` | Nome dell'autore o dell'azienda |
| `categoria` | Categoria dell'app per il filtraggio nel marketplace |
| `logoUrl` | Percorso del logo della tua app (relativo a `./assets/`) |
| `screenshots` | Array di percorsi degli screenshot (relativi a `./assets/`) |
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni" |
| `websiteUrl` | Link al tuo sito web |
| `termsUrl` | Link ai Termini di servizio |
| `emailSupport` | Indirizzo email di supporto |
| `issueReportUrl` | Link al sistema di tracciamento dei problemi |
#### Ruoli e permessi
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application-config.ts` indica il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
* La chiave API di runtime iniettata come `TWENTY_API_KEY` è derivata da questo ruolo funzione predefinito.
* Il client tipizzato sarà limitato ai permessi concessi a quel ruolo.
* Segui il principio del privilegio minimo: crea un ruolo dedicato con solo i permessi necessari alle tue funzioni, quindi fai riferimento al suo identificatore universale.
##### Ruolo funzione predefinito (*.role.ts)
Quando generi una nuova app con lo scaffolder, la CLI crea anche un file di ruolo predefinito. Usa `defineRole()` per definire ruoli con convalida integrata:
```typescript
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application-config.ts` come `defaultRoleUniversalIdentifier`. In altre parole:
* **\*.role.ts** definisce ciò che il ruolo funzione predefinito può fare.
* **application-config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
Note:
* Parti dal ruolo generato dallo scaffolder, quindi restringilo progressivamente seguendo il principio del privilegio minimo.
* Sostituisci `objectPermissions` e `fieldPermissions` con gli oggetti/campi di cui le tue funzioni hanno bisogno.
* `permissionFlags` controllano l'accesso alle funzionalità a livello di piattaforma. Mantienili al minimo; aggiungi solo ciò che ti serve.
* Vedi un esempio funzionante nell'app Hello World: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
### Configurazione e punto di ingresso della funzione logica
Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazione con un handler e trigger opzionali.
```typescript
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: '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,
triggers: [
// Public HTTP route trigger '/s/post-card/create'
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
// {
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
// type: 'cron',
// pattern: '0 0 1 1 *',
// },
// Database event trigger
// {
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
// type: 'databaseEvent',
// eventName: 'person.updated',
// updatedFields: ['name'],
// },
],
});
```
Tipi di trigger comuni:
* **route**: Espone la funzione su un percorso e metodo HTTP **sotto l'endpoint `/s/`**:
> es. `path: '/post-card/create',` -> chiamata su `<APP_URL>/s/post-card/create`
* **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.
> es. `person.updated`
Note:
* L'array `triggers` è facoltativo. Le funzioni senza trigger possono essere utilizzate come funzioni di utilità richiamate da altre funzioni.
* Puoi combinare più tipi di trigger in un'unica funzione.
### Funzioni di pre-installazione
Una funzione di pre-installazione è una funzione logica che viene eseguita automaticamente prima che la tua app venga installata in uno spazio di lavoro. È utile per attività di convalida, controlli dei prerequisiti o per preparare lo stato dello spazio di lavoro prima che proceda l'installazione principale.
Quando esegui lo scaffolding di una nuova app con `create-twenty-app`, viene generata una funzione di pre-installazione in `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Punti chiave:
* Le funzioni di pre-installazione utilizzano `definePreInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* È consentita una sola funzione di pre-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `preInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di preparazione più lunghe.
* Le funzioni di pre-installazione non necessitano di trigger — vengono invocate dalla piattaforma prima dell'installazione o manualmente tramite `exec --preInstall`.
### Funzioni post-installazione
Una funzione post-installazione è una funzione logica che viene eseguita automaticamente dopo che la tua app è stata installata in uno spazio di lavoro. Questo è utile per attività di configurazione una tantum come il popolamento di dati predefiniti, la creazione di record iniziali o la configurazione delle impostazioni dello spazio di lavoro.
Quando esegui lo scaffolding di una nuova app con `create-twenty-app`, viene generata automaticamente una funzione di post-installazione in `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --postInstall
```
Punti chiave:
* Le funzioni di post-installazione utilizzano `definePostInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `postInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* Le funzioni di post-installazione non necessitano di trigger — vengono invocate dalla piattaforma durante l'installazione o manualmente tramite `exec --postInstall`.
### Payload del trigger di route
<Warning>
**Modifica non retrocompatibile (v1.16, gennaio 2026):** Il formato del payload del trigger di route è cambiato. Prima della v1.16, i parametri di query, i parametri di percorso e il corpo venivano inviati direttamente come payload. A partire dalla v1.16, sono annidati all'interno di un oggetto `RoutePayload` strutturato.
**Prima della v1.16:**
```typescript
const handler = async (params) => {
const { param1, param2 } = params; // Direct access
};
```
**Dopo la v1.16:**
```typescript
const handler = async (event: RoutePayload) => {
const { param1, param2 } = event.body; // Access via .body
const { queryParam } = event.queryStringParameters;
const { id } = event.pathParameters;
};
```
**Per migrare le funzioni esistenti:** Aggiorna l'handler per estrarre i dati da `event.body`, `event.queryStringParameters` o `event.pathParameters` invece che direttamente dall'oggetto `params`.
</Warning>
Quando un trigger di route invoca la tua funzione logica, questa riceve un oggetto `RoutePayload` che segue il formato AWS HTTP API v2. Importa il tipo da `twenty-sdk`:
```typescript
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
const { headers, queryStringParameters, pathParameters, body } = event;
// HTTP method and path are available in requestContext
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Il tipo `RoutePayload` ha la seguente struttura:
| Proprietà | Tipo | Descrizione |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route (ad es., `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) |
| `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 |
### Inoltro delle intestazioni HTTP
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 esplicitamente nell'array `forwardedRequestHeaders`:
```typescript
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
],
});
```
Nel tuo handler, puoi quindi accedere a queste intestazioni:
```typescript
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 esempio, `event.headers['content-type']`).
</Note>
Puoi creare nuove funzioni in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere una nuova funzione logica. Questo genera un file iniziale con un handler e una configurazione.
* **Manuale**: Crea un nuovo file `*.logic-function.ts` e usa `defineLogicFunction()`, seguendo lo stesso schema.
### Contrassegnare una funzione logica come strumento
Le funzioni logiche possono essere esposte come **strumenti** per gli agenti di IA e i flussi di lavoro. Quando una funzione è contrassegnata come strumento, diventa individuabile dalle funzionalità di IA di Twenty e può essere selezionata come passaggio nelle automazioni dei flussi di lavoro.
Per contrassegnare una funzione logica come strumento, imposta `isTool: true` e fornisci un `toolInputSchema` che descriva i parametri di input attesi utilizzando [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
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,
isTool: true,
toolInputSchema: {
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'],
},
});
```
Punti chiave:
* **`isTool`** (`boolean`, predefinito: `false`): Quando impostato su `true`, la funzione viene registrata come strumento e diventa disponibile per gli agenti IA e le automazioni dei flussi di lavoro.
* **`toolInputSchema`** (`object`, opzionale): Un oggetto JSON Schema che descrive i parametri accettati dalla funzione. Gli agenti IA utilizzano questo schema per capire quali input si aspetta lo strumento e per convalidare le chiamate. Se omesso, lo schema assume il valore predefinito `{ type: 'object', properties: {} }` (nessun parametro).
* Le funzioni con `isTool: false` (o non impostato) **non** vengono esposte come strumenti. Possono comunque essere eseguite direttamente o chiamate da altre funzioni, ma non compariranno nell'individuazione degli strumenti.
* **Denominazione dello strumento**: Quando esposta come strumento, il nome della funzione viene normalizzato automaticamente in `logic_function_<name>` (in minuscolo, i caratteri non alfanumerici vengono sostituiti da trattini bassi). Ad esempio, `enrich-company` diventa `logic_function_enrich_company`.
* È possibile combinare `isTool` con i trigger — una funzione può essere sia uno strumento (invocabile dagli agenti IA) sia attivata da eventi (cron, eventi del database, routes) contemporaneamente.
<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>
### Componenti front-end
I componenti front-end ti consentono di creare componenti React personalizzati che vengono renderizzati all'interno dell'interfaccia di Twenty. Usa `defineFrontComponent()` per definire componenti con convalida integrata:
```typescript
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Punti chiave:
* I componenti front-end sono componenti React che eseguono il rendering in contesti isolati all'interno di Twenty.
* Il campo `component` fa riferimento al tuo componente React.
* I componenti vengono compilati e sincronizzati automaticamente durante `yarn twenty dev`.
Puoi creare nuovi componenti front-end in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Manuale**: Crea un nuovo file `.tsx` e usa `defineFrontComponent()`, seguendo lo stesso schema.
### Abilità
Le skill definiscono istruzioni e capacità riutilizzabili che gli agenti IA possono utilizzare all'interno del tuo spazio di lavoro. Usa `defineSkill()` per definire skill con convalida integrata:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Punti chiave:
* `name` è una stringa identificativa univoca per la skill (kebab-case consigliato).
* `label` è il nome di visualizzazione leggibile mostrato nell'UI.
* `content` contiene le istruzioni della skill — questo è il testo che l'agente IA utilizza.
* `icon` (opzionale) imposta l'icona visualizzata nell'UI.
* `description` (opzionale) fornisce contesto aggiuntivo sullo scopo della skill.
Puoi creare nuove skill in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere una nuova skill.
* **Manuale**: Crea un nuovo file e usa `defineSkill()`, seguendo lo stesso schema.
### 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 |
#### CoreApiClient
`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 build`, quindi è completamente tipizzato per corrispondere ai tuoi oggetti e campi.
```typescript
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: 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 provi a usarlo senza eseguire prima `yarn twenty dev` o `yarn twenty build`, genererà un errore. La generazione avviene automaticamente — la CLI esegue l'introspezione dello schema GraphQL del tuo spazio di lavoro, genera un client tipizzato usando `@genql/cli`, scrive le sorgenti generate in `node_modules/twenty-client-sdk/dist/core/generated/` e sostituisce gli stub in `node_modules/twenty-client-sdk/dist/core.mjs` e `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
#### Utilizzo di CoreSchema per le annotazioni di tipo
`CoreSchema` fornisce tipi TypeScript corrispondenti agli oggetti del tuo spazio di lavoro, utili per tipizzare lo stato dei componenti o i parametri delle funzioni:
```typescript
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
`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:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Credenziali di runtime
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_API_KEY` — 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 referenziato in `defaultRoleUniversalIdentifier` nel tuo `application-config.ts`.
#### Caricamento dei file
`MetadataApiClient` include un metodo `uploadFile` per allegare file ai campi di tipo file. Implementa la [specifica delle richieste GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
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 del file (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.
### Esempio Hello World
Esplora un esempio minimale end-to-end che dimostra oggetti, funzioni logiche, componenti front-end e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world).