i18n - docs translations (#21789)

Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-06-18 15:21:04 +02:00
committed by GitHub
parent 22baf2c6c5
commit 2b3b2362db
704 changed files with 38268 additions and 9002 deletions
@@ -79,7 +79,7 @@ type EmailFieldProps = {
const EmailField = ({ value }: EmailFieldProps) => (
<TextInput value={value} disabled fullWidth />
);},{
);
```
#### Nessuna espansione singola delle variabili delle props negli elementi JSX
@@ -37,7 +37,7 @@ Entrambe sono disponibili in REST e GraphQL. GraphQL aggiunge upsert in batch e
Authorization: Bearer YOUR_API_KEY
```
Crea una chiave API in **Impostazioni → API & Webhooks → + Crea chiave**. Copiala immediatamente — viene mostrata una sola volta. Le chiavi possono essere limitate a un ruolo specifico in **Impostazioni → Ruoli → scheda Assegnazione** per limitare ciò a cui possono accedere.
Crea una chiave API in **Impostazioni → API & Webhooks → + Crea chiave**. Copiala immediatamente — viene mostrata una sola volta. Le chiavi possono essere limitate a un ruolo specifico in **Impostazioni → Membri → Ruoli → scheda Assegnazione** per limitare ciò a cui possono accedere.
<VimeoEmbed videoId="928786722" title="Creazione della chiave API" />
@@ -0,0 +1,64 @@
---
title: Configurazione dell'applicazione
description: Dichiara l'identità della tua app, il ruolo predefinito, le variabili e i metadati del marketplace con defineApplication.
icon: rocket
---
Ogni app deve avere esattamente una chiamata a `defineApplication`. Dichiara:
* **Identità** — identificatore universale, nome visualizzato, descrizione.
* **Autorizzazioni** — il ruolo sotto il quale vengono eseguite le sue funzioni logiche e i componenti front-end.
* **Variabili** *(opzionali)* — coppie chiavevalore esposte al tuo codice come variabili d'ambiente.
* **Hook di pre-installazione / post-installazione** *(opzionali)* — vedi [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
Note:
* I campi `universalIdentifier` sono ID deterministici che possiedi. Generali una volta e mantienili stabili tra una sincronizzazione e l'altra.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni e i componenti front-end. Nelle funzioni di logica (lato server), sono disponibili come `process.env.VARIABLE_NAME`. Nei componenti front-end, usa `getApplicationVariable('VARIABLE_NAME')` da `twenty-sdk/front-component`. Le variabili contrassegnate con `isSecret: true` vengono iniettate solo nelle funzioni di logica. I componenti front-end ricevono solo variabili non segrete.
* Il ruolo predefinito viene rilevato automaticamente dal file di ruolo contrassegnato con [`defineApplicationRole()`](/l/it/developers/extend/apps/config/roles): non è necessario farvi riferimento da `defineApplication()`.
* Le funzioni di pre-installazione e post-installazione vengono rilevate automaticamente durante il build del manifest — non è necessario farne riferimento in `defineApplication()`.
* Il passaggio esplicito di `defaultRoleUniversalIdentifier` è ancora supportato per garantire la compatibilità con le versioni precedenti, ma è deprecato a favore di `defineApplicationRole()`.
## Ruolo funzione predefinito
Il ruolo dichiarato con [`defineApplicationRole()`](/l/it/developers/extend/apps/config/roles) controlla a cosa possono accedere le funzioni di logica e i componenti di interfaccia dell'app:
* Il token di runtime iniettato come `TWENTY_APP_ACCESS_TOKEN` è derivato da questo ruolo.
* Il client API tipizzato è limitato alle autorizzazioni concesse a quel ruolo.
* Segui il principio del privilegio minimo: dichiara solo le autorizzazioni necessarie alle tue funzioni.
Quando esegui lo scaffolding di una nuova app, la CLI crea un file di ruolo iniziale in `src/roles/default-role.ts`. Per la documentazione completa, vedi [Ruoli e autorizzazioni](/l/it/developers/extend/apps/config/roles).
## Metadati del marketplace
Se prevedi di [pubblicare la tua app](/l/it/developers/extend/apps/operations/publishing), questi campi opzionali controllano come appare nel marketplace:
| Campo | Descrizione |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Nome dell'autore o dell'azienda |
| `category` | Categoria dell'app per il filtraggio nel marketplace |
| `logoUrl` | Percorso del logo della tua app (ad es., `public/logo.png`) |
| `screenshots` | Array di percorsi degli screenshot (ad es., `public/screenshot-1.png`) |
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni". Se omesso, il marketplace utilizza il `README.md` del pacchetto da npm |
| `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 |
@@ -0,0 +1,206 @@
---
title: Hook di installazione
description: Esegui logica prima o dopo l'installazione — popola i dati, esegui il backup dei record, convalida l'aggiornamento.
icon: wrench
---
Gli hook di installazione sono funzioni logiche speciali che vengono eseguite durante il ciclo di vita di installazione o aggiornamento. Condividono lo stesso runtime del gestore delle [logic functions](/l/it/developers/extend/apps/logic/logic-functions) normali e ricevono un `InstallPayload`, ma sono dichiarati con le proprie funzioni di definizione — `definePostInstallLogicFunction()` e `definePreInstallLogicFunction()` — e vivono al di fuori del normale modello di trigger (eventi HTTP, cron, database).
Ogni app può definire **al massimo una funzione di pre-installazione** e **al massimo una funzione di post-installazione**. La build del manifesto genererà un errore se ne viene rilevata più di una per ciascun tipo.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="Viene eseguita dopo che la migrazione dei metadati dello spazio di lavoro è stata applicata">
Una funzione di post-installazione viene eseguita automaticamente una volta che la tua app ha terminato l'installazione in uno spazio di lavoro. Il server la esegue **dopo** che i metadati dell'app sono stati sincronizzati e il client SDK è stato generato, così lo spazio di lavoro è completamente pronto per l'uso e il nuovo schema è attivo. I casi d'uso tipici includono il popolamento di dati predefiniti, la creazione di record iniziali, la configurazione delle impostazioni dello spazio di lavoro o il provisioning di risorse su servizi di terze parti.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
```
Punti chiave:
* Le funzioni di post-installazione utilizzano `definePostInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
* L'handler riceve un `InstallPayload` con `{ previousVersion?: string; newVersion: string }` — `newVersion` è la versione in fase di installazione e `previousVersion` è la versione installata in precedenza (oppure `undefined` in caso di nuova installazione). Usa questi valori per distinguere le nuove installazioni dagli aggiornamenti e per eseguire logiche di migrazione specifiche per versione.
* **Quando viene eseguito l'hook**: solo sulle nuove installazioni, per impostazione predefinita. Passa `shouldRunOnVersionUpgrade: true` se vuoi che venga eseguito anche quando l'app viene aggiornata da una versione precedente. Se omesso, il flag è `false` per impostazione predefinita e gli aggiornamenti saltano l'hook.
* **Modello di esecuzione — asincrono per impostazione predefinita, sincrono su richiesta**: il flag `shouldRunSynchronously` controlla *come* viene eseguito il post-install.
* `shouldRunSynchronously: false` *(default)* — l'hook viene **messo in coda nella coda dei messaggi** con `retryLimit: 3` ed eseguito in modo asincrono in un worker. La risposta di installazione viene restituita non appena il job è messo in coda, quindi un handler lento o in errore non blocca il chiamante. Il worker riproverà fino a tre volte. **Usalo per job di lunga durata** — popolamento di dataset di grandi dimensioni, chiamate a API di terze parti lente, provisioning di risorse esterne, qualsiasi cosa che possa superare una finestra di risposta HTTP ragionevole.
* `shouldRunSynchronously: true` — l'hook viene eseguito **inline durante il flusso di installazione** (stesso executor del pre-install). La richiesta di installazione rimane bloccata finché l'handler non termina e, se genera un'eccezione, il chiamante dell'installazione riceve un `POST_INSTALL_ERROR`. Nessun tentativo automatico. **Usalo per attività rapide che devono completarsi prima della risposta** — ad esempio, emettere un errore di validazione all'utente, oppure un setup rapido di cui il client avrà bisogno immediatamente dopo il ritorno della chiamata di installazione. Tieni presente che la migrazione dei metadati è già stata applicata quando viene eseguito il post-install, quindi un errore in modalità sincrona **non** annulla le modifiche allo schema — si limita a far emergere l'errore.
* Assicurati che il tuo handler sia idempotente. In modalità asincrona la coda può riprovare fino a tre volte; in entrambe le modalità l'hook può essere eseguito di nuovo durante gli aggiornamenti quando `shouldRunOnVersionUpgrade: true`.
* Le variabili d'ambiente `APPLICATION_ID`, `APP_ACCESS_TOKEN` e `API_URL` sono disponibili all'interno dell'handler (come in qualsiasi altra funzione logica), quindi puoi chiamare le API di Twenty con un token di accesso applicativo con ambito sulla tua app.
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* I campi `universalIdentifier`, `shouldRunOnVersionUpgrade` e `shouldRunSynchronously` della funzione vengono associati automaticamente al manifest dell'applicazione nel campo `postInstallLogicFunction` durante la build — non è necessario referenziarli in [`defineApplication()`](/l/it/developers/extend/apps/config/application).
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* **Non eseguito in modalità dev**: quando un'app è registrata in locale (tramite `yarn twenty dev`), il server salta completamente il flusso di installazione e sincronizza i file direttamente tramite il watcher della CLI — quindi il post-install non viene mai eseguito in modalità dev, indipendentemente da `shouldRunSynchronously`. Usa `yarn twenty dev:function:exec --postInstall` per attivarlo manualmente su un workspace in esecuzione.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Viene eseguita prima che la migrazione dei metadati dello spazio di lavoro sia applicata">
Una funzione di pre-installazione viene eseguita automaticamente durante l'installazione, **prima che venga applicata la migrazione dei metadati dello spazio di lavoro**. Condivide la stessa struttura di payload del post-install (`InstallPayload`), ma è posizionata prima nel flusso di installazione così da poter preparare lo stato da cui dipenderà la migrazione imminente — usi tipici includono il backup dei dati, la validazione della compatibilità con il nuovo schema o l'archiviazione di record che stanno per essere ristrutturati o eliminati.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty dev:function:exec --preInstall
```
Punti chiave:
* Le funzioni di pre-install usano `definePreInstallLogicFunction()` — stessa configurazione specialistica del post-install, solo agganciata a uno slot di ciclo di vita diverso.
* Sia gli handler di pre- sia quelli di post-install ricevono lo stesso tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importalo una volta e riutilizzalo per entrambi gli hook.
* **Quando viene eseguito l'hook**: posizionato appena prima della migrazione dei metadati del workspace (`synchronizeFromManifest`). Prima dell'esecuzione, il server esegue una "sincronizzazione ridotta" puramente additiva che registra nei metadati del workspace la funzione di pre-install della versione **nuova** — nient'altro viene toccato — e poi la esegue. Poiché questa sincronizzazione è solo additiva, gli oggetti, i campi e i dati della versione precedente restano intatti quando il tuo handler viene eseguito: puoi leggere ed eseguire in sicurezza il backup dello stato pre-migrazione.
* **Modello di esecuzione**: il pre-install è eseguito **in modo sincrono** e **blocca l'installazione**. Se l'handler genera un'eccezione, l'installazione viene interrotta prima che vengano applicate modifiche allo schema — il workspace rimane sulla versione precedente in uno stato coerente. Questo è intenzionale: il pre-install è la tua ultima possibilità per rifiutare un aggiornamento rischioso.
* Come per il post-install, è consentita una sola funzione di pre-installazione per applicazione. Viene collegata automaticamente al manifest dell'applicazione nel campo `preInstallLogicFunction` durante la build.
* **Non eseguito in modalità dev**: come per il post-install — il flusso di installazione viene completamente saltato per le app registrate localmente, quindi il pre-install non viene mai eseguito con `yarn twenty dev`. Usa `yarn twenty dev:function:exec --preInstall` per attivarlo manualmente.
</Accordion>
<Accordion title="Pre-install vs post-install: quando usare l'uno o l'altro" description="Scegliere l'hook di installazione giusto">
Entrambi gli hook fanno parte dello stesso flusso di installazione e ricevono lo stesso `InstallPayload`. La differenza è **quando** vengono eseguiti rispetto alla migrazione dei metadati del workspace, e questo modifica quali dati possono gestire in sicurezza.
Il pre-install è sempre **sincrono** (blocca l'installazione e può interromperla). Il post-install è **asincrono per impostazione predefinita** — messo in coda su un worker con retry automatici — ma può optare per l'esecuzione sincrona con `shouldRunSynchronously: true`. Vedi l'accordion `definePostInstallLogicFunction` sopra per quando usare ciascuna modalità.
**Usa `post-install` per tutto ciò che richiede l'esistenza del nuovo schema.** Questo è il caso più comune:
* Popolamento di dati predefiniti (creazione di record iniziali, viste predefinite, contenuti demo) su oggetti e campi appena aggiunti.
* Registrazione di webhook con servizi di terze parti ora che l'app ha le proprie credenziali.
* Chiamare la tua API per completare il setup che dipende dai metadati sincronizzati.
* Logica idempotente di "ensure this exists" che dovrebbe riconciliare lo stato a ogni aggiornamento — da combinare con `shouldRunOnVersionUpgrade: true`.
Esempio — eseguire il seeding di un record `PostCard` predefinito dopo l'installazione:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Usa `pre-install` quando una migrazione altrimenti distruggerebbe o corromperebbe i dati esistenti.** Poiché il pre-install viene eseguito contro lo schema *precedente* e un suo fallimento annulla l'aggiornamento, è il posto giusto per qualsiasi operazione rischiosa:
* **Eseguire il backup dei dati che stanno per essere eliminati o ristrutturati** — ad esempio, stai rimuovendo un campo nella v2 e devi copiarne i valori in un altro campo o esportarli su uno storage prima che venga eseguita la migrazione.
* **Archiviare i record che un nuovo vincolo renderebbe non validi** — ad esempio, un campo sta diventando `NOT NULL` e devi prima eliminare o correggere le righe con valori nulli.
* **Validare la compatibilità e rifiutare l'aggiornamento se i dati attuali non possono essere migrati correttamente** — genera un'eccezione dall'handler e l'installazione si interrompe senza applicare modifiche. Questo è più sicuro che scoprire l'incompatibilità a migrazione in corso.
* **Rinominare o rigenerare le chiavi dei dati** prima di una modifica dello schema che farebbe perdere l'associazione.
Esempio — archiviare i record prima di una migrazione distruttiva:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Regola generale:**
| Vuoi... | Usa |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Popolare dati predefiniti, configurare il workspace, registrare risorse esterne | `post-install` |
| Eseguire seeding di lunga durata o chiamate a terze parti che non dovrebbero bloccare la risposta dell'installazione | `post-install` (predefinito — `shouldRunSynchronously: false`, con retry del worker) |
| Eseguire un setup rapido di cui il chiamante farà affidamento immediatamente dopo il ritorno della chiamata di installazione | `post-install` con `shouldRunSynchronously: true` |
| Leggere o eseguire il backup dei dati che la prossima migrazione perderebbe | `pre-install` |
| Rifiutare un aggiornamento che corromperebbe i dati esistenti | `pre-install` (genera un'eccezione dall'handler) |
| Eseguire la riconciliazione a ogni aggiornamento | `post-install` con `shouldRunOnVersionUpgrade: true` |
| Eseguire un setup una tantum solo alla prima installazione | `post-install` con `shouldRunOnVersionUpgrade: false` (predefinito) |
<Note>
In caso di dubbio, usa **post-install**. Ricorri al pre-install solo quando la migrazione stessa è distruttiva e devi intercettare lo stato precedente prima che vada perso.
</Note>
</Accordion>
</AccordionGroup>
@@ -0,0 +1,51 @@
---
title: Panoramica
description: Configura l'app stessa — la sua identità, le autorizzazioni predefinite e ciò che viene eseguito al momento dell'installazione.
icon: screwdriver-wrench
---
Il **config layer** di un'app Twenty è ciò che descrive l'app *alla piattaforma* — la sua identità, le autorizzazioni che detiene e il codice che viene eseguito durante l'installazione o l'aggiornamento. Queste dichiarazioni non aggiungono nuove strutture di dati o comportamenti a runtime; dicono a Twenty *chi è l'app* e *come configurarla*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
```
## In questa sezione
<CardGroup cols={2}>
<Card title="Configurazione dell'applicazione" icon="rocket" href="/l/it/developers/extend/apps/config/application">
`defineApplication` — identità, ruolo predefinito, variabili, metadati del marketplace.
</Card>
<Card title="Ruoli e permessi" icon="shield-halved" href="/l/it/developers/extend/apps/config/roles">
`defineRole` — dichiara ciò che le funzioni logiche della tua app possono leggere e scrivere.
</Card>
<Card title="Hook di installazione" icon="wrench" href="/l/it/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction` e `definePostInstallLogicFunction` — esegui il backup dei dati, imposta i valori predefiniti, convalida gli aggiornamenti.
</Card>
</CardGroup>
## Come si relazionano gli elementi
* L'**applicazione** è il punto di ingresso. Ogni app ha esattamente una chiamata a `defineApplication()` e punta a un **Ruolo** come predefinito.
* Il **Ruolo** controlla ciò che le funzioni di logica e i componenti front-end dell'app possono leggere e scrivere. Segui il principio del privilegio minimo: concedi solo le autorizzazioni di cui il tuo codice ha effettivamente bisogno.
* Gli **hook di installazione** vengono eseguiti durante l'installazione o l'aggiornamento — pre-installazione prima della migrazione dei metadati (così può rifiutare un aggiornamento rischioso), post-installazione dopo la migrazione (così può inserire dati predefiniti in base al nuovo schema).
<Note>
Gli hook di installazione condividono il runtime delle [logic function](/l/it/developers/extend/apps/logic/logic-functions) — stessa firma dell'handler, stesse variabili d'ambiente, stesso client API tipizzato — ma sono dichiarati con le proprie funzioni `define` e vivono al di fuori del normale modello di trigger (HTTP, cron, eventi del database).
</Note>
@@ -0,0 +1,67 @@
---
title: Asset pubblici
description: Distribuisci file statici — immagini, icone, font — insieme alla tua app tramite la cartella public/.
icon: folder-open
---
La cartella `public/` alla radice della tua app contiene file statici — immagini, icone, font o qualsiasi altro asset di cui la tua app ha bisogno a runtime. Questi file sono inclusi automaticamente nelle build, sincronizzati durante la modalità di sviluppo e caricati sul server.
I file posizionati in `public/` sono:
* **Pubblicamente accessibili** — una volta sincronizzati sul server, gli asset sono serviti a un URL pubblico. Non è necessaria alcuna autenticazione per accedervi.
* **Disponibili nei componenti front-end** — usa gli URL degli asset per visualizzare immagini, icone o qualsiasi media all'interno dei tuoi componenti React.
* **Disponibili nelle funzioni logiche** — fai riferimento agli URL degli asset nelle email, nelle risposte API o in qualsiasi logica lato server.
* **Usati per i metadati del marketplace** — i campi `logoUrl` e `screenshots` in `defineApplication()` fanno riferimento a file di questa cartella (ad es., `public/logo.png`). Questi vengono visualizzati nel marketplace quando la tua app viene pubblicata.
* **Sincronizzati automaticamente in modalità dev** — quando aggiungi, aggiorni o elimini un file in `public/`, viene sincronizzato automaticamente con il server. Nessun riavvio necessario.
* **Inclusi nelle build** — `yarn twenty dev:build` raggruppa tutti gli asset pubblici nell'output di distribuzione.
## Accedere agli asset pubblici con `getPublicAssetUrl`
Usa l'helper `getPublicAssetUrl` da `twenty-sdk` per ottenere l'URL completo di un file nella tua directory `public/`. Funziona sia nelle funzioni logiche che nei componenti front-end.
**In una funzione logica:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**In un componente front-end:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
L'argomento `path` è relativo alla cartella `public/` della tua app. Sia `getPublicAssetUrl('logo.png')` sia `getPublicAssetUrl('public/logo.png')` risolvono allo stesso URL — il prefisso `public/` viene rimosso automaticamente se presente.
@@ -0,0 +1,94 @@
---
title: Ruoli e permessi
description: Dichiara quali oggetti e campi le funzioni di logica e i componenti front-end della tua app possono leggere e scrivere.
icon: shield-halved
---
Un **ruolo** è un insieme di permessi: quali oggetti unapp può leggere o scrivere, quali campi può vedere e quali funzionalità a livello di piattaforma può usare. Tutte le funzioni di logica e i componenti front-end di ogni app ereditano i permessi del ruolo contrassegnato con `defineApplicationRole()` (vedi [The default function role](#the-default-function-role) di seguito).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Ruolo funzione predefinito
Quando esegui lo scaffolding di una nuova app, la CLI crea un file di ruolo predefinito dichiarato con `defineApplicationRole()`:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` è un leggero wrapper attorno a `defineRole()` che contrassegna il ruolo usato come predefinito per la tua applicazione al momento dellinstallazione. La convalida è identica a `defineRole`, ma la pipeline di build collega automaticamente il suo `universalIdentifier` al `defaultRoleUniversalIdentifier` del manifest dellapplicazione — quindi non è necessario farvi riferimento in [`defineApplication`](/l/it/developers/extend/apps/config/application).
Note:
* È consentita esattamente **una** chiamata a `defineApplicationRole(...)` per app — la build del manifest avrà esito negativo se ne trova più di una.
* Usa `defineRole()` (non `defineApplicationRole()`) per qualsiasi ruolo **aggiuntivo** fornito dalla tua app.
* Limpostazione esplicita di `defaultRoleUniversalIdentifier` in `defineApplication()` è ancora supportata per retrocompatibilità, ma è deprecata a favore di `defineApplicationRole()`.
## Migliori Pratiche
* Parti dal ruolo generato dallo scaffold, poi restringilo progressivamente: il ruolo predefinito concede un ampio accesso in lettura, che raramente è ciò che vuoi in produzione.
* Sostituisci `objectPermissions` e `fieldPermissions` con gli oggetti e i campi di cui le tue funzioni hanno realmente bisogno.
* `permissionFlagUniversalIdentifiers` controllano l'accesso alle funzionalità a livello di piattaforma. Mantienili al minimo.
* Vedi un esempio funzionante: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
@@ -0,0 +1,50 @@
---
title: Estendere gli oggetti
description: Aggiungi campi agli oggetti standard di Twenty (Person, Company, …) o agli oggetti di altre app usando defineField.
icon: wand-magic-sparkles
---
Usa `defineField()` per aggiungere un campo a un oggetto che non possiedi — un oggetto standard di Twenty come Person o Company, oppure un oggetto fornito da un'altra app installata. A differenza dei campi inline dichiarati all'interno di [`defineObject`](/l/it/developers/extend/apps/data/objects), i campi autonomi richiedono un `objectUniversalIdentifier` per specificare quale oggetto estendono.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
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 di Twenty, importa la costante da `twenty-sdk`:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Quando definisci campi **inline dentro `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()`.
* Il percorso del file dipende da te. La convenzione è `src/fields/\<name>.field.ts`, ma l'SDK rileva i campi ovunque in `src/`.
* Per aggiungere una scheda a un layout di pagina standard (ad es. la pagina di dettaglio di Task o Company), utilizza [`definePageLayoutTab`](/l/it/developers/extend/apps/layout/page-layouts#definepagelayouttab) con `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` da `twenty-sdk/define`.
## Aggiungere una relazione a un oggetto esistente
Per aggiungere un campo di relazione (ad esempio collegare il tuo oggetto personalizzato a un `Person` standard), usa `defineField()` con `FieldType.RELATION`. Lo schema è lo stesso delle relazioni inline, ma con `objectUniversalIdentifier` impostato esplicitamente. Vedi [Relazioni](/l/it/developers/extend/apps/data/relations) per lo schema bidirezionale.
@@ -0,0 +1,104 @@
---
title: Oggetti
description: Dichiara nuovi tipi di record — tabelle personalizzate con i propri campi — usando defineObject.
icon: tabella
---
Gli **oggetti** personalizzati sono nuovi tipi di record che la tua app aggiunge a uno spazio di lavoro — Cartolina, Fattura, Abbonamento, qualsiasi cosa specifica per il tuo dominio. Ogni oggetto dichiara il proprio schema (campi, relazioni, valori predefiniti) e un identificatore universale stabile che persiste tra le sincronizzazioni e le distribuzioni.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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
* 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.
* I campi inline definiti qui **non** hanno bisogno di un `objectUniversalIdentifier` — viene ereditato dall'oggetto padre. Usa [`defineField()`](/l/it/developers/extend/apps/data/extending-objects) per aggiungere campi a oggetti che non possiedi.
* Puoi generare nuovi oggetti con `yarn twenty dev:add object`, che ti guida nella denominazione, nei campi e nelle relazioni. Vedi [Architettura → Scaffolding delle entità](/l/it/developers/extend/apps/getting-started/scaffolding).
<Note>
**I campi base vengono aggiunti automaticamente.** Quando definisci un oggetto personalizzato, Twenty crea per te campi standard come `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`. Non è necessario dichiararli nel tuo array `fields` — solo i tuoi campi personalizzati. Puoi sovrascrivere un campo predefinito dichiarandone uno con lo stesso nome, ma è raramente una buona idea.
</Note>
## Valori predefiniti
I valori predefiniti letterali devono essere racchiusi tra apici singoli **all'interno** della stringa — `defaultValue: "'Draft'"`, non `defaultValue: "Draft"`. Ecco perché il campo `status` sopra utilizza `` `'${PostCardStatus.DRAFT}'` ``.
Le stringhe senza virgolette sono riservate ai valori predefiniti calcolati, valutati quando viene creato un record:
* `'uuid'` — genera un UUID (per i campi `UUID`)
* `'now'` — il timestamp corrente (per i campi `DATE_TIME`)
La stessa convenzione si applica ai sotto-campi stringa dei valori compositi predefiniti (ad es. `{ source: "'MANUAL'" }` su un campo `ACTOR`) e ai valori `SELECT`/`MULTI_SELECT`. Una stringa letterale predefinita lasciata senza virgolette genera un avviso quando la tua app viene compilata.
## Cosa c'è dopo
* **Collega questo oggetto ad altri** — vedi [Relazioni](/l/it/developers/extend/apps/data/relations) per il pattern di relazione bidirezionale.
* **Aggiungi campi a oggetti di altre app** — vedi [Estendere gli oggetti](/l/it/developers/extend/apps/data/extending-objects) per `defineField()`.
* **Mostra questo oggetto nell'interfaccia utente** — vedi [Viste](/l/it/developers/extend/apps/layout/views) e [Elementi del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items) per inserirlo nella barra laterale.
@@ -0,0 +1,97 @@
---
title: Panoramica
description: Definisci i dati che la tua app aggiunge a uno spazio di lavoro — oggetti, campi e relazioni.
icon: database
---
Il **data layer** di un'app Twenty è l'insieme dei dati che la tua app *aggiunge* a uno spazio di lavoro — i nuovi tipi di record che dichiara, le colonne che aggiunge agli oggetti esistenti e come questi record si collegano tra loro.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
├── lives in your app, OR
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## In questa sezione
<CardGroup cols={2}>
<Card title="Oggetti" icon="tabella" href="/l/it/developers/extend/apps/data/objects">
`defineObject` — dichiara nuovi tipi di record con i propri campi.
</Card>
<Card title="Estendere gli oggetti" icon="wand-magic-sparkles" href="/l/it/developers/extend/apps/data/extending-objects">
`defineField` — aggiunge campi agli oggetti standard o di altre app.
</Card>
<Card title="Relazioni" icon="diagram-project" href="/l/it/developers/extend/apps/data/relations">
Connessioni bidirezionali `MANY_TO_ONE` / `ONE_TO_MANY` tra oggetti.
</Card>
</CardGroup>
## Entità in sintesi
| Entità | Scopo | Definito con |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Oggetto** | Un nuovo tipo di record personalizzato (ad es. PostCard, Invoice) con i propri campi | `defineObject()` |
| **Campo** | Una colonna su un oggetto. I campi autonomi possono estendere oggetti che non hai creato tu (ad es. aggiungere `loyaltyTier` a Company) | `defineField()` |
| **Relazione** | Un collegamento bidirezionale tra due oggetti — entrambi i lati sono dichiarati come campi | `defineField()` con `FieldType.RELATION` |
| **Indice** | Un indice di database per velocizzare una query ricorrente su uno dei tuoi oggetti | `defineIndex()` |
L'SDK rileva questi elementi tramite analisi AST in fase di build, quindi l'organizzazione dei file dipende da te — la convenzione è `src/objects/`, `src/fields/` e `src/indexes/`. Gli UUID `universalIdentifier` stabili collegano tutto tra le varie distribuzioni.
## Indici (facoltativi)
Le app possono distribuire indici insieme ai loro oggetti per mantenere veloci le query ricorrenti. Il caso più comune è una colonna di stato o di chiave esterna che leggi spesso.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Indici univoci
`defineIndex` accetta `isUnique: true` sia per l'univocità a singola colonna sia per quella multi-colonna. Questa è la primitiva consigliata — `defineField({ isUnique: true })` è deprecata e verrà rimossa in una futura release.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Altri vincoli
* Le clausole `WHERE` parziali restano sotto il controllo dell'amministratore — le app non possono dichiararle.
* Ogni oggetto è limitato a 10 indici personalizzati (gli indici propri del framework non contano).
Ordina l'array `fields` nel modo in cui Postgres dovrebbe usarlo — la colonna più a sinistra per prima, come in un elenco telefonico. Gli indici non sono gratuiti: ogni scrittura sulla tabella li aggiorna. Aggiungine uno solo quando hai una query che ne ha bisogno.
<Note>
Cerchi **Application Config** o **Roles & Permissions**? Quelli descrivono l'app in sé piuttosto che i dati che aggiunge — si trovano sotto [Config](/l/it/developers/extend/apps/config/overview). Cerchi **Connections** (Linear, GitHub, Slack OAuth)? Quelle esistono per essere chiamate *dalle* funzioni di logica e si trovano sotto [Logic](/l/it/developers/extend/apps/logic/connections).
</Note>
@@ -0,0 +1,160 @@
---
title: Relazioni
description: Collega gli oggetti tra loro con relazioni bidirezionali MANY_TO_ONE / ONE_TO_MANY.
icon: diagram-project
---
Le relazioni collegano due oggetti tra loro. In Twenty, le relazioni sono sempre **bidirezionali** — ogni relazione ha due lati e ciascun lato è dichiarato come un campo che fa riferimento all'altro.
| 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
Un `PostCard` può 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"):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
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):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
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. 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`:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
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 |
| ------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `type` | 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` o `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
Puoi anche dichiarare una relazione direttamente all'interno di [`defineObject`](/l/it/developers/extend/apps/data/objects). Quando è inline, ometti `objectUniversalIdentifier` — viene ereditato dall'oggetto padre:
```ts
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
],
});
```
@@ -0,0 +1,101 @@
---
title: Concetti
description: Come funzionano le app Twenty — modello di entità, sandboxing e ciclo di vita dellinstallazione.
icon: sitemap
---
Le app di Twenty sono pacchetti TypeScript che estendono il tuo spazio di lavoro con oggetti personalizzati, logica, componenti dell'interfaccia utente (UI) e funzionalità di IA. Vengono eseguite sulla piattaforma Twenty con sandboxing completo e controlli delle autorizzazioni.
## Come funzionano le app
Un'app è una raccolta di **entità** dichiarate utilizzando le funzioni `defineEntity()` del pacchetto `twenty-sdk`. L'SDK rileva queste dichiarazioni tramite analisi dell'AST in fase di build e produce un **manifest** — una descrizione completa di ciò che la tua app aggiunge a uno spazio di lavoro. Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**L'organizzazione dei file dipende da te.** Il rilevamento delle entità è basato sull'AST — l'SDK trova le chiamate a `export default defineEntity(...)` indipendentemente da dove si trova il file. La struttura delle cartelle sopra è una convenzione, non un requisito.
</Note>
## Tipi di entità
| Entità | Scopo | Documentazione |
| -------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| **Applicazione** | Identità dellapp, ruolo predefinito, variabili | [Configurazione dellapplicazione](/l/it/developers/extend/apps/config/application) |
| **Ruolo** | Set di autorizzazioni per oggetti e campi | [Ruoli e autorizzazioni](/l/it/developers/extend/apps/config/roles) |
| **Oggetto** | Tipi di record personalizzati con campi | [Oggetti](/l/it/developers/extend/apps/data/objects) |
| **Campo** | Aggiungi campi agli oggetti di altre app | [Estensione degli oggetti](/l/it/developers/extend/apps/data/extending-objects) |
| **Relazione** | Collegamenti bidirezionali tra oggetti | [Relazioni](/l/it/developers/extend/apps/data/relations) |
| **Funzione logica** | TypeScript lato server con trigger | [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) |
| **Abilità** | Istruzioni riutilizzabili per agenti IA | [Abilità e agenti](/l/it/developers/extend/apps/logic/skills-and-agents) |
| **Agente** | Assistenti IA con prompt personalizzati | [Abilità e agenti](/l/it/developers/extend/apps/logic/skills-and-agents) |
| **Provider di connessione** | Credenziali OAuth per API di terze parti | [Connessioni](/l/it/developers/extend/apps/logic/connections) |
| **Vista** | Viste di elenco dei record preconfigurate | [Viste](/l/it/developers/extend/apps/layout/views) |
| **Voce del menu di navigazione** | Voci della barra laterale personalizzate | [Voci del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items) |
| **Layout di pagina** | Schede e widget nella pagina dei dettagli di un record | [Layout di pagina](/l/it/developers/extend/apps/layout/page-layouts) |
| **Componente front-end** | UI React in sandbox allinterno di Twenty | [Componenti front-end](/l/it/developers/extend/apps/layout/front-components) |
| **Voce del menu comandi** | Azioni rapide e voci Cmd+K | [Voci del menu comandi](/l/it/developers/extend/apps/layout/command-menu-items) |
## Sandboxing
* **Le funzioni logiche** vengono eseguite in processi Node.js isolati sul server. Accedono ai dati solo tramite il client API tipizzato, con ambito limitato alle autorizzazioni del ruolo dell'app.
* **I componenti front-end** vengono eseguiti in Web Workers utilizzando il Remote DOM — isolati dalla pagina principale ma renderizzando elementi DOM nativi (non iframe). Comunicano con Twenty tramite un'API host basata sul passaggio di messaggi.
* **Le autorizzazioni** vengono applicate a livello di API. Il token di runtime (`TWENTY_APP_ACCESS_TOKEN`) è derivato dal ruolo definito in `defineApplication()`.
## Ciclo di vita dell'app
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — monitora i file sorgente e sincronizza in tempo reale le modifiche su un server Twenty connesso. Il client API tipizzato viene rigenerato automaticamente quando lo schema cambia.
* **`yarn twenty dev:build`** — compila TypeScript, crea i bundle delle funzioni logiche e dei componenti front-end con esbuild e produce un manifest.
* **Hook di pre/post-installazione** — funzioni opzionali che vengono eseguite durante linstallazione. Vedi [Hook di installazione](/l/it/developers/extend/apps/config/install-hooks) per i dettagli.
## Prossimi passaggi
<CardGroup cols={2}>
<Card title="Configurazione" icon="screwdriver-wrench" href="/l/it/developers/extend/apps/config/overview">
Identità dellapplicazione, ruolo predefinito e hook di installazione.
</Card>
<Card title="Dati" icon="database" href="/l/it/developers/extend/apps/data/overview">
Oggetti, campi e relazioni bidirezionali.
</Card>
<Card title="Logica" icon="bolt" href="/l/it/developers/extend/apps/logic/overview">
Funzioni logiche, abilità, agenti e connessioni OAuth.
</Card>
<Card title="Layout" icon="table-columns" href="/l/it/developers/extend/apps/layout/overview">
Viste, navigazione, layout di pagina, componenti front-end.
</Card>
<Card title="Operazioni" icon="rocket" href="/l/it/developers/extend/apps/operations/overview">
CLI, test, remoti, CI e pubblicazione della tua app.
</Card>
</CardGroup>
@@ -0,0 +1,87 @@
---
title: Server locale
description: Gestisci il server Docker Twenty locale — avvia, arresta, aggiorna, istanza di test parallela e configurazione manuale dell'SDK.
icon: server
---
## Gestire il server locale
Usa `yarn twenty docker:*` per controllare il container Twenty locale:
| Comando | Cosa fa |
| -------------------------------------- | -------------------------------------------------- |
| `yarn twenty docker:start` | Avvia il server (scarica l'immagine se necessario) |
| `yarn twenty docker:start 2.2.0` | Avvia una versione specifica del server |
| `yarn twenty docker:start --port 3030` | Avvia su una porta personalizzata |
| `yarn twenty docker:stop` | Arresta il server (conserva i dati) |
| `yarn twenty docker:status` | Mostra URL, versione e credenziali di accesso |
| `yarn twenty docker:logs` | Trasmetti in streaming i log del server |
| `yarn twenty docker:reset` | Cancella i dati e riparti da zero |
| `yarn twenty docker:upgrade` | Scarica l'ultima immagine `twenty-app-dev` |
| `yarn twenty docker:upgrade 2.2.0` | Aggiorna a una versione specifica |
I dati persistono tra i riavvii in due volumi Docker (`twenty-app-dev-data` per PostgreSQL, `twenty-app-dev-storage` per i file). Usa `reset` per cancellare tutto.
## Blocco della versione del server
Quando non viene passata alcuna versione, `docker:start` risolve la versione dall'intervallo `engines.twenty` della tua app in `package.json`, lo stesso intervallo rispetto al quale il server effettua la convalida quando la tua app viene installata. Avvia l'immagine `twenty-app-dev` più recente pubblicata che soddisfa l'intervallo, ricorrendo a `latest` quando il campo è assente o nessuna versione pubblicata corrisponde:
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Passa esplicitamente una versione per ignorare l'intervallo per una singola esecuzione: `yarn twenty docker:start 2.3.0`. Se un container esiste già con una versione diversa, `docker:start` lo aggiorna sul posto (ricreando il container ma preservando i volumi dei tuoi dati).
## Aggiornamento dell'immagine del server
`yarn twenty docker:upgrade` scarica l'ultima immagine, confronta i digest e ricrea il container solo se è cambiato qualcosa. I volumi vengono preservati — viene sostituito solo il container. Se è stata scaricata una nuova immagine e il container era in esecuzione, l'upgrade avvia automaticamente un nuovo container; esegui poi `yarn twenty docker:start` per attendere che diventi operativo.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Verifica la versione in esecuzione con `yarn twenty docker:status` (mostra `APP_VERSION` incorporata nel container).
## Esecuzione di un'istanza di test parallela
Passa `--test` a qualsiasi comando `docker:*` per gestire una seconda istanza completamente isolata — utile per test di integrazione o per sperimentare senza toccare i tuoi dati di sviluppo principali:
| Comando | Cosa fa |
| ----------------------------------- | ------------------------------------------------------------------------ |
| `yarn twenty docker:start --test` | Avvia l'istanza di test (per impostazione predefinita usa la porta 2021) |
| `yarn twenty docker:stop --test` | Arrestala |
| `yarn twenty docker:status --test` | Mostra il suo stato |
| `yarn twenty docker:logs --test` | Trasmetti in streaming i suoi log |
| `yarn twenty docker:reset --test` | Cancella i suoi dati |
| `yarn twenty docker:upgrade --test` | Aggiorna la sua immagine |
L'istanza di test ha il proprio container (`twenty-app-dev-test`), i propri volumi (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e la propria configurazione — viene eseguita insieme alla tua istanza principale senza conflitti. Combina `--test` con `--port` per sovrascrivere 2021.
## Configurazione manuale (senza lo scaffolder)
Salta lo strumento di scaffolding se stai aggiungendo l'SDK a un progetto esistente:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Aggiungi lo script a `package.json`:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Ora puoi eseguire `yarn twenty dev`, `yarn twenty docker:start` e il resto.
<Note>
Non installare `twenty-sdk` globalmente — bloccalo per progetto in modo che ogni app usi la propria versione.
</Note>
@@ -0,0 +1,61 @@
---
title: Struttura del progetto
description: Cosa contiene un'app Twenty creata con lo scaffold — file, cartelle e cosa fa ognuno di essi.
icon: folder-tree
---
Una nuova app generata da `npx create-twenty-app` si presenta così:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
```
## File principali
| File / Cartella | Scopo |
| ---------------------------------------- | -------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Obbligatorio.** Il file di configurazione principale della tua app. |
| `src/default-role.ts` | Ruolo predefinito che controlla a cosa possono accedere le tue funzioni logiche. |
| `src/constants/universal-identifiers.ts` | UUID generati automaticamente e metadati (nome visualizzato, descrizione). |
| `src/__tests__/` | Test di integrazione (setup + test di esempio). |
| `public/` | Asset statici (immagini, font) serviti insieme alla tua app. |
<Note>
**L'organizzazione dei file dipende da te.** Le cartelle sopra sono convenzioni — l'SDK rileva le entità tramite analisi AST sulle chiamate a `export default defineEntity(...)` indipendentemente da dove si trova il file.
</Note>
## Dipendenze
Entrambi i pacchetti Twenty SDK devono essere inseriti sotto `devDependencies`, non sotto `dependencies`:
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "^2.13.0",
"twenty-sdk": "^2.13.0"
}
}
```
* **`twenty-sdk`** fornisce la CLI `twenty` e gli strumenti di build/scaffolding. Viene eseguito solo in fase di sviluppo e di build e non viene mai importato dal runtime dell'app pubblicata.
* **`twenty-client-sdk`** *viene* importato dal codice della tua app (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), ma Twenty lo fornisce a runtime: le funzioni di logica lo ricevono da un layer SDK generato e i componenti di front-end lo risolvono da moduli forniti dal server. La copia installata viene utilizzata solo per il type checking e per la build al momento del deploy, quindi non è mai necessario includerla nel bundle distribuito.
Mantenere uno qualsiasi dei pacchetti sotto `dependencies` lo inserisce nel bundle di runtime dell'app installata, dove rappresenta solo zavorra. `twenty build` emette un avviso quando uno dei due è ancora elencato sotto `dependencies`.
Aggiungi come di consueto le dipendenze di runtime proprie della tua app (librerie che le tue funzioni di logica importano effettivamente a runtime) sotto `dependencies`.
@@ -0,0 +1,176 @@
---
title: Avvio rapido
icon: rocket
description: Crea la tua prima app Twenty in pochi minuti.
---
## Prerequisiti
* **Node.js 24+** — [Scarica](https://nodejs.org/)
* **Yarn 4** — incluso con Node.js tramite Corepack. Abilitalo: `corepack enable`
* **Docker** — [Scarica](https://www.docker.com/products/docker-desktop/). Necessario per eseguire un server Twenty locale. Salta se hai già Twenty in esecuzione altrove.
La creazione di un'app Twenty ha tre fasi. Lo strumento di scaffolding le combina in un unico comando per il percorso ottimale, ma ogni fase è un concetto distinto — quando qualcosa fallisce, sapere in quale fase ti trovi indica cosa correggere.
| Fase | Cosa fai | Strumento | Risultato |
| ----------------------- | ------------------------------------------------------ | ----------------------------- | ---------------------------------- |
| **1. Crea struttura** | Genera il codice sorgente dell'app | `npx create-twenty-app` | Un progetto TypeScript sul disco |
| **2. Esegui un server** | Avvia un server Twenty con cui sincronizzare | Docker + `yarn twenty server` | Un'istanza Twenty in esecuzione |
| **3. Sincronizza** | Sincronizza in tempo reale il tuo codice con il server | `yarn twenty dev` | Le tue modifiche compaiono nell'UI |
---
## Fase 1 — Crea la struttura del progetto
Crea una nuova app dal modello:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Ti verrà chiesto un nome e una descrizione — premi **Invio** per usare i valori predefiniti. Questo genera un progetto TypeScript in `my-twenty-app/` con un `application-config.ts` iniziale, un ruolo predefinito, un workflow CI e un test di integrazione.
**Dopo questa fase:** hai il codice sorgente dell'app sulla tua macchina. Non è ancora in esecuzione — questa è la Fase 2.
---
## Fase 2 — Esegui un server Twenty locale
La tua app ha bisogno di un server Twenty con cui sincronizzarsi. Il server è un'istanza Twenty completa — UI, API GraphQL, PostgreSQL — in esecuzione in locale su Docker. Il tuo codice locale carica le sue definizioni su quel server, che le rende visibili nell'UI.
Lo strumento di scaffolding ti propone di avviarne uno per te:
> **Vuoi configurare un'istanza locale di Twenty?**
* **Sì (consigliato)** — scarica l'immagine Docker `twentycrm/twenty-app-dev` e la avvia sulla porta `2020`. Assicurati prima che Docker sia in esecuzione.
* **No** — scegli questa opzione se hai già un server Twenty a cui vuoi connetterti. Puoi collegarlo in seguito con `yarn twenty remote:add`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Avviare l'istanza locale?" />
</div>
Quando il server è attivo, si apre il browser per l'accesso. Usa l'account demo preconfigurato:
* **Email:** `tim@apple.dev`
* **Password:** `tim@apple.dev`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Schermata di accesso di Twenty" />
</div>
Fai clic su **Authorize** nella schermata successiva — questo concede alla CLI l'accesso al tuo spazio di lavoro.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Schermata di autorizzazione della CLI di Twenty" />
</div>
Il terminale confermerà che tutto è configurato.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App creata con successo" />
</div>
**Dopo questa fase:** hai un server Twenty in esecuzione su [http://localhost:2020](http://localhost:2020) con la tua CLI autorizzata a sincronizzarsi con esso.
<Note>
Se Docker non è installato o in esecuzione, lo strumento di scaffolding ti indicherà il comando di avvio corretto per il tuo sistema operativo. Quando Docker è attivo, puoi riprendere con `yarn twenty docker:start` — non è necessario rifare lo scaffolding.
</Note>
---
## Fase 3 — Sincronizza le tue modifiche
Questo è il ciclo interno in cui passerai la maggior parte del tempo.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Questo osserva `src/`, ricompila a ogni modifica e sincronizza il risultato con il server. Modifica un file, salvalo e in pochi secondi il server riflette la modifica. Vedrai un pannello di stato in tempo reale nel terminale.
Per un output più dettagliato (log di build, richieste di sincronizzazione, tracce di errore), aggiungi `--verbose`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Output del terminale in modalità sviluppo" />
</div>
Apri [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Dovresti vedere la tua app sotto **Your Apps**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Elenco Your Apps che mostra My twenty app" />
</div>
Fai clic su **My twenty app** per vedere la sua **registrazione dell'applicazione** — un record a livello di server che descrive la tua app (nome, identificatore, credenziali OAuth, origine). Una registrazione può essere installata in più spazi di lavoro sullo stesso server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Dettagli della registrazione dell'applicazione" />
</div>
Fai clic su **View installed app** per vedere l'installazione nello spazio di lavoro. La scheda **About** mostra la versione e le opzioni di gestione.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="App installata" />
</div>
**Dopo questa fase:** hai un ciclo di sviluppo in tempo reale. Modifica qualsiasi file in `src/` e compare nell'UI.
### Sincronizzazione una tantum per CI e script
Passa `--once` per eseguire una singola build + sincronizzazione ed uscire — stessa pipeline, nessun watcher:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comando | Comportamento | Quando usarlo |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora e risincronizza a ogni modifica. Rimane in esecuzione finché non lo interrompi. | Sviluppo locale interattivo. |
| `yarn twenty dev --once` | Singola build + sincronizzazione, termina con codice `0` in caso di successo, `1` in caso di errore. | CI, hook pre-commit, agenti IA, flussi di lavoro scriptati. |
| `yarn twenty dev --once --dry-run` | Crea e stampa le modifiche ai metadati **senza applicarle**. | Ispezionare quali modifiche verrebbero apportate da una sincronizzazione prima di confermarla. |
Entrambe le modalità richiedono un remoto autenticato. Vedi [Sincronizzazione e ripristino](/l/it/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run) per maggiori informazioni su `--dry-run`.
### Opzioni della modalità di sviluppo
| Opzione | Descrizione |
| ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `--once` | Esegui una build e una sincronizzazione una sola volta, quindi esci. |
| `--dry-run` | Con `--once`, visualizza in anteprima le modifiche ai metadati senza applicarle. Non scrive nulla. |
| `--debounceMs \<ms>` | Imposta il ritardo di debounce delle modifiche ai file in millisecondi (predefinito: `2000`). |
| `--verbose` / `--debug` | Mostra log di build dettagliati, richieste di sincronizzazione e tracce di errore. |
## Cosa puoi creare
Le app sono composte da **entità** — ciascuna definita come un file TypeScript con un singolo `export default`:
| Entità | Cosa fa |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| **Oggetti e campi** | Modelli di dati personalizzati (Cartolina, Fattura, ecc.) con campi tipizzati |
| **Funzioni logiche** | TypeScript lato server attivato da route HTTP, pianificazioni cron o eventi del database |
| **Componenti front-end** | Componenti React che vengono renderizzati all'interno dell'UI di Twenty (pannello laterale, widget, menu comandi) |
| **Skill e agenti** | Funzionalità di IA — istruzioni riutilizzabili e assistenti autonomi |
| **Viste e navigazione** | Viste elenco preconfigurate e voci di menu della barra laterale |
| **Layout di pagina** | Pagine di dettaglio dei record personalizzate con schede e widget |
Riferimento completo: [Concetti](/l/it/developers/extend/apps/getting-started/concepts).
## Prossimi passaggi
<CardGroup cols={2}>
<Card title="Configurazione" icon="screwdriver-wrench" href="/l/it/developers/extend/apps/config/overview">
Identità dell'applicazione, ruolo predefinito, hook di installazione, asset pubblici.
</Card>
<Card title="Dati" icon="database" href="/l/it/developers/extend/apps/data/overview">
Oggetti, campi e relazioni bidirezionali.
</Card>
<Card title="Logica" icon="bolt" href="/l/it/developers/extend/apps/logic/overview">
Funzioni logiche, skill, agenti e connessioni OAuth.
</Card>
<Card title="Layout" icon="table-columns" href="/l/it/developers/extend/apps/layout/overview">
Viste, navigazione, layout di pagina, componenti front-end.
</Card>
<Card title="Operazioni" icon="rocket" href="/l/it/developers/extend/apps/operations/overview">
CLI, test, remoti, CI e pubblicazione della tua app.
</Card>
</CardGroup>
@@ -0,0 +1,58 @@
---
title: Scaffolding
description: "Genera i file delle entità in modo interattivo con yarn twenty dev:add: oggetti, campi, viste, funzioni di logica e altro ancora."
icon: wand-magic-sparkles
---
Invece di creare manualmente i file delle entità, puoi usare lo scaffolder interattivo:
```bash filename="Terminal"
yarn twenty dev:add
```
Ti chiede di scegliere un tipo di entità e ti guida attraverso i campi richiesti, quindi scrive un file pronto alluso con un `universalIdentifier` stabile e la corretta chiamata a `defineEntity()`.
Puoi anche passare direttamente il tipo di entità per saltare il primo prompt:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Tipi di entità disponibili
| Tipo di entità | Comando | File generato |
| ---------------------------- | ---------------------------------------- | ------------------------------------------------------- |
| Oggetto | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Campo | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Funzione logica | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Componente front-end | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Ruolo | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Abilità | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agente | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Vista | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Voce del menu di navigazione | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Layout di pagina | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
## Cosa genera lo scaffolder
Ogni tipo di entità ha il proprio template. Ad esempio, `yarn twenty dev:add object` richiede:
1. **Nome (singolare)** — ad es., `invoice`
2. **Nome (plurale)** — ad es., `invoices`
3. **Etichetta (singolare)** — compilata automaticamente dal nome (ad es., `Invoice`)
4. **Etichetta (plurale)** — compilata automaticamente (ad es., `Invoices`)
5. **Creare una vista e una voce di navigazione?** — se rispondi sì, lo scaffolder genera anche una vista corrispondente e un link nella barra laterale per il nuovo oggetto.
Gli altri tipi di entità hanno prompt più semplici — la maggior parte chiede solo un nome.
Il tipo di entità `field` è più dettagliato: chiede il nome del campo, l'etichetta, il tipo (da un elenco di tutti i tipi di campo disponibili come `TEXT`, `NUMBER`, `SELECT`, `RELATION`, ecc.) e l'`universalIdentifier` dell'oggetto di destinazione.
## Percorso di output personalizzato
Usa il flag `--path` per posizionare il file generato in una posizione personalizzata:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```
@@ -0,0 +1,14 @@
---
title: Risoluzione dei problemi
description: Problemi comuni al primo avvio — Docker, versione di Node, Yarn, dipendenze.
icon: wrench
---
* **Errori di Docker** — Assicurati che Docker Desktop (o il demone) sia in esecuzione prima di `yarn twenty docker:start`. Il messaggio di errore mostrerà il comando di avvio corretto per il tuo sistema operativo.
* **Versione di Node errata** — È necessaria la versione 24 o superiore. Verifica con `node -v`.
* **Manca Yarn 4** — Esegui `corepack enable`.
* **Dipendenze danneggiate** — `rm -rf node_modules && yarn install`.
* **Errori di `twenty-sdk` dopo l'aggiornamento alla v2.8.0** — è stato spostato da `dependencies` a `devDependencies` nella v2.8.0. Vedi [Struttura del progetto → Dipendenze](/l/it/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty build` mostra un avviso su `twenty-client-sdk` sotto `dependencies`** — viene fornito in fase di esecuzione da Twenty, quindi dovrebbe essere spostato in `devDependencies` insieme a `twenty-sdk`. Vedi [Struttura del progetto → Dipendenze](/l/it/developers/extend/apps/getting-started/project-structure#dependencies).
Bloccato? Chiedi aiuto su [Discord di Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
@@ -0,0 +1,148 @@
---
title: Voci del menu comandi
description: Esponi i componenti front-end come azioni rapide e voci del menu comandi (Cmd+K) con defineCommandMenuItem.
icon: terminale
---
Un **elemento del menu comandi** è il collegamento tra l'utente e un [front component](/l/it/developers/extend/apps/layout/front-components). Registra il componente nel menu comandi (Cmd+K) di Twenty e, opzionalmente, come pulsante di azione rapida fissato nell'angolo in alto a destra della pagina.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| --------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sì | ID univoco stabile per il comando |
| `label` | Sì | Etichetta completa mostrata nel menu comandi (Cmd+K) |
| `frontComponentUniversalIdentifier` | Sì | L'`universalIdentifier` del componente front-end che questo comando apre |
| `shortLabel` | No | Etichetta breve visualizzata sul pulsante di azione rapida fissato |
| `icon` | No | Nome dell'icona visualizzato accanto all'etichetta (ad es. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | No | Quando `true`, mostra il comando come pulsante di azione rapida nell'angolo in alto a destra della pagina |
| `availabilityType` | No | Controlla dove compare il comando: `'GLOBAL'` (sempre disponibile), `'RECORD_SELECTION'` (solo quando sono selezionati dei record) o `'FALLBACK'` (mostrato quando nessun altro comando corrisponde) |
| `availabilityObjectUniversalIdentifier` | No | Limita il comando alle pagine di uno specifico tipo di oggetto (ad es. solo sui record Company) |
| `conditionalAvailabilityExpression` | No | Un'espressione booleana che controlla dinamicamente la visibilità (vedi sotto) |
## Comandi headless
Un elemento del menu comandi abbinato a un [headless front component](/l/it/developers/extend/apps/layout/front-components#headless-vs-non-headless) è il modo idiomatico per distribuire un'azione con un clic: eseguire codice, navigare oppure confermare ed eseguire. La pagina Front Components tratta i [SDK Command components](/l/it/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) che gestiscono il pattern di action-and-unmount.
Un flusso tipico:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## Espressioni di disponibilità condizionale
Il campo `conditionalAvailabilityExpression` consente di controllare quando un comando è visibile in base al contesto della pagina corrente. Importa variabili tipizzate e operatori da `twenty-sdk` per costruire espressioni:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` implica già una selezione non vuota — usa `numberOfSelectedRecords` solo per conteggi specifici (ad es. `>= 2`).
</Note>
### Variabili di contesto
Rappresentano lo stato corrente della pagina:
| Variabile | Tipo | Descrizione |
| ------------------------------ | --------- | ------------------------------------------------------------------------ |
| `pageType` | `string` | Tipo di pagina corrente (ad es. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Indica se il componente è renderizzato in un pannello laterale |
| `numberOfSelectedRecords` | `number` | Numero di record attualmente selezionati |
| `isSelectAll` | `boolean` | Indica se "seleziona tutto" è attivo |
| `selectedRecords` | `array` | Gli oggetti dei record selezionati |
| `favoriteRecordIds` | `array` | ID dei record aggiunti ai preferiti |
| `objectPermissions` | `object` | Autorizzazioni per il tipo di oggetto corrente |
| `targetObjectReadPermissions` | `object` | Autorizzazioni di lettura per l'oggetto di destinazione |
| `targetObjectWritePermissions` | `object` | Autorizzazioni di scrittura per l'oggetto di destinazione |
| `featureFlags` | `object` | Flag delle funzionalità attivi |
| `objectMetadataItem` | `object` | Metadati del tipo di oggetto corrente |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Indica se la vista corrente ha un filtro di soft-delete |
### Operatori
Combinano le variabili in espressioni booleane:
| Operatore | Descrizione |
| ----------------------------------- | -------------------------------------------------------------------- |
| `isDefined(value)` | `true` se il valore non è null/undefined |
| `isNonEmptyString(value)` | `true` se il valore è una stringa non vuota |
| `includes(array, value)` | `true` se l'array contiene il valore |
| `includesEvery(array, prop, value)` | `true` se la proprietà di ogni elemento include il valore |
| `every(array, prop)` | `true` se la proprietà è truthy su ogni elemento |
| `everyDefined(array, prop)` | `true` se la proprietà è definita su ogni elemento |
| `everyEquals(array, prop, value)` | `true` se la proprietà è uguale al valore su ogni elemento |
| `some(array, prop)` | `true` se la proprietà è truthy su almeno un elemento |
| `someDefined(array, prop)` | `true` se la proprietà è definita su almeno un elemento |
| `someEquals(array, prop, value)` | `true` se la proprietà è uguale al valore su almeno un elemento |
| `someNonEmptyString(array, prop)` | `true` se la proprietà è una stringa non vuota su almeno un elemento |
| `none(array, prop)` | `true` se la proprietà è falsy su ogni elemento |
| `noneDefined(array, prop)` | `true` se la proprietà è undefined su ogni elemento |
| `noneEquals(array, prop, value)` | `true` se la proprietà non è uguale al valore su alcun elemento |
@@ -0,0 +1,545 @@
---
title: Componenti front-end
description: Crea componenti React che vengono renderizzati all'interno della UI di Twenty con isolamento in sandbox.
icon: window-maximize
---
I componenti front-end sono componenti React che vengono renderizzati direttamente all'interno della UI di Twenty. Vengono eseguiti in un **Web Worker** isolato utilizzando Remote DOM — il tuo codice è in sandbox ma viene renderizzato in modo nativo nella pagina, non in un iframe.
## Dove possono essere utilizzati i componenti front-end
I componenti front-end possono essere renderizzati in due posizioni all'interno di Twenty:
* **Pannello laterale** — I componenti front-end non headless si aprono nel pannello laterale destro. Questo è il comportamento predefinito quando un componente front-end viene avviato dal menu comandi.
* **Widget (dashboard e pagine dei record)** — I componenti front possono essere incorporati come widget all'interno dei [layout di pagina](/l/it/developers/extend/apps/layout/page-layouts). Quando si configura una dashboard o il layout di una pagina record, gli utenti possono aggiungere un widget del componente front-end.
Un componente front da solo non è raggiungibile dall'interfaccia utente: devi renderlo visibile. I due modi per farlo sono:
* **Associarlo a un [elemento di menu dei comandi](/l/it/developers/extend/apps/layout/command-menu-items)** — lo registra nel menu dei comandi (Cmd+K) e, facoltativamente, come azione rapida fissata.
* **Incorporarlo come widget in un [layout di pagina](/l/it/developers/extend/apps/layout/page-layouts)** — lo posiziona nella pagina dei dettagli di un record o in una dashboard.
## Esempio di base
Il modo più rapido per vedere un componente front in azione è associarlo a [`defineCommandMenuItem`](/l/it/developers/extend/apps/layout/command-menu-items), così appare come pulsante di azione rapida nellangolo in alto a destra della pagina:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Dopo la sincronizzazione con `yarn twenty dev` (o eseguendo una volta sola `yarn twenty dev --once`), l'azione rapida appare nell'angolo in alto a destra della pagina:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Pulsante di azione rapida nell'angolo in alto a destra" />
</div>
Fai clic per renderizzare il componente in linea.
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| --------------------- | ------------ | ---------------------------------------------------------------------- |
| `universalIdentifier` | Sì | ID univoco stabile per questo componente |
| `component` | Sì | Una funzione di componente React |
| `name` | No | Nome visualizzato |
| `description` | No | Descrizione di ciò che fa il componente |
| `isHeadless` | No | Imposta su `true` se il componente non ha una UI visibile (vedi sotto) |
## Posizionare un componente front-end su una pagina
Oltre ai comandi, puoi incorporare un componente front-end direttamente in una pagina record aggiungendolo come widget in un **layout di pagina**. Vedi [Layout di pagina](/l/it/developers/extend/apps/layout/page-layouts) per i dettagli.
## Headless vs non headless
I componenti front-end prevedono due modalità di rendering controllate dall'opzione `isHeadless`:
**Non headless (predefinito)** — Il componente renderizza un'interfaccia utente visibile. Quando viene avviato dal menu comandi, si apre nel pannello laterale. Questo è il comportamento predefinito quando `isHeadless` è `false` o omesso.
**Headless (`isHeadless: true`)** — Il componente viene montato in modo invisibile in background. Non apre il pannello laterale. I componenti headless sono pensati per azioni che eseguono una logica e poi si smontano — ad esempio, eseguire un'attività asincrona, navigare a una pagina o mostrare una finestra modale di conferma. Si abbinano naturalmente ai componenti Command dell'SDK descritti di seguito.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
Poiché il componente restituisce `null`, Twenty evita di renderizzare un contenitore per esso — non appare alcuno spazio vuoto nel layout. Il componente ha comunque accesso a tutti gli hook e all'API di comunicazione con l'host.
## Componenti Command dell'SDK
Il pacchetto `twenty-sdk` fornisce quattro componenti di supporto Command progettati per i componenti front-end headless. Ogni componente esegue un'azione al montaggio, gestisce gli errori mostrando una notifica snackbar e smonta automaticamente il componente front-end al termine.
Importali da `twenty-sdk/command`:
* **`Command`** — Esegue una callback asincrona tramite la prop `execute`.
* **`CommandLink`** — Naviga verso un percorso dell'app. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Apre una finestra modale di conferma. Se l'utente conferma, esegue la callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Apre una specifica pagina del pannello laterale. Props: `page`, `pageTitle`, `pageIcon`.
Ecco un esempio completo di componente front-end headless che usa `Command` per eseguire un'azione dal menu comandi:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
E un esempio che usa `CommandModal` per chiedere conferma prima di eseguire:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
## Chiamare una funzione logica
I componenti front vengono eseguiti lato browser in un Web Worker in sandbox, mentre le [funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) vengono eseguite lato server. Non esiste una chiamata diretta in-process tra i due; invece, un front component chiama una funzione logica tramite HTTP.
Una funzione logica dichiarata con `httpRouteTriggerSettings` è esposta sotto lendpoint `/s/` su `${TWENTY_API_URL}/s\<path>`. Il tuo front component chiama quella route con il `RestApiClient` da `twenty-client-sdk/rest`, che si autentica con il `TWENTY_APP_ACCESS_TOKEN` che Twenty inserisce nel worker.
Il `RestApiClient` è stato creato proprio per questo. Legge `TWENTY_API_URL` e `TWENTY_APP_ACCESS_TOKEN` dall'ambiente del worker, aggiunge l'header `Authorization: Bearer`, serializza e analizza il JSON e genera un `RestApiClientError` quando il token o l'URL mancano o la risposta non è 2xx — così non devi reimplementare quel boilerplate in ogni componente.
Un front component headless può eseguire la chiamata al mount tramite il componente `Command`, quindi smontarsi automaticamente:
```tsx src/front-components/sync-prs.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { RestApiClient } from 'twenty-client-sdk/rest';
const SyncPrs = () => {
const execute = async () => {
const client = new RestApiClient();
await client.post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
Il percorso passato al client è il percorso pubblico della route — la proprietà `httpRouteTriggerSettings.path` della funzione di logica con prefisso `/s`. Mantieni `isAuthRequired: true`; il client fornisce il token di accesso dell'app emesso da Twenty per il tuo componente:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_API_URL` e `TWENTY_APP_ACCESS_TOKEN` vengono inseriti automaticamente — vedi [Variabili dellapplicazione](#application-variables). Poiché le variabili di applicazione segrete non vengono mai esposte ai front component, mantieni le chiavi API e altra logica sensibile allinterno della funzione logica, non nel front component.
</Note>
### Riferimento a RestApiClient
Importa `RestApiClient` da `twenty-client-sdk/rest`. Appartiene alla stessa famiglia di client di `CoreApiClient` e `MetadataApiClient`, ma si rivolge alle route HTTP della tua app invece della GraphQL API.
| Metodo | Descrizione |
| --------------------------------- | -------------------------------------------- |
| `get(path, options?)` | Invia una richiesta `GET` |
| `post(path, body?, options?)` | Invia una richiesta `POST` |
| `put(path, body?, options?)` | Invia una richiesta `PUT` |
| `patch(path, body?, options?)` | Invia una richiesta `PATCH` |
| `delete(path, options?)` | Invia una richiesta `DELETE` |
| `request(method, path, options?)` | Richiesta generica con qualsiasi metodo HTTP |
`options` accetta `headers`, `query` (un record di parametri della query string; i valori nullish vengono ignorati) e un `AbortSignal` tramite `signal`. Un oggetto `body` non-`FormData` viene serializzato automaticamente in JSON. In caso di `401`, il client aggiorna il token di accesso una volta tramite l'host e ritenta la richiesta.
L'URL di base e il token vengono risolti dall'ambiente per impostazione predefinita. Passa gli override al costruttore quando necessario — per esempio nei test:
```ts
const client = new RestApiClient({
baseUrl: 'https://api.example.com',
token: 'my-token',
});
```
Le richieste non riuscite generano un `RestApiClientError` che espone `status`, `statusText`, `url` e il `body` analizzato:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const prs = await client.get('/s/github/fetch-prs', {
query: { state: 'open' },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## Accesso al contesto di runtime
All'interno del tuo componente, usa gli hook dell'SDK per accedere all'utente corrente, al record e all'istanza del componente:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useRecordId,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const recordId = useRecordId();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
Hook disponibili:
| Hook | Restituisce | Descrizione |
| --------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
| `useUserId()` | `string` o `null` | L'ID dell'utente corrente |
| `useSelectedRecordIds()` | `string[]` | Tutti gli ID dei record selezionati (array vuoto se nessuno è selezionato) |
| `useRecordId()` | `string` o `null` | **Deprecato.** Usa `useSelectedRecordIds()` al suo posto |
| `useFrontComponentId()` | `string` | L'ID di questa istanza di componente |
| `useColorScheme()` | `'light'` o `'dark'` | Lo schema di colori attivo dell'interfaccia utente dell'host (`System` è già stato risolto) |
| `useFrontComponentExecutionContext(selector)` | varia | Accedi all'intero contesto di esecuzione con una funzione selettore |
## Variabili dell'applicazione
Le variabili dell'applicazione definite in [`defineApplication()`](/l/it/developers/extend/apps/config/application) con `isSecret: false` sono disponibili all'interno dei componenti front-end tramite l'utility `getApplicationVariable`:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Le variabili segrete (`isSecret: true`) **non** sono esposte ai componenti front-end. Sono disponibili solo nelle [funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions), che vengono eseguite lato server. Questo impedisce che valori sensibili come le chiavi API vengano inviati al browser.
</Warning>
Le seguenti variabili di sistema sono sempre disponibili tramite `process.env`:
| Variabile | Descrizione |
| ------------------------- | ---------------------------------------------------------------- |
| `TWENTY_API_URL` | URL di base delle API di Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token di breve durata con ambito limitato al ruolo della tua app |
## API di comunicazione con l'host
I componenti front-end possono attivare navigazione, modali e notifiche utilizzando funzioni da `twenty-sdk`:
| Funzione | Descrizione |
| ----------------------------------------------- | ------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Naviga a una pagina dell'app |
| `openSidePanelPage(params)` | Apri un pannello laterale |
| `closeSidePanel()` | Chiudi il pannello laterale |
| `openCommandConfirmationModal(params)` | Mostra una finestra di conferma |
| `enqueueSnackbar(params)` | Mostra una notifica toast |
| `unmountFrontComponent()` | Smonta il componente |
| `updateProgress(progress)` | Aggiorna un indicatore di avanzamento |
Ecco un esempio che usa l'API host per mostrare una snackbar e chiudere il pannello laterale dopo il completamento di un'azione:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Lavorare con più record
Usa `useSelectedRecordIds()` per gestire più record selezionati. Questo è utile per operazioni in blocco:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## Asset pubblici
I componenti front-end possono accedere ai file dalla directory `public/` dell'app utilizzando `getPublicAssetUrl`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
Vedi la [sezione sugli asset pubblici](/l/it/developers/extend/apps/config/public-assets) per i dettagli.
## Stile
I componenti front-end supportano diversi approcci di styling. Puoi usare:
* **Stili inline** — `style={{ color: 'red' }}`
* **Componenti Twenty UI** — importali da `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar e altro)
* **Emotion** — CSS-in-JS con `@emotion/react`
* **Styled-components** — pattern `styled.div`
* **Tailwind CSS** — classi di utilità
* **Qualsiasi libreria CSS-in-JS** compatibile con React
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
@@ -0,0 +1,44 @@
---
title: Voci del menu di navigazione
description: Aggiungi voci personalizzate alla barra laterale dellarea di lavoro — collegamenti a viste salvate o URL esterni.
icon: bars
---
Una **voce del menu di navigazione** è un elemento nella barra laterale sinistra. Usa `defineNavigationMenuItem()` per distribuire collegamenti personalizzati nella barra laterale — in genere uno per ogni [vista](/l/it/developers/extend/apps/layout/views) che distribuisci — o per puntare a URL esterni.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## Punti chiave
* `type` determina a cosa collega la voce di menu. Ogni tipo è associato a uno specifico campo identificatore:
| Tipo | Cosa fa | Campo obbligatorio |
| ------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Apre una vista salvata | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Apre un URL esterno | `link` |
| `NavigationMenuItemType.FOLDER` | Raggruppa gli elementi nidificati sotto unetichetta | `name` (e gli elementi figli fanno riferimento alla cartella tramite `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Apre la pagina indice predefinita di un oggetto | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Apre un layout di pagina autonomo | `pageLayoutUniversalIdentifier` |
* `position` controlla lordinamento nella barra laterale.
* `icon` e `color` sono opzionali e personalizzano laspetto della voce.
* `folderUniversalIdentifier` è inoltre disponibile su qualsiasi elemento per annidarlo allinterno di un genitore di tipo `FOLDER`.
<Note>
**Problema comune:** creare un oggetto senza una vista associata e una voce del menu di navigazione rende quelloggetto invisibile per gli utenti. A meno che non si tratti di un oggetto tecnico/interno, ogni oggetto personalizzato dovrebbe avere una vista predefinita *e* una voce nella barra laterale che punti ad essa.
</Note>
@@ -0,0 +1,56 @@
---
title: Panoramica
description: "Inserisci la tua app all'interno dell'interfaccia utente di Twenty: voci della barra laterale, viste salvate, schede nella pagina di dettaglio dei record e componenti React isolati."
icon: table-columns
---
Il **livello di layout** di un'app Twenty è tutto ciò che l'utente vede: dove l'app viene visualizzata nella barra laterale, quali viste elenco fornisce, come sono organizzate le sue pagine di dettaglio dei record e quali componenti React personalizzati vengono renderizzati all'interno di quelle pagine.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## In questa sezione
<CardGroup cols={2}>
<Card title="Viste" icon="list" href="/l/it/developers/extend/apps/layout/views">
`defineView` — configurazioni di elenco salvate: colonne visibili, filtri, gruppi.
</Card>
<Card title="Voci del menu di navigazione" icon="bars" href="/l/it/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — voci della barra laterale che puntano a viste o URL esterni.
</Card>
<Card title="Layout Pagina" icon="table-columns" href="/l/it/developers/extend/apps/layout/page-layouts">
`definePageLayout` e `definePageLayoutTab` — schede e widget nella pagina di dettaglio di un record.
</Card>
<Card title="Componenti front-end" icon="window-maximize" href="/l/it/developers/extend/apps/layout/front-components">
`defineFrontComponent` — componenti React isolati che vengono renderizzati all'interno di Twenty.
</Card>
<Card title="Voci del menu comandi" icon="terminal" href="/l/it/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — registra componenti front-end come voci Cmd+K e azioni rapide.
</Card>
</CardGroup>
## Dove viene visualizzata l'app
| Superficie | Cosa controlla | Entità |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Barra laterale** | Una voce personalizzata che collega a una vista salvata o a un URL esterno | `defineNavigationMenuItem` |
| **Elenco record** | Una configurazione salvata per un oggetto — colonne visibili, ordine, filtri, gruppi | `defineView` |
| **Pagina di dettaglio del record** | Le schede e i widget nella pagina di un record (del tuo oggetto o di un oggetto standard) | `definePageLayout`, `definePageLayoutTab` |
| **All'interno di uno qualsiasi dei precedenti** | Un widget React personalizzato: pulsanti, moduli, dashboard, integrazioni | `defineFrontComponent` |
| **Menu comandi (Cmd+K)** | Un'azione rapida fissata o un comando nascosto | `defineCommandMenuItem` |
I componenti front-end vengono eseguiti all'interno di un Web Worker isolato usando Remote DOM: vengono renderizzati in modo nativo nella pagina (non all'interno di un iframe), ma non possono accedere direttamente alla pagina host o al DOM. La comunicazione con Twenty avviene tramite un'API host basata sul passaggio di messaggi.
@@ -0,0 +1,132 @@
---
title: Layout Pagina
description: Personalizza le pagine di dettaglio dei record — schede, widget e dove vengono renderizzati i front components — usando `definePageLayout` e `definePageLayoutTab`.
icon: table-columns
---
Un **layout di pagina** controlla come è organizzata la pagina di dettaglio di un record: quali schede compaiono e quali widget contengono. Usa `definePageLayout()` per dichiarare un layout per un oggetto di tua proprietà, oppure `definePageLayoutTab()` per aggiungere una singola scheda a un layout già esistente (tuo o standard di Twenty).
| Caso d'uso | Entità |
| ---------------------------------------------------------------------------------- | --------------------- |
| Definire lintero layout per la pagina di un record su un oggetto di tua proprietà | `definePageLayout` |
| Aggiungere una scheda a un layout esistente (un tuo oggetto o uno standard) | `definePageLayoutTab` |
## definePageLayout
Usa questo quando possiedi lintera pagina di dettaglio — in genere per un oggetto personalizzato che hai definito tu stesso.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### Punti chiave
* `type` è in genere `'RECORD_PAGE'` per personalizzare la vista dei dettagli di un oggetto specifico.
* `objectUniversalIdentifier` specifica a quale oggetto si applica questo layout.
* Ogni `tab` definisce una sezione della pagina con un `title`, `position` e `layoutMode` (`CANVAS` per il layout libero).
* Ogni `widget` all'interno di una scheda può renderizzare un [front component](/l/it/developers/extend/apps/layout/front-components), un elenco di relazioni o altri tipi di widget integrati.
* `position` sulle schede controlla il loro ordine. Usa valori più alti (ad es., 50) per posizionare le schede personalizzate dopo quelle integrate.
## definePageLayoutTab
Usa questo quando vuoi solo **aggiungere** una scheda a un layout esistente — per esempio, una scheda di analisi sulla pagina Company standard, o una scheda di riepilogo AI collegata al layout del tuo oggetto.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Punti chiave
* `pageLayoutUniversalIdentifier` è **obbligatorio** e deve puntare a un layout di pagina che esiste già al momento dellinstallazione — oppure un layout standard di Twenty o uno definito dalla tua app. I riferimenti tra app a layout di proprietà di unaltra app installata non sono attualmente supportati. Quando il layout di pagina padre manca, linstallazione non va a buon fine e restituisce un chiaro errore di validazione.
* Per i layout standard di Twenty, importa gli identificatori da `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Ogni voce di layout espone anche i propri `tabs` e i relativi `widgets`, così puoi fare riferimento a qualsiasi livello:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
```
È disponibile anche un alias breve `STANDARD_PAGE_LAYOUT`:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* I `widgets` sono limitati solo a questa scheda — fanno riferimento a [front components](/l/it/developers/extend/apps/layout/front-components), viste, ecc. esattamente come i widget definiti inline in `definePageLayout`.
* `position` controlla l'ordinamento rispetto alle schede esistenti nel layout di destinazione. Scegli un valore che collochi la tua scheda dove desideri rispetto alle schede integrate.
* Usa questo invece di `definePageLayout` quando vuoi solo aggiungere a un layout esistente. Usa `definePageLayout` quando possiedi lintero layout.
@@ -0,0 +1,97 @@
---
title: Viste
description: Distribuisci viste preconfigurate salvate — ordine delle colonne, filtri, gruppi — per gli oggetti nella tua app.
icon: list
---
Una **vista** è una configurazione salvata per come vengono visualizzati i record di un oggetto: quali campi compaiono, il loro ordine, se sono visibili e gli eventuali filtri o gruppi applicati. Usa `defineView()` per distribuire viste preconfigurate con la tua app — in genere una vista indice predefinita per ogni oggetto personalizzato che crei.
```ts src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## Punti chiave
* `objectUniversalIdentifier` specifica a quale oggetto si applica questa vista. Può essere un oggetto personalizzato che hai definito o un oggetto Twenty standard.
* `key` determina il tipo di vista — `ViewKey.INDEX` è la vista elenco principale per l'oggetto.
* `fields` controlla quali colonne compaiono e in quale ordine. Ogni campo fa riferimento a un `fieldMetadataUniversalIdentifier`.
* Puoi anche definire `filters`, `filterGroups`, `groups` e `fieldGroups` per configurazioni più avanzate.
* `position` controlla l'ordinamento quando esistono più viste per lo stesso oggetto.
## Filtri
Una vista può essere fornita con filtri preapplicati. Ogni filtro ha tre coordinate: il **campo** che viene filtrato, l'**operando** (come confrontare) e il **valore** (con cosa confrontare). Tutti e tre devono allinearsi: l'uso di un operando che non si applica a un tipo di campo verrà rifiutato al momento della sincronizzazione.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operatori supportati per tipo di campo
| Tipo di campo | Operandi supportati |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> I tipi di campo con nomi simili possono usare operandi completamente diversi — `SELECT` e `MULTI_SELECT` sono un caso comune.
### Forma del valore per operando
Il campo `value` è sempre un valore serializzabile in JSON, ma la forma prevista dipende dall'operando:
| Famiglia di operandi | Forma del valore | Esempio |
| ----------------------------------------------------- | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` su `SELECT` | array di chiavi di opzione (stringhe) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` su `MULTI_SELECT` | array di chiavi di opzione (stringhe) | `['TAG_A']` |
| `IS`, `IS_NOT` su `RELATION` | array di ID di record (uuid) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` su campi testuali | stringa | `'acme'` |
| `IS`, `IS_NOT` su `NUMBER` | stringa (il valore) | `'5'` |
| `IS` su `RATING` / `UUID` | stringa (il valore) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | stringa (il limite) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` su `DATE` / `DATE_TIME` | stringa ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | stringa vuota | `''` |
| `IS` su `BOOLEAN` | `'true'` o `'false'` | `'true'` |
## Come le viste vengono visualizzate nell'interfaccia utente
Una vista da sola non è raggiungibile dalla barra laterale. Per farla apparire lì, collegala a una [voce del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items) di tipo `VIEW` che punti all'`universalIdentifier` della vista. Questo è il pattern canonico: ogni oggetto personalizzato in genere distribuisce una vista predefinita + una voce nella barra laterale che la apre.
@@ -0,0 +1,192 @@
---
title: Connessioni
description: Consenti alla tua app di agire per conto di un utente nei servizi di terze parti tramite OAuth.
icon: plug
---
Le connessioni sono credenziali che un utente detiene per un servizio esterno (Linear, GitHub, Slack, ...). La tua app dichiara **come** vengono ottenute tali credenziali — un **provider di connessione** — e le utilizza in fase di esecuzione per effettuare chiamate autenticate all'API di terze parti.
Oggi è supportato solo OAuth 2.0. Tipi di credenziali futuri (token di accesso personali, chiavi API, autenticazione di base) si integreranno nella stessa interfaccia — le app che già usano `defineConnectionProvider({ type: 'oauth', ... })` non dovranno migrare.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Dichiara come vengono ottenute le connessioni della tua app">
Un provider di connessione descrive l'handshake OAuth di cui la tua app ha bisogno. L'utente fa clic su "Aggiungi connessione" nelle impostazioni della tua app, completa la schermata di consenso del provider e viene creata una riga `ConnectedAccount` nel suo spazio di lavoro.
Una configurazione funzionante richiede **due file** — il provider di connessione e una dichiarazione `serverVariables` corrispondente su `defineApplication` che contiene le credenziali client OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Punti chiave:
* `name` è la stringa identificativa univoca usata in `listConnections({ providerName })` (kebab-case, deve corrispondere a `^[a-z][a-z0-9-]*$`).
* `displayName` viene mostrato nella scheda delle impostazioni per app e nell'elenco degli strumenti AI.
* `clientIdVariable` / `clientSecretVariable` sono **nomi**, non valori — devono corrispondere alle chiavi dichiarate in `defineApplication.serverVariables`. I `client_id` e `client_secret` effettivi vengono inseriti dall'amministratore del server tramite l'interfaccia di registrazione dell'app e non vengono mai aggiunti al tuo repository.
* Usa `serverVariables` (non `applicationVariables`) — le credenziali OAuth sono a livello di server e c'è una sola app OAuth per ogni server Twenty.
* Finché entrambe le `serverVariables` non sono compilate, la scheda delle impostazioni per app mostra un suggerimento "richiede l'amministratore del server" e il pulsante "Aggiungi connessione" è disattivato.
* `type: 'oauth'` è l'unico valore supportato oggi. Il discriminatore è compatibile con versioni future: tipi futuri (`'pat'`, `'api-key'`, ...) aggiungeranno nuovi blocchi di sotto-configurazione accanto a `oauth`.
L'URL di callback OAuth che il tuo provider deve inserire nella whitelist è:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Usa le connessioni da una funzione di logica">
All'interno di un gestore di funzione di logica, `listConnections({ providerName })` restituisce le righe `ConnectedAccount` di questa app per il provider indicato, con token di accesso aggiornati.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Ogni connessione ha:
| Campo | Descrizione |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID di riga univoco; passalo a `getConnection(id)` per recuperarne una singola connessione |
| `visibility` | `'user'` (privata per un membro dello spazio di lavoro) oppure `'workspace'` (condivisa con tutti i membri) |
| `scopes` | Autorizzazioni OAuth concesse dal provider a monte (distinte da `visibility` — non sono correlate) |
| `userWorkspaceId` | L'id userWorkspace del proprietario — utile per selezionare "la connessione dell'utente della richiesta" nei trigger di route HTTP |
| `accessToken` | Token di accesso OAuth aggiornato (rinnovato automaticamente se scaduto) |
| `name` / `handle` | Il nome visualizzato della connessione (derivato automaticamente al callback OAuth, rinominabile dall'utente) |
| `authFailedAt` | Impostato quando l'aggiornamento più recente non è riuscito; l'utente deve riconnettersi |
Punti chiave:
* Passa `{ providerName }` per filtrare per provider; omettilo per ottenere tutte le connessioni possedute da questa app su tutti i provider.
* Il server rinnova in modo trasparente il token di accesso prima di restituire la risposta. Il tuo handler vede sempre un token utilizzabile (oppure `authFailedAt` impostato).
* `getConnection(id)` è l'equivalente a singola riga.
</Accordion>
<Accordion title="Visibilità per utente vs condivisa nello spazio di lavoro" description="Come gli utenti scelgono tra credenziali private e condivise">
Quando un utente fa clic su "Aggiungi connessione", viene richiesto di scegliere una visibilità:
* **Solo per me** — la credenziale è privata per l'utente che effettua la connessione. Qualsiasi funzione di logica chiamata per loro conto (trigger di route HTTP con `isAuthRequired: true`) la vede; i trigger cron e gli eventi del database no.
* **Condivisa nello spazio di lavoro** — qualsiasi membro dello spazio di lavoro può usare la credenziale. Anche i trigger cron/database la vedono, poiché non hanno un utente della richiesta.
Usa quella giusta per ciascun handler:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Sono consentite più connessioni per (utente, provider), quindi lo stesso utente può avere affiancate "Linear personale" e "Linear di lavoro".
</Accordion>
<Accordion title="Configurazione una tantum del provider" description="Registra la tua app OAuth presso il servizio di terze parti">
Per ciascun provider di connessione, l'amministratore del server deve prima registrare un'app OAuth presso il servizio di terze parti.
1. Vai alle impostazioni sviluppatore del provider (ad es. https://linear.app/settings/api/applications/new).
2. Imposta la **Redirect URI** su `\<SERVER_URL>/auth/apps/callback`.
3. Copia il **Client ID** e il **Client Secret** generati.
4. Apri l'app installata in Twenty come amministratore del server → imposta i valori nelle corrispondenti `serverVariables`.
5. I membri dello spazio di lavoro possono quindi aggiungere connessioni dalla sezione **Connessioni** dell'app.
</Accordion>
</AccordionGroup>
@@ -0,0 +1,514 @@
---
title: Funzioni logiche
description: Definisci funzioni TypeScript lato server con trigger HTTP, cron e trigger di eventi del database.
icon: bolt
---
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
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 **sotto l'endpoint `/s/`**:
> ad es. `path: '/post-card/create'` è invocabile su `https://your-twenty-server.com/s/post-card/create`
<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.*`
<Note>
Puoi anche eseguire manualmente una funzione utilizzando la CLI:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Puoi osservare i log con:
```bash filename="Terminal"
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
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
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
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
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>
#### 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 sullarea di lavoro e sulloggetto di origine con levento a livello di record.
```ts
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
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
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
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Esempio di evento "destroyed":
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Esporre una funzione come strumento di IA o come azione del flusso di lavoro
Le funzioni logiche possono essere esposte su due superfici, ciascuna con il proprio trigger:
* **`toolTriggerSettings`** — rende la funzione individuabile dalle funzionalità di IA di Twenty (chat, MCP, function calling). Usa lo standard JSON Schema, il formato che gli LLM comprendono nativamente.
* **`workflowActionTriggerSettings`** — fa apparire la funzione come un passaggio nel builder visivo dei flussi di lavoro. Usa il ricco `InputSchema` di Twenty affinché il builder possa visualizzare correttamente editor di campi, selettori di variabili ed etichette.
Una funzione può optare per uno, l'altro o entrambi. Si affiancano a `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — stesso schema, stessa struttura.
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Punti chiave:
* Una funzione può combinare le superfici — dichiara sia `toolTriggerSettings` sia `workflowActionTriggerSettings` per esporla in chat E nel builder dei flussi di lavoro.
* `toolTriggerSettings.inputSchema` e `workflowActionTriggerSettings.inputSchema` sono entrambi opzionali. Se omessi, il builder del manifest li deduce dal codice sorgente dell'handler (JSON Schema per lo strumento di IA, `InputSchema` di Twenty per l'azione del flusso di lavoro). Forniscine uno esplicitamente quando desideri una tipizzazione più ricca — ad esempio, con campi compatibili con `FieldMetadataType` come `CURRENCY` o `RELATION` per il builder dei flussi di lavoro, oppure con campi `description` che l'agente di IA può leggere:
```ts
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'],
},
},
});
```
<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>
**Hook di installazione** — i gestori di pre-installazione e post-installazione — condividono questo runtime, ma sono dichiarati con le proprie funzioni di definizione e non accettano impostazioni dei trigger. Consulta [Hook di installazione](/l/it/developers/extend/apps/config/install-hooks) per `definePreInstallLogicFunction` e `definePostInstallLogicFunction`.
</Note>
## 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
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
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.
```ts
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
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 con ambito al ruolo funzione predefinito della tua applicazione
Non è **necessario** passarle ai client — vengono lette automaticamente da `process.env`. I permessi della chiave API sono determinati dal ruolo dichiarato con `defineApplicationRole()` (o referenziato tramite `defaultRoleUniversalIdentifier` in `application-config.ts`).
</Note>
@@ -0,0 +1,55 @@
---
title: Panoramica
description: TypeScript lato server che viene eseguito all'interno di Twenty — attivato da route HTTP, pianificazioni cron, eventi del database, strumenti di AI o azioni di workflow.
icon: bolt
---
Il **livello logico** di un'app Twenty è il codice che *viene eseguito* — handler TypeScript lato server che reagiscono a richieste HTTP, pianificazioni cron e modifiche ai record; skill e agenti di AI che vivono all'interno dello spazio di lavoro; e connessioni OAuth che permettono alle tue funzioni di agire per conto dell'utente in servizi di terze parti.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## In questa sezione
<CardGroup cols={2}>
<Card title="Funzioni logiche" icon="bolt" href="/l/it/developers/extend/apps/logic/logic-functions">
Il blocco di base — tipi di trigger, payload e client API tipizzato.
</Card>
<Card title="Skill e agenti" icon="robot" href="/l/it/developers/extend/apps/logic/skills-and-agents">
Istruzioni riutilizzabili per agenti AI e assistenti con prompt di sistema personalizzati.
</Card>
<Card title="Connessioni" icon="plug" href="/l/it/developers/extend/apps/logic/connections">
Credenziali OAuth che la tua app detiene per servizi di terze parti — Linear, GitHub, Slack e altri.
</Card>
</CardGroup>
## Tipi di trigger in sintesi
Una funzione logica sceglie uno o più trigger — ogni voce qui sotto è un campo separato su `defineLogicFunction()`:
| Scatenante | Quando viene eseguito | Impostazione |
| ----------------------- | --------------------------------------------------------------------- | ------------------------------- |
| **Route HTTP** | Una richiesta raggiunge il tuo endpoint `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | Viene soddisfatta un'espressione CRON | `cronTriggerSettings` |
| **Evento database** | Un record dello spazio di lavoro viene creato, aggiornato o eliminato | `databaseEventTriggerSettings` |
| **Strumento AI** | Una funzionalità AI di Twenty decide di chiamare la tua funzione | `toolTriggerSettings` |
| **Azione del workflow** | Uno step di workflow richiama la tua funzione | `workflowActionTriggerSettings` |
Le funzioni vengono eseguite in sandbox in processi Node.js isolati e accedono allo spazio di lavoro tramite un client API tipizzato, limitato al ruolo dichiarato su [`defineApplication()`](/l/it/developers/extend/apps/config/application).
<Note>
**Hook di installazione** — codice che viene eseguito prima o dopo l'installazione — condividono questo runtime ma usano le proprie funzioni define e risiedono in [Config → Install Hooks](/l/it/developers/extend/apps/config/install-hooks).
</Note>
@@ -0,0 +1,138 @@
---
title: Skill e agenti
description: Definisci skill e agenti di IA per la tua app.
icon: robot
---
<Warning>
Le skill e gli agenti sono attualmente in fase alfa. La funzionalità funziona ma è ancora in evoluzione.
</Warning>
Le app possono definire capacità di IA che risiedono all'interno dello spazio di lavoro — istruzioni di skill riutilizzabili e agenti con prompt di sistema personalizzati.
<AccordionGroup>
<Accordion title="defineSkill" description="Definisci le skill degli agenti IA">
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:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
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.
</Accordion>
<Accordion title="defineAgent" description="Definisci agenti IA con prompt personalizzati">
Gli agenti sono assistenti IA che vivono all'interno del tuo spazio di lavoro. Usa `defineAgent()` per creare agenti con un prompt di sistema personalizzato:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Punti chiave:
* `name` è la stringa identificativa univoca dell'agente (kebab-case consigliato).
* `label` è il nome visualizzato nell'UI.
* `prompt` è il prompt di sistema che definisce il comportamento dell'agente.
* `description` (opzionale) fornisce contesto su ciò che fa l'agente.
* `icon` (opzionale) imposta l'icona visualizzata nell'UI.
* `modelId` (opzionale) sostituisce il modello di IA predefinito utilizzato dall'agente.
* `responseFormat` (facoltativo) controlla la forma dell'output dell'agente. Per il testo in formato libero, il valore predefinito è `{ type: 'text' }`. Usa `{ type: 'json', schema }` per forzare un output JSON strutturato.
Per impostazione predefinita, un agente restituisce testo in formato libero. Per ottenere un output strutturato, imposta `responseFormat` su `{ type: 'json' }` e fornisci uno `schema`:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Note sullo schema:
* Lo schema è un oggetto piatto: il `type` di ogni proprietà deve essere un primitivo (`string`, `number` o `boolean`). Gli oggetti annidati e gli array non sono supportati.
* `description` (facoltativo) su ogni proprietà guida il modello su cosa inserire lì.
* `required` (facoltativo) elenca le proprietà che il modello deve sempre restituire.
* `additionalProperties: false` (facoltativo) vieta qualsiasi proprietà non dichiarata in `properties`.
</Accordion>
<Accordion title="runAgent" description="Esegui un agente da una funzione di logica">
`runAgent()` permette a una funzione di logica di eseguire uno degli agenti della tua app (con le sue skill e i suoi strumenti). Identifica l'agente tramite l'`universalIdentifier` che hai passato a `defineAgent()`:
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Punti chiave:
* L'agente viene eseguito in modo **sincrono** e può leggere/aggiornare direttamente i record tramite i propri strumenti — `runAgent()` viene risolta una volta completata l'esecuzione.
* Un'app può eseguire solo i propri agenti.
* Il [ruolo predefinito](/l/it/developers/extend/apps/config/roles) dell'app deve concedere il flag di autorizzazione `AI` — aggiungi `SystemPermissionFlag.AI` ai suoi `permissionFlagUniversalIdentifiers` (oppure imposta `canAccessAllTools: true`).
In mancanza di ciò, `runAgent()` non riesce con un errore di autorizzazione.
* Imposta un valore generoso per `timeoutSeconds` sulla funzione di logica — l'esecuzione degli agenti può richiedere diversi secondi.
* `success` è `true` e `result` è non nullo quando l'esecuzione viene completata; in caso di errore `success` è `false`, `result` è `null` ed `error` contiene il motivo (ad esempio, quando lo spazio di lavoro esaurisce i crediti AI durante l'esecuzione).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Evita i loop:** se chiami `runAgent()` da un trigger di evento del database `*.updated` e l'agente aggiorna lo stesso record, limita il trigger con `updatedFields` a un campo che l'agente non scrive mai (ad esempio l'URL di origine), oppure controlla che almeno uno dei campi di destinazione sia ancora vuoto prima di chiamare `runAgent()`.
</Warning>
</Accordion>
</AccordionGroup>
@@ -0,0 +1,105 @@
---
title: CLI
description: Comandi di `yarn twenty` per eseguire funzioni, eseguire lo streaming dei log, gestire le installazioni delle app e passare da un remoto all'altro.
icon: terminale
---
Oltre a `dev`, `dev:build`, `dev:add` e `dev:typecheck`, la CLI `yarn twenty` fornisce comandi per eseguire funzioni, visualizzare i log e gestire le installazioni delle app.
## Esecuzione delle funzioni (`yarn twenty dev:function:exec`)
Esegui manualmente una funzione logica senza attivarla tramite HTTP, cron o evento del database:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty dev:function:exec --postInstall
```
## Visualizzazione dei log delle funzioni (`yarn twenty dev:function:logs`)
Esegui lo streaming dei log di esecuzione per le funzioni logiche della tua app:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Questo è diverso da `yarn twenty docker:logs`, che mostra i log del container Docker. `yarn twenty dev:function:logs` mostra i log di esecuzione delle funzioni della tua app dal server Twenty.
</Note>
## Generare il client tipizzato (`yarn twenty dev:generate-client`)
Rigenera il client API tipizzato (`twenty-client-sdk`) dallo schema del remoto attivo, senza eseguire la build né sincronizzare un'app. Usalo per ottenere un client tipizzato in qualsiasi progetto — ad esempio un servizio backend che vive in un repository separato — che comunica con la tua istanza Twenty:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Quindi importa il client nel tuo codice:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Esegui nuovamente il comando ogni volta che il tuo modello di dati cambia, per aggiornare i tipi generati.
<Note>
Il client viene generato all'interno di `node_modules`, quindi non viene inserito nel commit insieme al tuo codice. Esegui `yarn twenty dev:generate-client` dopo ogni installazione (ad esempio in uno script `postinstall` o nella CI).
</Note>
## Disinstallazione di un'app (`yarn twenty app:uninstall`)
Rimuovi la tua app dallo spazio di lavoro attivo:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## Gestione dei remoti
Un **remoto** è un server Twenty a cui la tua app si connette. Durante la configurazione, lo strumento di scaffolding ne crea uno automaticamente per te. Puoi aggiungere altri remoti o passare da uno all'altro in qualsiasi momento.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
```
Le tue credenziali sono archiviate in `~/.twenty/config.json`.
@@ -0,0 +1,32 @@
---
title: Panoramica
description: Crea, testa e distribuisci la tua app — comandi CLI, test di integrazione, CI e pubblicazione su un server o su npm.
icon: rocket
---
Il **livello operativo** è tutto ciò che fai *alla* tua app piuttosto che *con* essa: eseguire comandi CLI, eseguire test di integrazione contro un server Twenty reale, configurare la CI e distribuire le release — sia come tarball distribuito su un singolo server, sia come pacchetto npm elencato nel marketplace.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## In questa sezione
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/it/developers/extend/apps/operations/cli">
Riferimento a `yarn twenty` — exec, logs, uninstall, remotes.
</Card>
<Card title="Sincronizzazione e ripristino" icon="bussola" href="/l/it/developers/extend/apps/operations/sync-and-recovery">
Quale comando usare e quando, lettura del diff di sincronizzazione e una scaletta di ripristino.
</Card>
<Card title="Testing" icon="flask" href="/l/it/developers/extend/apps/operations/testing">
Configurazione di Vitest, test di integrazione, controllo dei tipi, workflow di CI.
</Card>
<Card title="Pubblicazione" icon="upload" href="/l/it/developers/extend/apps/operations/publishing">
Crea, distribuisci un tarball, pubblica su npm, installa.
</Card>
</CardGroup>
@@ -0,0 +1,294 @@
---
title: Pubblicazione
icon: carica
description: Distribuisci la tua app Twenty nel marketplace oppure distribuiscila internamente.
---
## Panoramica
Una volta che la tua app è stata [compilata e testata localmente](/l/it/developers/extend/apps/getting-started/concepts), hai due modalità per distribuirla:
* **Distribuisci un tarball** — carica la tua app direttamente su un server Twenty specifico per uso interno o privato.
* **Pubblica su npm** — elenca la tua app nel marketplace di Twenty affinché qualsiasi spazio di lavoro possa scoprirla e installarla.
Entrambi i percorsi partono dalla stessa fase di **build**.
## Compilazione della tua app
Esegui il comando di build per compilare la tua app e generare un `manifest.json` pronto per la distribuzione:
```bash filename="Terminal"
yarn twenty dev:build
```
Questo compila i sorgenti TypeScript, transpila le funzioni di logica e i componenti front-end e scrive tutto in `.twenty/output/`. Aggiungi `--tarball` per produrre anche un pacchetto `.tgz` per la distribuzione manuale o per il comando di publish.
## Distribuzione su un server (tarball)
Per le app che non vuoi rendere pubbliche — strumenti proprietari, integrazioni solo aziendali o build sperimentali — puoi distribuire un tarball direttamente su un server Twenty.
### Prerequisiti
Prima della distribuzione, ti serve un remote configurato che punti al server di destinazione. I remote memorizzano localmente l'URL del server e le credenziali di autenticazione in `~/.twenty/config.json`.
Aggiungi un remote:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Distribuzione
Compila e carica la tua app sul server in un solo passaggio:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Condivisione di un'app distribuita
<Warning>
La condivisione di app private (tarball) tra spazi di lavoro è una funzionalità **Enterprise**. La scheda **Distribuzione** mostrerà un invito all'aggiornamento al posto dei controlli di condivisione finché il tuo spazio di lavoro non dispone di una chiave Enterprise valida. Vedi [Impostazioni > Pannello di amministrazione > Enterprise](/settings/admin-panel#enterprise) per attivarla.
</Warning>
Le app in formato tarball non sono elencate nel marketplace pubblico, quindi altri spazi di lavoro sullo stesso server non le troveranno navigando. Una volta che il tuo spazio di lavoro è sul piano Enterprise, puoi condividere un'app distribuita in questo modo:
1. Vai su **Impostazioni > Applicazioni > Registrazioni** e apri la tua app
2. Nella scheda **Distribuzione**, fai clic su **Copia link di condivisione**
3. Condividi questo link con utenti su altri spazi di lavoro — li porterà direttamente alla pagina di installazione dell'app
Il link di condivisione utilizza l'URL di base del server (senza alcun sottodominio dello spazio di lavoro) così funziona per qualsiasi spazio di lavoro sul server.
### Gestione delle versioni
Quando si aggiorna un'app in formato tarball già distribuita, il server richiede che la `version` in `package.json` sia **strettamente superiore** (per l'ordinamento [semver](https://semver.org)) rispetto alla versione attualmente distribuita. Eseguire nuovamente il deploy della stessa versione, o pubblicarne una inferiore, viene rifiutato prima che il tarball venga archiviato — vedrai un errore `VERSION_ALREADY_EXISTS` nella CLI.
Per rilasciare un aggiornamento:
1. Incrementa il campo `version` nel tuo `package.json` (ad es. `1.2.3` → `1.2.4`, `1.3.0` o `2.0.0`).
2. Esegui `yarn twenty app:publish --private` (oppure `yarn twenty app:publish --private --remote production`)
3. Gli spazi di lavoro che hanno l'app installata vedranno l'aggiornamento disponibile nelle proprie impostazioni
<Note>
I tag di pre-release funzionano come previsto: incrementare `1.0.0-rc.1` → `1.0.0-rc.2` è consentito e una release finale come `1.0.0` viene correttamente riconosciuta come superiore a `1.0.0-rc.5`. La versione in `package.json` deve essere essa stessa una stringa semver valida.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilità della versione del server
Se la tua app utilizza una funzionalità introdotta in una specifica versione del server Twenty (ad esempio, i provider OAuth aggiunti nella v2.3.0), dovresti dichiarare la versione minima del server richiesta dalla tua app utilizzando il campo `engines.twenty` in `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Il valore è un [intervallo semver](https://github.com/npm/node-semver#ranges) standard. Modelli comuni:
| Intervallo | Significato |
| ---------------------------------- | -------------------------------------------------------------- |
| `>=2.3.0` | Qualsiasi server a partire dalla 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 o successive, ma precedenti alla prossima versione major |
| `^2.3.0` | Equivale a `>=2.3.0 \<3.0.0` |
**Cosa succede durante il deploy e l'installazione:**
* Se `engines.twenty` è impostato e la versione del server di destinazione non soddisfa l'intervallo, il deploy (caricamento del tarball) o l'installazione vengono rifiutati con un errore `SERVER_VERSION_INCOMPATIBLE` e un messaggio che indica sia l'intervallo richiesto sia la versione effettiva del server.
* Se `engines.twenty` **non è impostato**, l'app è accettata su qualsiasi versione del server (retrocompatibile con le app esistenti).
* Se sul server non è configurata `APP_VERSION`, il controllo viene ignorato.
<Note>
Il server è l'autorità di controllo — convalida `engines.twenty` sia durante il caricamento del tarball sia durante l'installazione nello spazio di lavoro. Se distribuisci un tarball out-of-band o installi dal marketplace, il server continua comunque a imporre la compatibilità.
</Note>
## CI/CD automatizzati (workflow preconfigurati)
Le app generate con `create-twenty-app` includono due workflow di GitHub Actions pronti all'uso, nella cartella `.github/workflows/`. Sono pronti all'esecuzione non appena esegui il push del repository su GitHub — non è necessaria alcuna configurazione aggiuntiva per la CI e la CD richiede solo un singolo secret.
### CI — `ci.yml`
Esegue automaticamente i test di integrazione a ogni push su `main` e sulle pull request.
**Cosa fa:**
1. Esegue il checkout del codice sorgente della tua app.
2. Avvia un'istanza di test isolata di Twenty utilizzando l'azione composita `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (l'equivalente per la CI di `yarn twenty docker:start --test`).
3. Abilita Corepack, configura Node.js dal tuo `.nvmrc` e installa le dipendenze con `yarn install --immutable`.
4. Esegue `yarn test`, passando `TWENTY_API_URL` e `TWENTY_API_KEY` dall'istanza avviata affinché i tuoi test possano comunicare con un server reale.
**Opzioni di configurazione:**
* `TWENTY_VERSION` (variabile di ambiente, predefinito `latest`) — fissa la versione del server Twenty usata nella CI modificando questo valore in `ci.yml`.
* La concorrenza è raggruppata per `github.ref` e annulla le esecuzioni in corso in caso di nuovi push.
Non sono necessari Secrets — l'istanza di test è effimera ed esiste solo per la durata del job.
### CD — `cd.yml`
Esegue il deploy della tua app su un server Twenty configurato a ogni push su `main` e, facoltativamente, da una pull request quando viene applicata l'etichetta `deploy`.
**Cosa fa:**
1. Esegue il checkout della testa della PR (per le PR etichettate) oppure del commit inviato.
2. Esegue `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — l'equivalente per la CI di `yarn twenty app:publish --private`.
3. Esegue `twentyhq/twenty/.github/actions/install-twenty-app@main` in modo che la versione appena distribuita venga installata nello spazio di lavoro di destinazione.
**Configurazione richiesta:**
| Impostazione | Dove | Scopo |
| ----------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (predefinito `http://localhost:3000`) | Il server Twenty su cui effettuare il deploy. Modificalo con l'URL reale del tuo server prima del primo utilizzo. |
| `TWENTY_DEPLOY_API_KEY` | Repository GitHub **Settings → Secrets and variables → Actions** | Chiave API con autorizzazione di deploy sul server di destinazione. |
<Note>
Il valore predefinito di `TWENTY_DEPLOY_URL`, `http://localhost:3000`, è un segnaposto — non raggiungerà alcuna risorsa da un runner ospitato su GitHub. Aggiornalo all'URL pubblico del tuo server (oppure usa un runner self-hosted con accesso di rete) prima di abilitare il CD.
</Note>
**Attivare un deploy di anteprima da una PR:**
Aggiungi l'etichetta `deploy` a una pull request. La condizione `if:` in `cd.yml` eseguirà il job per quella PR utilizzando il commit di testa della PR, permettendoti di convalidare una modifica sul server di destinazione prima del merge.
### Bloccare le azioni riutilizzabili
Entrambi i workflow fanno riferimento ad azioni riutilizzabili a `@main`, quindi gli aggiornamenti delle azioni nel repository `twentyhq/twenty` vengono recepiti automaticamente. Se desideri build deterministiche, sostituisci `@main` con uno SHA di commit o un tag di release in ciascuna riga `uses:`.
## Pubblicazione su npm
La pubblicazione su npm rende la tua app scopribile nel marketplace di Twenty. Qualsiasi spazio di lavoro Twenty può sfogliare, installare e aggiornare le app del marketplace direttamente dall'interfaccia utente.
### Requisiti
* Un account [npm](https://www.npmjs.com)
* La parola chiave `twenty-app` nell'array `keywords` del tuo `package.json` (aggiungila manualmente — non è inclusa per impostazione predefinita nel template `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Metadati del marketplace
La configurazione `defineApplication()` supporta campi opzionali che controllano come la tua app appare nel marketplace. Usa `logoUrl` e `screenshots` per fare riferimento alle immagini nella cartella `public/`:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logoUrl: 'public/logo.png',
screenshots: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Vedi l'[accordion defineApplication](/l/it/developers/extend/apps/config/application#marketplace-metadata) nella pagina Building Apps per l'elenco completo dei campi del marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, ecc.).
#### Dimensioni consigliate degli screenshot
Il marketplace visualizza gli `screenshots` in un contenitore `8:5` fisso (ad esempio, `1600×1000 px`).
<Note>
Gli screenshot di qualsiasi rapporto d'aspetto vengono visualizzati per intero e non vengono mai ritagliati, ma tutto ciò che è significativamente più alto o più stretto di `8:5` mostrerà bande vuote ai lati.
</Note>
### Pubblica
```bash filename="Terminal"
yarn twenty app:publish
```
Per pubblicare con un dist-tag specifico (ad es. `beta` o `next`):
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### Come funziona l'individuazione nel marketplace
Il server Twenty sincronizza il proprio catalogo del marketplace dal registro npm **ogni ora**.
Puoi attivare la sincronizzazione immediatamente invece di aspettare:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
I metadati visualizzati nel marketplace provengono dalla configurazione `defineApplication()` — campi come `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
<Note>
Se la tua app non definisce un `aboutDescription` in `defineApplication()`, il marketplace userà automaticamente il `README.md` del tuo pacchetto su npm come contenuto della pagina Informazioni. Questo significa che puoi mantenere un unico README sia per npm sia per il marketplace di Twenty. Se desideri una descrizione diversa nel marketplace, imposta esplicitamente `aboutDescription`.
</Note>
### Pubblicazione con CI
Usa questo workflow di GitHub Actions per pubblicare automaticamente a ogni release (usa [OIDC](https://docs.npmjs.com/trusted-publishers)):
```yaml filename=".github/workflows/publish.yml"
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty dev:build
- run: npm publish --provenance --access public
working-directory: .twenty/output
```
Per altri sistemi CI (GitLab CI, CircleCI, ecc.), si applicano gli stessi tre comandi: `yarn install`, `yarn twenty dev:build`, quindi `npm publish` da `.twenty/output`.
<Note>
**npm provenance** è opzionale ma consigliata. La pubblicazione con `--provenance` aggiunge un badge di attendibilità alla tua scheda npm, consentendo agli utenti di verificare che il pacchetto sia stato creato a partire da uno specifico commit in una pipeline CI pubblica. Consulta la [documentazione su npm provenance](https://docs.npmjs.com/generating-provenance-statements) per le istruzioni di configurazione.
</Note>
## Installazione delle app
Una volta che un'app è stata pubblicata (npm) o distribuita (tarball), gli spazi di lavoro possono installarla tramite l'interfaccia utente.
Vai alla pagina **Impostazioni > Applicazioni** in Twenty, dove è possibile sfogliare e installare sia le app del marketplace sia quelle distribuite tramite tarball.
{/* TODO: add screenshot of the UI when the app is registered */}
Puoi anche installare le app dalla riga di comando:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
Il server applica il versioning semver durante linstallazione, rispecchiando le regole del deploy:
* Linstallazione della stessa versione già installata nel tuo spazio di lavoro viene rifiutata con un errore `APP_ALREADY_INSTALLED`.
* Linstallazione di una versione inferiore rispetto a quella attualmente installata viene rifiutata con un errore `CANNOT_DOWNGRADE_APPLICATION`.
Per installare una versione più recente, effettua prima il deploy o la pubblicazione, quindi riesegui `yarn twenty app:install`.
</Note>
@@ -0,0 +1,111 @@
---
title: Sincronizzazione e ripristino
description: Quale comando usare e quando, come leggere l'output della sincronizzazione e una scala di ripristino per quando i metadati locali divergono — prima di arrivare a un ripristino completo.
icon: bussola
---
Lo sviluppo di app in locale ruota attorno alla **sincronizzazione**: la CLI ricostruisce il tuo manifest e il server applica solo la differenza tra questo e i metadati già presenti nel tuo spazio di lavoro. Questa pagina spiega quale comando usare, come leggere ciò che una sincronizzazione ha modificato e cosa fare — in ordine — quando lo stato locale sembra incoerente.
## Quale comando, quando
<Note>
Per literazione locale quotidiana vuoi quasi sempre `yarn twenty dev`. Il deploy e la pubblicazione servono a distribuire le release, **non** per il ciclo locale.
</Note>
| Vuoi… | Comando | Note |
| ----------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Iterare in locale con sincronizzazione in tempo reale | `yarn twenty dev` | Monitora i file e sincronizza a ogni modifica. |
| Sincronizzare una volta e uscire (CI, script, hook) | `yarn twenty dev --once` | Esegue una build + sincronizzazione, poi termina. |
| Visualizzare in anteprima le modifiche **senza applicarle** | `yarn twenty dev --once --dry-run` | Calcola e stampa il diff; non scrive nulla. |
| Rimuovere l'app dallo spazio di lavoro | `yarn twenty app:uninstall` | Aggiungi `--yes` per saltare il prompt. |
| Inviare un tarball a un server | `yarn twenty app:publish --private` | Richiede una versione di `package.json` **strettamente superiore** — vedi [Publishing](/l/it/developers/extend/apps/operations/publishing). |
| Pubblicare nel marketplace (npm) | `yarn twenty app:publish` | — |
| Installare / aggiornare una versione distribuita | `yarn twenty app:install` | Installa la versione attualmente distribuita. |
| Pulire il server locale e ripartire da zero | `yarn twenty docker:reset` | Elimina **tutti** i dati locali — ultima risorsa. |
### La sincronizzazione locale non richiede un incremento di versione
La regola della `version` strettamente crescente (`VERSION_ALREADY_EXISTS` in fase di deploy, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` in fase di installazione) si applica a **`app:publish` / `app:install`** — il percorso di release. `yarn twenty dev` sincronizza il tuo manifest in-place e non richiede mai una modifica di versione, quindi non devi toccare `package.json` per iterare. Se ti ritrovi ad aumentare la versione per testare una modifica locale, stai usando il percorso di release quando invece vuoi il ciclo di sviluppo.
## Lettura dell'output di sincronizzazione
Ogni sincronizzazione stampa le modifiche ai metadati che ha applicato (o che applicherebbe, con `--dry-run`):
```text filename="Terminal"
Metadata changes: 2 created, 1 updated, 1 deleted
created objectMetadata rocket
created fieldMetadata timelineActivities
updated fieldMetadata launchedAt
deleted pageLayout legacyTab
✓ Synced
```
Questo è il tuo primo strumento diagnostico: ti dice esattamente quali oggetti, campi e layout sono cambiati, così puoi confermare che una sincronizzazione ha fatto ciò che ti aspettavi prima di controllare l'interfaccia utente (UI).
Quando una sincronizzazione fallisce su una singola entità, l'errore indica l'entità in questione e il suo `universalIdentifier`, per esempio:
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Usa quell'identificatore per trovare l'entità nel tuo manifest (e, se necessario, nello spazio di lavoro) invece di indovinare quale sia in conflitto.
## Anteprima delle modifiche (dry run)
`yarn twenty dev --once --dry-run` crea il tuo manifest, chiede al server il piano di migrazione e lo stampa — **senza applicare nulla**. È il modo sicuro per rispondere a "cosa cambierebbe questa sincronizzazione?" prima di impegnarti ad applicarla.
```bash filename="Terminal"
yarn twenty dev --once --dry-run
```
```text filename="Terminal"
Building manifest...
Computing metadata diff (dry run, nothing will be applied)...
Metadata changes: 1 created, 1 updated
created fieldMetadata timelineActivities
updated objectMetadata rocket
✓ Dry run complete for My App — no changes were applied
```
Un dry run:
* **Non scrive nulla** — nessuna migrazione dei metadati, nessun aggiornamento del record dell'applicazione, nessuna modifica ai ruoli/schede predefiniti e nessuna generazione del client API.
* Restituisce lo **stesso diff** che una sincronizzazione reale applicherebbe, così puoi esaminare in anticipo le entità create/aggiornate/eliminate.
* È utile prima di una modifica rischiosa, quando si rivede una modifica generata da un'IA o in uno script che deve fallire se sta per essere applicata una modifica imprevista.
<Note>
Un dry run mostra in anteprima solo le modifiche ai **metadati** e richiede che l'app sia stata sincronizzata almeno una volta (così lo spazio di lavoro la conosce). Se lo esegui su un'app che non è mai stata sincronizzata, il server segnala che l'app non è installata — esegui prima `yarn twenty dev` una volta.
</Note>
## Scala di ripristino
Quando i metadati locali sembrano errati, procedi in quest'ordine e fermati non appena ti sblocchi. Ogni passaggio è più invasivo del precedente.
1. **Nuova sincronizzazione.** Esegui di nuovo `yarn twenty dev --once`. Le sincronizzazioni sono idempotenti — rieseguire un manifest pulito è sicuro e spesso risolve un problema temporaneo.
2. **Visualizza in anteprima il piano.** Esegui `yarn twenty dev --once --dry-run` per vedere esattamente cosa intende cambiare la prossima sincronizzazione, senza applicarlo.
3. **Leggi l'errore nominale.** Se una sincronizzazione fallisce, annota il tipo di metadato e lo `universalIdentifier` nel messaggio (vedi sopra) e individua quell'entità nel tuo manifest. Un conflitto di solito indica un identificatore duplicato o riutilizzato.
4. **Disinstalla e reinstalla.** `yarn twenty app:uninstall`, poi sincronizza di nuovo (`yarn twenty dev`). Questo ricostruisce i metadati dell'app partendo da zero, lasciando intatto il resto del tuo spazio di lavoro.
5. **Ripristino completo (ultima risorsa).** `yarn twenty docker:reset`, poi esegui di nuovo il seeding e la sincronizzazione.
<Warning>
`yarn twenty docker:reset` elimina **tutti** i dati nella tua istanza locale — ogni workspace, record e app. Usalo solo quando i passaggi precedenti hanno fallito.
</Warning>
<Note>
Hai riscontrato un errore di metadati? Per favore [apri una issue](https://github.com/twentyhq/twenty/issues/new/choose) e includi il messaggio di migrazione non riuscita (con il tipo di metadato e lo `universalIdentifier`), l'output `Metadata changes` della sincronizzazione e i comandi che hai eseguito.
</Note>
## Evitare sincronizzazioni concorrenti sullo stesso spazio di lavoro
La sincronizzazione applica le migrazioni dei metadati. Eseguire diverse operazioni di sincronizzazione, deploy o installazione sullo **stesso spazio di lavoro nello stesso momento** — per esempio, più terminali o agenti di IA che iterano in parallelo — può intrecciare queste migrazioni e lasciare i metadati in uno stato parzialmente applicato.
Il server serializza le sincronizzazioni per spazio di lavoro per evitare questo, ma dovresti comunque convogliare le operazioni sensibili sui metadati attraverso un **unico** processo invece di eseguirle in parallelo. Se orchestri lo sviluppo con più agenti, instrada le loro chiamate di sync/deploy/install attraverso una coda unica in modo che ne venga eseguita solo una alla volta.
## Distinguere i tipi di errore
Quando qualcosa va storto, il diff dei metadati e gli errori nominali ti permettono di localizzare il problema:
* **Errore di build del manifest** — la CLI fallisce prima della sincronizzazione (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); correggi il sorgente della tua app.
* **Errore di sincronizzazione / migrazione** — la build riesce ma l'applicazione del diff fallisce, indicando l'entità e lo `universalIdentifier`; correggi i metadati in conflitto.
* **Errore di runtime del codice dell'app** — la sincronizzazione va a buon fine ma le tue funzioni di logica o i componenti si comportano in modo anomalo in fase di esecuzione; controlla i [log delle funzioni](/l/it/developers/extend/apps/operations/cli).
* **Stato locale dell'istanza** — nessuno dei casi precedenti e lo spazio di lavoro continua a sembrare errato; procedi lungo la scala di ripristino.
@@ -0,0 +1,301 @@
---
title: Testing
description: Configurazione di Vitest, test di integrazione contro un server Twenty reale, controllo dei tipi e CI con GitHub Actions.
icon: flask
---
L'SDK fornisce API programmatiche che ti consentono di compilare, distribuire, installare e disinstallare la tua app dal codice di test. In combinazione con [Vitest](https://vitest.dev/) e i client API tipizzati, puoi scrivere test di integrazione che verificano che la tua app funzioni end-to-end contro un server Twenty reale.
## Uso dei pacchetti npm
Puoi installare e usare qualsiasi pacchetto npm nella tua app. Sia le funzioni logiche sia i componenti front-end vengono impacchettati con [esbuild](https://esbuild.github.io/), che incorpora tutte le dipendenze nell'output — non sono necessari i `node_modules` a runtime.
### Installazione di un pacchetto
```bash filename="Terminal"
yarn add axios
```
Quindi importalo nel tuo codice:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Lo stesso vale per i componenti front-end:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Come funziona il bundling
La fase di build usa esbuild per produrre un singolo file autonomo per ogni funzione logica e per ogni componente front-end. Tutti i pacchetti importati sono incorporati nel bundle.
**Le funzioni logiche** vengono eseguite in un ambiente Node.js. I moduli integrati di Node (`fs`, `path`, `crypto`, `http`, ecc.) sono disponibili e non necessitano di essere installati.
**I componenti front-end** vengono eseguiti in un Web Worker. I moduli integrati di Node non sono disponibili — solo le API del browser e i pacchetti npm che funzionano in un ambiente browser.
Entrambi gli ambienti hanno `twenty-client-sdk/core` e `twenty-client-sdk/metadata` disponibili come moduli preforniti — questi non vengono inclusi nel bundle ma vengono risolti a runtime dal server.
## Impostazione
L'app generata tramite scaffolding include già Vitest. Se la configuri manualmente, installa le dipendenze:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Crea un `vitest.config.ts` alla radice della tua app:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:2020',
TWENTY_API_KEY: 'your-api-key',
},
},
});
```
Crea un file di setup che verifichi che il server sia raggiungibile prima dell'esecuzione dei test:
```ts src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(async () => {
// Verify the server is running
const response = await fetch(`${TWENTY_API_URL}/healthz`);
if (!response.ok) {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Start the server before running integration tests.',
);
}
// Write a temporary config for the SDK
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
}, null, 2),
);
});
```
## API programmatiche dell'SDK
Il sottopercorso `twenty-sdk/cli` esporta funzioni che puoi chiamare direttamente dal codice di test:
| Funzione | Descrizione |
| -------------- | ----------------------------------------------- |
| `appBuild` | Compila l'app e, opzionalmente, crea un tarball |
| `appDeploy` | Carica un tarball sul server |
| `appInstall` | Installa l'app nello spazio di lavoro attivo |
| `appUninstall` | Disinstalla l'app dallo spazio di lavoro attivo |
Ogni funzione restituisce un oggetto risultato con `success: boolean` e `data` oppure `error`.
## Scrivere un test di integrazione
Ecco un esempio completo che compila, distribuisce e installa l'app, quindi verifica che compaia nello spazio di lavoro:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## Esecuzione dei test
Assicurati che il tuo server Twenty locale sia in esecuzione, quindi:
```bash filename="Terminal"
yarn test
```
Oppure in modalità watch durante lo sviluppo:
```bash filename="Terminal"
yarn test:watch
```
## Controllo dei tipi
Puoi anche eseguire il controllo dei tipi sulla tua app senza eseguire i test:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Questo esegue `tsc --noEmit` e riporta eventuali errori di tipo.
## CI con GitHub Actions
Lo strumento di scaffolding genera un workflow GitHub Actions pronto all'uso in `.github/workflows/ci.yml`. Esegue automaticamente i test di integrazione a ogni push su `main` e sulle pull request.
Il workflow:
1. Esegue il checkout del tuo codice
2. Avvia un server Twenty temporaneo utilizzando l'azione `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Installa le dipendenze con `yarn install --immutable`
4. Esegue `yarn test` con `TWENTY_API_URL` e `TWENTY_API_KEY` iniettati dagli output dell'azione
```yaml .github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: latest
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
```
Non è necessario configurare alcun secret — l'azione `spawn-twenty-docker-image` avvia un server Twenty effimero direttamente nel runner e fornisce i dettagli di connessione. Il secret `GITHUB_TOKEN` è fornito automaticamente da GitHub.
Per fissare una versione specifica di Twenty invece di `latest`, modifica la variabile d'ambiente `TWENTY_VERSION` all'inizio del workflow.
@@ -88,7 +88,7 @@ La tua chiave API concede l'accesso a dati sensibili. Non condividerla con servi
Per una maggiore sicurezza, assegna un ruolo specifico per limitare l'accesso:
1. Vai a **Impostazioni → Ruoli**
1. Vai a **Impostazioni → Membri → Ruoli**
2. Fai clic sul ruolo da assegnare
3. Apri la scheda **Assegnazione**
4. In **Chiavi API**, fai clic su **+ Assegna alla chiave API**
@@ -51,7 +51,7 @@ Segui questi passaggi per un setup manuale.
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
2. **Genera Token Segreti**
2. **Genera una chiave di cifratura**
Esegui il seguente comando per generare una stringa casuale unica:
@@ -59,16 +59,18 @@ Segui questi passaggi per un setup manuale.
openssl rand -base64 32
```
**Importante:** Tieni questo valore segreto / non condividerlo.
**Importante:** Tieni questo valore segreto / non condividerlo. La perdita di `ENCRYPTION_KEY` significa perdere l'accesso a tutti i segreti archiviati nel database (token OAuth, variabili dell'applicazione, segreti TOTP, ecc.).
3. **Aggiorna il `.env`**
Sostituisci il valore segnaposto nel tuo file .env con il token generato:
```ini
APP_SECRET=prima_stringa_casuale
ENCRYPTION_KEY=random_string
```
Consulta la [Guida alla rotazione delle chiavi](/l/it/developers/self-host/capabilities/key-rotation) per le istruzioni su come ruotarla senza tempi di inattività.
4. **Imposta la Password di Postgres**
Aggiorna il valore `PG_DATABASE_PASSWORD` nel file .env con una password forte senza caratteri speciali.
@@ -0,0 +1,60 @@
---
title: Rotazione delle chiavi
icon: rotate
---
Twenty ha due famiglie di chiavi indipendenti:
* **Chiavi di firma JWT** — coppie di chiavi asimmetriche ES256 (con tag `kid`) archiviate in `core."signingKey"`, utilizzate per firmare e verificare i token di accesso / aggiornamento.
* **Chiave di crittografia a riposo** — `ENCRYPTION_KEY`, utilizzata per crittografare token OAuth, variabili dell'applicazione, chiavi private delle chiavi di firma, valori di configurazione sensibili e segreti TOTP all'interno di un involucro `enc:v2:`.
`APP_SECRET` è un segreto legacy mantenuto per compatibilità con le versioni precedenti: quando `ENCRYPTION_KEY` non è impostata funge da fallback per la crittografia a riposo / cookie di sessione e continua a verificare i token di accesso HS256 preesistenti. Sarà deprecata.
## Chiavi di firma JWT
Ogni chiave contiene una `publicKey` (mantenuta indefinitamente così da poter verificare i token emessi in precedenza), una `privateKey` crittografata (utilizzata solo mentre la chiave è quella corrente), un flag `isCurrent` (esattamente una riga alla volta) e un campo `revokedAt` opzionale.
### Ruotare la chiave corrente
Imposta `SIGNING_KEY_ROTATION_DAYS` per aderire: un cron giornaliero emette quindi una nuova chiave corrente quando quella esistente è più vecchia di tale soglia. Le chiavi precedenti *non* vengono revocate, quindi i token firmati con esse continuano a essere verificati. Lascia la variabile non impostata per disattivare la rotazione automatica.
<Note>La rotazione automatica è disponibile a partire dalla versione v2.6+.</Note>
### Revocare una chiave (solo in caso di fuga di dati / emergenza)
**Settings → Admin Panel → Signing keys → Revoke** su una riga non corrente. Cancella il materiale privato crittografato, imposta `revokedAt` e rifiuta ogni token esistente firmato con quel `kid`.
## Ruotare `ENCRYPTION_KEY`
<Note>Il comando `secret-encryption:rotate` descritto di seguito è disponibile dalla v2.6+.</Note>
Ogni valore crittografato è incapsulato come `enc:v2:\<keyId>:\<payload>`, dove `\<keyId>` è un prefisso esadecimale a 8 caratteri derivato dalla chiave grezza. La rotazione è online e riprendibile.
1. **Generare una nuova chiave**: `openssl rand -base64 32`.
2. **Configurare entrambe le chiavi affiancate** in `.env`, quindi riavviare:
```ini
ENCRYPTION_KEY=NEW_VALUE
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
```
Le nuove scritture usano la nuova chiave, le righe esistenti vengono ancora decrittate tramite il fallback.
3. **Ricrittografare le righe esistenti**:
```bash
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
```
Il comando passa in rassegna sei sezioni (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). Un filtro SQL salta le righe già con il nuovo `\<keyId>`, quindi il comando è idempotente: interrompi e riesegui secondo necessità. Esce con codice diverso da zero se una qualsiasi riga fallisce — riesegui per riprovare.
| Opzione | Descrizione |
| ---------------------------------------- | ----------------------------------------------------------- |
| `-s, --site \<site>` | Limita a un singolo sito. |
| `-b, --batch-size \<n>` | Righe per batch (predefinito `200`, massimo `5000`). |
| `-d, --dry-run` | Decrittografa + ricrittografa in memoria, salta l'`UPDATE`. |
4. **Eliminare il fallback** una volta che `--dry-run` mostra zero righe rimanenti: rimuovere `FALLBACK_ENCRYPTION_KEY` e riavviare.
## Supporto legacy per `APP_SECRET`
Le istanze meno recenti che non hanno mai impostato `ENCRYPTION_KEY` usano `APP_SECRET` come chiave di crittografia a riposo (e come segreto del cookie di sessione, derivato da essa). Questo percorso è mantenuto per compatibilità con le versioni precedenti ma è **deprecato** — imposta una `ENCRYPTION_KEY` dedicata e segui la procedura di rotazione sopra per migrare. `APP_SECRET` continua a essere utilizzata per verificare i token di accesso HS256 legacy.
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # predefinito
<Warning>
Ogni variabile è documentata con descrizioni nel tuo pannello di amministrazione in **Impostazioni → Pannello di Amministrazione → Variabili di Configurazione**.
Alcune impostazioni infrastrutturali come connessioni al database (`PG_DATABASE_URL`), URL del server (`SERVER_URL`) e segreti dell'app (`APP_SECRET`) possono essere configurati solo tramite file `.env`.
Alcune impostazioni infrastrutturali come connessioni al database (`PG_DATABASE_URL`), URL del server (`SERVER_URL`) e segreti (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) possono essere configurate solo tramite file `.env`.
[Riferimento tecnico completo →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
</Warning>
## Chiavi di crittografia
Twenty utilizza due chiavi di crittografia disponibili solo tramite variabili d'ambiente:
| Variabile | Scopo | Obbligatorio |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `ENCRYPTION_KEY` | Chiave primaria utilizzata per crittografare i segreti a riposo (token OAuth, variabili dell'applicazione, chiavi private delle chiavi di firma, segreti TOTP, valori di configurazione sensibili). | Sì per le nuove installazioni (le installazioni legacy possono invece fare affidamento su `APP_SECRET` — vedi sotto) |
| `FALLBACK_ENCRYPTION_KEY` | Chiave utilizzata solo per la verifica. Impostata durante una rotazione alla *precedente* `ENCRYPTION_KEY` in modo che le righe esistenti rimangano decifrabili. | Solo durante la rotazione |
Per garantire la retrocompatibilità, se `ENCRYPTION_KEY` non è impostata, Twenty torna a utilizzare `APP_SECRET` per la crittografia a riposo, in modo da rispecchiare il comportamento legacy delle distribuzioni meno recenti. Le nuove installazioni dovrebbero sempre impostare una `ENCRYPTION_KEY` dedicata.
Genera i valori con `openssl rand -base64 32` e conservali in un luogo sicuro (un secrets manager, configurazioni sigillate, ecc.). Perdere `ENCRYPTION_KEY` significa perdere l'accesso a ogni segreto memorizzato nel database.
Per ruotare `ENCRYPTION_KEY` senza tempi di inattività, consulta la [guida alla rotazione delle chiavi](/l/it/developers/self-host/capabilities/key-rotation).
## 2. Configurazione Solo-Ambiente
```bash
@@ -31,6 +31,18 @@ A partire da **v1.22**, Twenty supporta gli aggiornamenti tra versioni. Puoi pas
Ad esempio, l'aggiornamento da v1.22 direttamente a v2.0 è pienamente supportato.
## Aggiornamento alla v2.5+ — envelope di crittografia dei dati a riposo
A partire dalla **v2.5**, Twenty memorizza i segreti a riposo (token OAuth, variabili dell'applicazione, chiavi private di firma, valori di configurazione sensibili, segreti TOTP) all'interno di un envelope versionato `enc:v2:` crittografato con `ENCRYPTION_KEY` (o `APP_SECRET` se `ENCRYPTION_KEY` non è impostata).
Il primo avvio su v2.5 esegue comandi di aggiornamento lenti che popolano retroattivamente le righe esistenti nel nuovo envelope. Sono idempotenti — se si interrompe e si riavvia il server, riprende da dove si era interrotto — ma possono richiedere tempo su database di grandi dimensioni. Puoi monitorare l'avanzamento con `upgrade:status`.
Dovresti impostare un `ENCRYPTION_KEY` dedicato **prima** dell'aggiornamento a v2.5, in modo che il popolamento retroattivo scriva fin dall'inizio le righe sotto tale chiave. Cambiare chiave dopo il popolamento retroattivo richiede una [rotazione](/l/it/developers/self-host/capabilities/key-rotation).
## Rotazione di segreti e chiavi di firma
Per le attività operative quotidiane come ruotare `ENCRYPTION_KEY`, ruotare la chiave di firma JWT o revocare una chiave di firma compromessa, vedi la [Guida alla rotazione delle chiavi](/l/it/developers/self-host/capabilities/key-rotation) dedicata.
## Verifica dello stato dell'aggiornamento
Il comando `upgrade:status` consente di ispezionare lo stato corrente della tua istanza e delle migrazioni degli spazi di lavoro. È utile per eseguire il debug di problemi di aggiornamento o quando si apre una richiesta di assistenza.