--- title: App di Twenty description: Crea e gestisci le personalizzazioni di Twenty come codice. --- Le app sono attualmente in fase alfa. La funzionalità è funzionante ma ancora in evoluzione. ## Cosa sono le app? Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come codice**. Invece di configurare tutto tramite l'interfaccia utente, definisci in codice il modello dati e le funzioni serverless — rendendo più veloce creare, mantenere e distribuire su più spazi di lavoro. **Cosa puoi fare oggi:** * Definisci oggetti e campi personalizzati come codice (modello dati gestito) * Crea funzioni serverless con trigger personalizzati * Distribuisci la stessa app su più spazi di lavoro **In arrivo:** * Layout e componenti UI personalizzati ## Prerequisiti * Node.js 24+ e Yarn 4 * Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks) ## Per iniziare Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare: ```bash filename="Terminal" # Scaffold a new app npx create-twenty-app@latest my-twenty-app cd my-twenty-app # Authenticate using your API key (you'll be prompted) yarn auth:login # Start dev mode: automatically syncs local changes to your workspace yarn app:dev ``` Da qui puoi: ```bash filename="Terminal" # Add a new entity to your application (guided) yarn app:create-entity # Generate a typed Twenty client and workspace entity types yarn app:generate # Run a one‑time sync (instead of watch mode) yarn app:sync # Watch your application's functions logs yarn function:logs # Execute a function by name yarn function:execute -n my-function -p '{"name": "test"}' # Uninstall the application from the current workspace yarn app:uninstall # Display commands' help yarn app:help ``` Vedi anche: le pagine di riferimento della CLI per [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) e [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk). ## Struttura del progetto (generata dallo scaffolder) Quando esegui `npx create-twenty-app@latest my-twenty-app`, lo scaffolder: * Copia un'applicazione base minimale in `my-twenty-app/` * Aggiunge una dipendenza locale `twenty-sdk` e la configurazione di Yarn 4 * Crea file di configurazione e script collegati alla CLI `twenty` * Genera una configurazione applicativa predefinita e un ruolo funzione predefinito Un'app appena generata dallo scaffolder si presenta così: ```text filename="my-twenty-app/" my-twenty-app/ package.json yarn.lock .gitignore .nvmrc .yarnrc.yml .yarn/ releases/ yarn-4.9.2.cjs install-state.gz eslint.config.mjs tsconfig.json README.md src/ app/ application.config.ts # Required - main application configuration default-function.role.ts # Default role for serverless functions // your entities (*.object.ts, *.function.ts, *.role.ts) utils/ # Optional - handler implementations & utilities ``` ### Convenzioni invece della configurazione Le applicazioni usano un approccio basato sulle **convenzioni invece della configurazione** in cui le entità vengono rilevate in base al suffisso del file. Questo consente un'organizzazione flessibile all'interno della cartella `src/app/`: | Suffisso del file | Tipo di entità | | ----------------- | ------------------------------------- | | `*.object.ts` | Definizioni di oggetti personalizzati | | `*.function.ts` | Definizioni di funzioni serverless | | `*.role.ts` | Definizioni di ruoli | ### Organizzazioni di cartelle supportate Puoi organizzare le tue entità in uno qualsiasi di questi modelli: **Tradizionale (per tipo):** ```text src/app/ ├── application.config.ts ├── objects/ │ └── postCard.object.ts ├── functions/ │ └── createPostCard.function.ts └── roles/ └── admin.role.ts ``` **Per funzionalità:** ```text src/app/ ├── application.config.ts └── post-card/ ├── postCard.object.ts ├── createPostCard.function.ts └── postCardAdmin.role.ts ``` **Struttura piatta:** ```text src/app/ ├── application.config.ts ├── postCard.object.ts ├── createPostCard.function.ts └── admin.role.ts ``` A livello generale: * **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk`, oltre a script come `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall` e `auth` che delegano alla CLI locale `twenty`. * **.gitignore**: Ignora i file generati comuni come `node_modules`, `.yarn`, `generated/` (client tipizzato), `dist/`, `build/`, cartelle di coverage, file di log e file `.env*`. * **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloccano e configurano la toolchain Yarn 4 utilizzata dal progetto. * **.nvmrc**: Fissa la versione di Node.js prevista dal progetto. * **eslint.config.mjs** e **tsconfig.json**: Forniscono linting e configurazione TypeScript per i sorgenti TypeScript della tua app. * **README.md**: Un breve README nella radice dell'app con istruzioni di base. * **src/app/**: Il luogo principale in cui definisci la tua applicazione come codice: * `application.config.ts`: Configurazione globale della tua app (metadati e collegamenti di runtime). Vedi "Configurazione dell'applicazione" qui sotto. * `*.role.ts`: Definizioni di ruoli usate dalle tue funzioni serverless. Vedi "Ruolo funzione predefinito" qui sotto. * `*.object.ts`: Definizioni di oggetti personalizzati. * `*.function.ts`: Definizioni di funzioni serverless. * **src/utils/**: Cartella opzionale per le implementazioni degli handler e le utility. Comandi successivi aggiungeranno altri file e cartelle: * `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types). * `yarn app:create-entity` will add entity definition files under `src/app/` for your custom objects, functions, or roles. l ## Autenticazione The first time you run `yarn auth:login`, you'll be prompted for: * URL dell'API (predefinito a http://localhost:3000 o al profilo dello spazio di lavoro corrente) * Chiave API Le tue credenziali sono archiviate per utente in `~/.twenty/config.json`. Puoi mantenere più profili e passare dall'uno all'altro usando `--workspace `. Esempi: ```bash filename="Terminal" # Login interactively (recommended) yarn auth:login # Use a specific workspace profile yarn auth:login --workspace my-custom-workspace ``` ## 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 quattro funzioni helper con convalida integrata per definire le entità della tua app: | Funzione | Scopo | | ------------------ | ------------------------------------------------------- | | `defineApp()` | Configura i metadati dell'applicazione | | `defineObject()` | Definisci oggetti personalizzati con campi | | `defineFunction()` | Definisci funzioni serverless con handler | | `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti | Queste funzioni convalidano la configurazione a runtime e offrono un migliore completamento automatico nell'IDE e una maggiore 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/app/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. * You can scaffold new objects using `yarn app:create-entity`, which guides you through naming, fields, and relationships. **I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard come `name`, `createdAt`, `updatedAt`, `createdBy`, `position` e `deletedAt`. Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati. Puoi anche definire oggetti utilizzando i decorator di TypeScript. Questo approccio utilizza una sintassi basata sulle classi con i decorator `@Object`, `@Field` e `@Relation`: ```typescript import { type AddressField, Field, FieldType, type FullNameField, Object, OnDeleteAction, Relation, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk'; import { type Note } from '../../generated'; @Object({ universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', nameSingular: 'postCard', namePlural: 'postCards', labelSingular: 'Post card', labelPlural: 'Post cards', description: 'A post card object', icon: 'IconMail', }) export class PostCard { @Field({ universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', type: FieldType.TEXT, label: 'Content', description: "Postcard's content", icon: 'IconAbc', }) content: string; @Relation({ universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe', type: RelationType.ONE_TO_MANY, label: 'Notes', icon: 'IconComment', inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note, onDelete: OnDeleteAction.CASCADE, }) notes: Note[]; } ``` Nota: L'approccio con i decorator richiede `experimentalDecorators` nella configurazione di TypeScript. ### 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 chiave–valore esposte alle funzioni come variabili d'ambiente. Usa `defineApp()` per definire la configurazione della tua applicazione: ```typescript // src/app/application.config.ts import { defineApp } from 'twenty-sdk'; import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; export default defineApp({ 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, }, }, functionRoleUniversalIdentifier: DEFAULT_FUNCTION_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`). * `functionRoleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto). #### Ruoli e permessi Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `functionRoleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni serverless 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/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ universalIdentifier: DEFAULT_FUNCTION_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: [ { objectNameSingular: 'postCard', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, }, ], fieldPermissions: [ { objectNameSingular: 'postCard', fieldName: 'content', canReadFieldValue: false, canUpdateFieldValue: false, }, ], permissionFlags: [PermissionFlag.APPLICATIONS], }); ``` L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `functionRoleUniversalIdentifier`. 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 serverless Ogni file di funzione usa `defineFunction()` per esportare una configurazione con un handler e trigger opzionali. Usa il suffisso di file `*.function.ts` per il rilevamento automatico. ```typescript // src/app/createPostCard.function.ts import { defineFunction } from 'twenty-sdk'; import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk'; import Twenty, { type Person } from '../../generated'; const handler = async ( params: | RoutePayload | DatabaseEventPayload> | CronPayload, ) => { const client = new Twenty(); // generated typed client 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 defineFunction({ 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 `/s/post-card/create` * **cron**: Esegue la tua funzione secondo una pianificazione utilizzando un'espressione CRON. * **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. > e.g. `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. ### Route trigger payload **Breaking change (v1.16, January 2026):** The route trigger payload format has changed. Prior to v1.16, query parameters, path parameters, and body were sent directly as the payload. Starting with v1.16, they are nested inside a structured `RoutePayload` object. **Before v1.16:** ```typescript const handler = async (params) => { const { param1, param2 } = params; // Direct access }; ``` **After v1.16:** ```typescript const handler = async (event: RoutePayload) => { const { param1, param2 } = event.body; // Access via .body const { queryParam } = event.queryStringParameters; const { id } = event.pathParameters; }; ``` **To migrate existing functions:** Update your handler to destructure from `event.body`, `event.queryStringParameters`, or `event.pathParameters` instead of directly from the params object. When a route trigger invokes your function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`: ```typescript import { defineFunction, 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' }; }; ``` The `RoutePayload` type has the following structure: | Property | Tipo | Descrizione | | ---------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------- | | `headers` | `Record` | HTTP headers (only those listed in `forwardedRequestHeaders`) | | `queryStringParameters` | `Record` | Query string parameters (multiple values joined with commas) | | `pathParameters` | `Record` | Path parameters extracted from the route pattern (e.g., `/users/:id` → `{ id: '123' }`) | | `corpo` | `object \| null` | Parsed request body (JSON) | | `isBase64Encoded` | `booleano` | Whether the body is base64 encoded | | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | `requestContext.http.path` | `string` | Raw request path | ### Forwarding HTTP headers By default, HTTP headers from incoming requests are **not** passed to your serverless function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array: ```typescript export default defineFunction({ 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'], }, ], }); ``` In your handler, you can then access these headers: ```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 }; }; ``` Header names are normalized to lowercase. Access them using lowercase keys (for example, `event.headers['content-type']`). Puoi creare nuove funzioni in due modi: * **Scaffolded**: Run `yarn app:create-entity` and choose the option to add a new function. Questo genera un file iniziale con un handler e una configurazione. * **Manuale**: Crea un nuovo file `*.function.ts` e usa `defineFunction()`, seguendo lo stesso schema. ### Client tipizzato generato Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Usalo nelle tue funzioni: ```typescript import Twenty from './generated'; const client = new Twenty(); const { me } = await client.query({ me: { id: true, displayName: true } }); ``` The client is re-generated by `yarn app:generate`. Re-run after changing your objects and `yarn app:sync` or when onboarding to a new workspace. #### Credenziali di runtime nelle funzioni serverless Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le credenziali come variabili d'ambiente prima dell'esecuzione del tuo codice: * `TWENTY_API_URL`: URL di base dell'API Twenty a cui punta la tua app. * `TWENTY_API_KEY`: Chiave a breve durata con ambito al ruolo funzione predefinito della tua applicazione. Note: * Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione. * I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `functionRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni serverless della tua applicazione. * Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `functionRoleUniversalIdentifier` all'identificatore universale di quel ruolo. ### Esempio Hello World Esplora un esempio minimale end‑to‑end che dimostra oggetti, funzioni e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world): ## Configurazione manuale (senza lo scaffolder) Sebbene consigliamo di utilizzare `create-twenty-app` per la migliore esperienza iniziale, puoi anche configurare un progetto manualmente. Non installare la CLI globalmente. Invece, aggiungi `twenty-sdk` come dipendenza locale e collega gli script nel tuo package.json: ```bash filename="Terminal" yarn add -D twenty-sdk ``` Quindi aggiungi script come questi: ```json filename="package.json" { "scripts": { "auth": "twenty auth login", "generate": "twenty app generate", "dev": "twenty app dev", "sync": "twenty app sync", "uninstall": "twenty app uninstall", "logs": "twenty app logs", "create-entity": "twenty app add", "help": "twenty --help" } } ``` Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:sync`, etc. ## Risoluzione dei problemi * Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions. * Impossibile connettersi al server: verifica l'URL dell'API e che il server Twenty sia raggiungibile. * Types or client missing/outdated: run `yarn app:generate` and then `yarn app:dev`. * Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment. Canale di supporto su Discord: https://discord.com/channels/1130383047699738754/1130386664812982322