snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
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 chiave–valore esposte al tuo codice come variabili d'ambiente.
* **Hook di pre-installazione / post-installazione / disinstallazione** *(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, post-installazione e disinstallazione 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()`.
* `serverVariables` sono configurazioni e segreti con ambito di istanza (ad esempio chiavi API). A differenza di `applicationVariables`, non dichiarano alcun valore nel manifest — l’operatore dello spazio di lavoro li compila dalle impostazioni dell’app e vengono iniettati nelle funzioni di logica solo una volta impostati.
* Entrambi i tipi di variabile accettano `isDeprecated: true`. Usala per ritirare una variabile invece di eliminarla: mantenere la chiave dichiarata preserva il valore memorizzato (eliminandola si distrugge il valore inserito dall'operatore) e la variabile è ancora iniettata, così il tuo codice può ripiegare su di essa — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Una variabile deprecata scompare dalle impostazioni dell'app non appena non ha più un valore e non viene mai conteggiata nel controllo di configurazione dell'app, quindi `isDeprecated` prevale su `isRequired`.
* Per eseguire il rendering di un'interfaccia di configurazione personalizzata all'interno della scheda **Settings** dell'app (al posto della sezione predefinita di configurazione delle variabili), dichiara un front component con [`defineSettingsFrontComponent()`](/l/it/developers/extend/apps/layout/front-components#custom-settings-component) in un proprio file. Ne è consentito solo uno per app. Le sezioni gestite dal sistema (auto-upgrade, App URL, connessioni) rimangono sempre visibili.
## Tipi di variabili
Sia `applicationVariables` che `serverVariables` accettano un `type` opzionale (e, per `SELECT` / `MULTI_SELECT`, un elenco di `options`). Tipi supportati: `TEXT` (predefinito), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Il `type` influisce solo su **presentazione e convalida**: seleziona l’input corrispondente nell’interfaccia delle impostazioni dell’area di lavoro (un interruttore, campo numerico, menu a discesa, selettore di data, editor JSON, …) e consente alla build di convalidare la tua configurazione (ad esempio, `SELECT` / `MULTI_SELECT` devono dichiarare `options` non vuote). **Non** cambia il modo in cui il valore arriva al tuo codice.
I valori sono **sempre inseriti come stringhe**: ciò è intrinseco alle variabili di ambiente (`process.env.*` accetta solo stringhe). Quando la tua funzione di logica viene eseguita, l’executor serializza ogni valore in base al `type` dichiarato mentre costruisce `process.env`, quindi il formato della stringa è coerente indipendentemente da come è stato impostato il valore (valore predefinito del manifest, interfaccia delle impostazioni o una versione precedente):
| Tipo | stringa di `process.env` |
| ------------------------------------- | ----------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | il valore grezzo (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | stringa decimale (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | array JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | oggetto JSON (`'{"retries":3}'`) |
Analizza nuovamente la stringa nel tipo che ti aspetti:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Lo stesso vale per i componenti front-end che leggono i valori tramite `getApplicationVariable('VARIABLE_NAME')`: il valore restituito è una stringa; analizzalo secondo le necessità.
## 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:
* I token di runtime iniettati nelle funzioni logiche sono derivati da questo ruolo. Una chiamata che agisce come una persona è ulteriormente limitata a ciò che tale persona può fare, quindi non può mai superare nessuno dei due. Vedi [L'accesso utilizzato da una chiamata](/l/it/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* 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 |
| `logo` | Percorso del logo dell'app in bundle in `public/` (ad esempio, `public/logo.png`) |
| `galleryImages` | Array dei percorsi delle immagini della galleria raggruppati in `public/` (ad esempio, `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 |
<Note>
`logoUrl` e `screenshots` sono alias deprecati di `logo` e `galleryImages`. Gli URL assoluti esterni (`http://` o `https://`) non sono supportati per questi campi: vengono eliminati con un avviso al momento della generazione. Raccogli invece le immagini nella cartella `public/` della tua app.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hook di installazione
description: Esegui la logica durante il ciclo di vita di installazione, aggiornamento o disinstallazione — inserisci dati iniziali, esegui il backup dei record, valida l'aggiornamento, pulisci le risorse esterne.
icon: wrench
---
Gli hook di installazione sono funzioni logiche speciali che vengono eseguite durante il ciclo di vita di installazione, aggiornamento o disinstallazione. Condividono lo stesso runtime del gestore delle [logic functions](/l/it/developers/extend/apps/logic/logic-functions) normali, ma sono dichiarati con le proprie funzioni di definizione e vivono al di fuori del normale modello di trigger (HTTP, cron, eventi del database). Gli hook di installazione ricevono un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` è `undefined` in caso di nuova installazione); l'hook di disinstallazione riceve un `UninstallPayload` (`{ version?: string }` — la versione che viene rimossa).
Ogni app può definire **al massimo uno** per ciascun hook (pre-install, post-install, uninstall). La build del manifesto genera un errore se viene rilevato più di un hook per qualsiasi tipo.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## A colpo d'occhio
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Quando viene eseguito | Prima della migrazione dei metadati — lo schema e i dati **precedenti** sono ancora intatti | Dopo la migrazione e la generazione dell'SDK — il **nuovo** schema è in vigore |
| Esecuzione | Sempre sincrona; blocca l'installazione | Async per impostazione predefinita (in coda, 3 tentativi); modalità sync tramite opt-in con `shouldRunSynchronously: true` |
| In caso di errore | L'installazione viene **annullata** prima di qualsiasi modifica allo schema | Async: ritentato fino a 3 volte. Sync: il chiamante riceve `POST_INSTALL_ERROR` (le modifiche allo schema **non** vengono annullate) |
| Uso tipico | Eseguire il backup o correggere dati che una migrazione perderebbe; rifiutare un aggiornamento rischioso lanciando un'eccezione | Popolare dati predefiniti, configurare il workspace, registrare risorse esterne |
**Regola generale:** usa post-install come impostazione predefinita. Ricorri al pre-install solo quando la migrazione stessa è distruttiva e devi intercettare lo stato precedente prima che vada perso.
| Vuoi... | Usa |
| ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| Popolare i dati, configurare il workspace, registrare risorse esterne | `post-install` |
| Lavoro di lunga durata che non dovrebbe bloccare la risposta dell'installazione | `post-install` (modalità async predefinita, con retry del worker) |
| Eseguire un setup rapido da cui il chiamante dipende immediatamente dopo il completamento dell'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) |
| Riconciliazione a ogni aggiornamento | Uno qualsiasi dei due hook con `shouldRunOnVersionUpgrade: true` |
## Comportamento condiviso da entrambi gli hook
* La config è una config di `defineLogicFunction` meno le impostazioni di trigger, più `shouldRunOnVersionUpgrade`.
* **Quando viene eseguito**: solo sulle nuove installazioni, per impostazione predefinita. Imposta `shouldRunOnVersionUpgrade: true` per eseguirlo anche sugli upgrade. Usa `previousVersion` / `newVersion` per ramificare in base al percorso di upgrade.
* **L'idempotenza è importante**: il post-install async può essere ritentato e qualsiasi hook viene rieseguito sugli upgrade quando `shouldRunOnVersionUpgrade` è attivo.
* Il consueto ambiente delle logic-function (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) viene iniettato, così puoi chiamare le API di Twenty con il token della tua app.
* L'hook viene collegato automaticamente al manifesto dell'applicazione in fase di build (`preInstallLogicFunction` / `postInstallLogicFunction`) — non c'è nulla da referenziare in [`defineApplication()`](/l/it/developers/extend/apps/config/application).
* Il `timeoutSeconds` predefinito è 300 per consentire attività di setup più lunghe, come il seeding dei dati.
* **Non eseguito in modalità dev**: `yarn twenty dev` salta il flusso di installazione e sincronizza direttamente i file, quindi gli hook non vengono mai eseguiti in quell'ambiente. Attivali invece manualmente:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="Viene eseguita dopo che la migrazione dei metadati dello spazio di lavoro è stata applicata">
Viene eseguito una volta che l'installazione della tua app è terminata: metadati sincronizzati, client SDK generato, nuovo schema interrogabile. Esempio — eseguire il seeding di un record predefinito nelle nuove installazioni:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Il flag `shouldRunSynchronously` controlla il modello di esecuzione:
* `false` *(predefinito)* — messo in coda nella message queue (`retryLimit: 3`) ed eseguito da un worker. La risposta dell'installazione ritorna non appena il job viene messo in coda. **Da usare per lavoro di lunga durata** — seeding di grandi dataset, API di terze parti lente.
* `true` — eseguito inline durante il flusso di installazione. La richiesta di installazione rimane bloccata finché l'handler non termina; un errore lanciato viene esposto al chiamante come `POST_INSTALL_ERROR` (nessun retry). **Da usare per lavoro rapido che deve completarsi prima della risposta.** La migrazione è già stata applicata a questo punto, quindi un errore non annulla le modifiche allo schema — si limita a esporre l'errore.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Viene eseguita prima che la migrazione dei metadati dello spazio di lavoro sia applicata">
Viene eseguito prima della migrazione dei metadati, contro lo schema **precedente** — il posto giusto per eseguire il backup di dati che una migrazione perderebbe o per rifiutare un upgrade rischioso. Prima dell'esecuzione, il server esegue una "sincronizzazione ridotta" puramente additiva che registra solo la funzione di pre-install della versione nuova; tutto il resto — oggetti, campi e dati della versione precedente — rimane intatto quando il tuo handler viene eseguito.
Il pre-install è sempre **sincrono** e blocca l'installazione. Se l'handler genera un'eccezione, l'installazione viene interrotta prima che venga applicata qualsiasi modifica 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.
Esempio — copiare i valori di un campo legacy prima che la migrazione lo elimini:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
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 = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
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,
});
```
</Accordion>
</AccordionGroup>
## Hook di disinstallazione
`defineUninstallLogicFunction` dichiara un hook che viene eseguito quando un utente disinstalla la tua app. Viene eseguito **prima** che i metadati, i dati e il codice dell'app vengano rimossi — una volta eseguita la migrazione di eliminazione non rimane più nulla da eseguire — quindi il tuo gestore può ancora interrogare gli oggetti e i record dell'app. Usalo per la pulizia delle risorse esterne: deprovisioning delle risorse API, eliminazione dei bot rimanenti, revoca dei webhook.
Note:
* L'hook è "best-effort": viene eseguito in modo sincrono, ma un errore viene registrato e **non blocca mai la disinstallazione** — la pulizia non deve rendere impossibile rimuovere un'app.
* Riceve `UninstallPayload` (`{ version?: string }` — la versione che viene rimossa).
* Non viene eseguito quando un tentativo di nuova installazione non riuscito viene annullato — l'app non ha mai terminato l'installazione.
* L'hook non può essere eseguito dopo che l'app è stata rimossa, quindi la pulizia esterna che dipende dai dati dell'app (ad es. ID dei bot memorizzati nei record) deve essere eseguita qui, non in un job pianificato esterno.
* Come gli hook di installazione, **non viene eseguito in modalità dev** — attivalo manualmente invece:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
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
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## 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`, `definePostInstallLogicFunction` e `defineUninstallLogicFunction` — esegui il backup dei dati, imposta i valori predefiniti, convalida gli aggiornamenti, pulisci al momento della rimozione.
</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). L'hook di disinstallazione viene eseguito subito prima che l'app venga rimossa, in modo da poter ripulire le risorse esterne mentre i dati dell'app sono ancora leggibili.
<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>

View File

@@ -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.
* **Utilizzato per i metadati di mercato** — i campi `logo` e `galleryImages` nei file di riferimento `defineApplication()` di questa cartella (ad esempio, `public/logo.png`). Questi vengono visualizzati nel marketplace quando la tua app viene pubblicata. Gli URL assoluti esterni vengono ignorati per questi campi — raggruppano invece le immagini in `public/`. Ogni immagine non deve superare i 10 MB.
* **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.

View File

@@ -0,0 +1,207 @@
---
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 un’app 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],
});
```
## Sicurezza a livello di riga
Le autorizzazioni sugli oggetti e sui campi determinano *quali oggetti e campi* un ruolo può gestire. **I predicati di autorizzazione
a livello di riga** vanno oltre e determinano *quali record* un ruolo può vedere e su cui può agire — per
esempio, un ruolo self-service in cui ogni utente esterno vede solo i propri record.
Dichiara i predicati con `rowLevelPermissionPredicates` sul ruolo. Come il resto del manifest,
ogni predicato ha il proprio `universalIdentifier` e fa riferimento a un oggetto e a un campo tramite il loro
`universalIdentifier`, un `operand` e (facoltativamente) un campo workspaceMember il cui valore viene iniettato
al momento della query — così puoi esprimere "la relazione di proprietario del record **è** l'attuale membro dello spazio di lavoro":
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Poiché i predicati vengono distribuiti nel manifest, vengono creati, aggiornati e rimossi insieme al ruolo a ogni installazione e aggiornamento — non è necessario alcun passaggio post-installazione separato per mantenerli sincronizzati.
### Combinare i predicati con i gruppi
Per impostazione predefinita, i predicati di un ruolo sono combinati con `AND`. Per combinarne alcuni con `OR` (o per annidare la logica), dichiara una voce `rowLevelPermissionPredicateGroups` e collega ogni predicato ad essa tramite `predicateGroupUniversalIdentifier`. Questo ruolo consente a un partner di vedere un'Opportunità di cui **è proprietario**
o per cui è il **punto di contatto**:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Note:
* Assegna a ogni predicato e gruppo un `universalIdentifier` stabile (qualsiasi uuid) — identifica l'entità attraverso gli aggiornamenti e i predicati fanno riferimento ai gruppi tramite esso.
* I predicati possono fare riferimento a oggetti e campi di proprietà della tua app o degli oggetti standard di Twenty.
* La sicurezza a livello di riga è applicata per gli spazi di lavoro sui piani che la includono; i predicati vengono comunque sincronizzati sugli altri piani, semplicemente non vengono applicati.
## 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 dell’installazione. La convalida è identica a `defineRole`, ma la pipeline di build collega automaticamente il suo `universalIdentifier` al `defaultRoleUniversalIdentifier` del manifest dell’applicazione — 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.
* L’impostazione 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 di lavoro: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
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/`.
* La `writability` può anche essere impostata per singolo campo: `MetadataWritability.APPLICATION` limita le scritture alle sole funzioni di logica della tua app, mentre `MetadataWritability.SYSTEM` è riservata ai dati gestiti dalla piattaforma. La writability di un campo può essere solo più restrittiva rispetto al livello del relativo oggetto e le letture non sono influenzate. Consulta [Objects](/l/it/developers/extend/apps/data/objects) per i dettagli completi sulla semantica.
* 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.

View File

@@ -0,0 +1,143 @@
---
title: Oggetti
description: Dichiara nuovi tipi di record — tabelle personalizzate con i propri campi — usando defineObject.
icon: table
---
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.
* `openRecordIn` imposta dove si aprono i record di questo oggetto quando vengono cliccati: `ObjectOpenRecordIn.USER_CHOICE` (l'impostazione predefinita, che segue la preferenza di ciascun membro dell'area di lavoro da Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` oppure `ObjectOpenRecordIn.RECORD_PAGE`. Fissalo su `RECORD_PAGE` per i record che devono occupare un'intera pagina per essere usabili, come avviene per i workflow e le dashboard, oppure su `SIDE_PANEL` per i record che hanno senso solo come pannello rapido, come avviene per gli eventi di calendario.
* `writability` controlla chi può scrivere i record dell'oggetto in generale, prima che si applichino le autorizzazioni basate sui ruoli: `MetadataWritability.OPEN` (impostazione predefinita — i ruoli dello spazio di lavoro decidono), `MetadataWritability.APPLICATION` (solo le funzioni logiche della tua app possono creare, aggiornare o eliminare record; usa questa impostazione per oggetti simili a configurazioni i cui record determinano il comportamento, in modo che i membri dello spazio di lavoro con un ampio accesso ai record non possano modificarli tramite l'API) oppure `MetadataWritability.SYSTEM` (riservato ai dati gestiti dalla piattaforma). Le letture non sono influenzate — questo è applicato lato server, diversamente da `isUIEditable`, che si limita a nascondere gli elementi dell'interfaccia utente. Esiste anche per singolo campo, dove può essere soltanto più restrittivo rispetto al livello dell'oggetto.
* 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>
## Tipi di campo
L’insieme completo dei valori di `FieldType`, esportati da `twenty-sdk/define`:
| Categoria | Tipi |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Testo | `TEXT`, `RICH_TEXT`, `ARRAY` (di stringhe), `RAW_JSON` |
| Numerico | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (precisione arbitraria), `RATING`, `POSITION` |
| Date | `DATE`, `DATE_TIME` |
| Scelta | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Compositi | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identificatori e relazioni | `UUID`, `RELATION`, `MORPH_RELATION` (vedi [Relazioni](/l/it/developers/extend/apps/data/relations)) |
| Sistema | `TS_VECTOR` (vettore per la ricerca full-text, gestito dal server) |
I tipi compositi memorizzano più sotto-campi (ad es. `FULL_NAME` = nome + cognome; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` e `MULTI_SELECT` richiedono un array `options` come nell’esempio sopra.
## 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.
## Nullabilità
`isNullable` controlla se un campo accetta `NULL`. Il valore predefinito è `true` — omettilo per i campi facoltativi. Imposta `isNullable: false` per rendere un campo obbligatorio a livello di database.
Le modifiche a `isNullable` vengono applicate a ogni sincronizzazione, incluse quelle che aggiornano un campo esistente — quindi puoi cambiare la nullabilità di un campo modificando il manifest e rieseguendo la sincronizzazione.
<Note>
**Rendere un campo esistente che non ammette `NULL` richiede un valore predefinito.** Quando imposti `isNullable: false` per un campo, devi anche fornire un `defaultValue` non nullo. Il valore predefinito esegue il backfill di tutte le righe `NULL` esistenti prima che venga applicato il vincolo `NOT NULL`; senza di esso la sincronizzazione non riesce con l'errore `Default value cannot be null for non-nullable fields`. I campi di relazione e i campi `TS_VECTOR` ammettono sempre `NULL`, quindi `isNullable` non ha alcun effetto su di essi.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## 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()`.
* **Visualizza questo oggetto nell'interfaccia utente (UI)** — vedi [Voci del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items) per aggiungere una voce nella barra laterale; vedi [Viste](/l/it/developers/extend/apps/layout/views) per aggiungere configurazioni di elenco personalizzate.

View File

@@ -0,0 +1,101 @@
---
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="table" 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>
<Card title="Tipi di attività della timeline" icon="clock-rotate-left" href="/l/it/developers/extend/apps/data/timeline-activity-types">
Definisci eventi di audit automatici ed eventi espliciti dell'app per le timeline dei record.
</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()` |
| **Tipo di attività della timeline** | Un evento di audit automatico o un evento di dominio esplicito visualizzato nelle timeline dei record | `defineTimelineActivityType()` |
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/`, `src/indexes/` e `src/timeline-activity-types/`. 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>

View File

@@ -0,0 +1,184 @@
---
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 di giunzione
Un oggetto di giunzione collega un record di origine a uno o più record di destinazione. Ad esempio, `PostCardRecipient` può contenere sia una relazione `postCard` sia una relazione `recipient`. Nel campo `ONE_TO_MANY` dell'oggetto di origine, imposta `junctionTargetFieldUniversalIdentifier` sul campo di relazione dell'oggetto di giunzione che punta alla destinazione:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` deve identificare una relazione `MANY_TO_ONE` o morph nell'oggetto di giunzione. Indica a Twenty quale record correlato è la destinazione quando una funzionalità come la [distribuzione delle attività della timeline](/l/it/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) attraversa la giunzione.
## 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`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Solo ONE_TO_MANY di giunzione | Campo di relazione nell'oggetto di giunzione che punta alla destinazione della distribuzione |
## 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
],
});
```

View File

@@ -0,0 +1,211 @@
---
title: Fare riferimento ai metadati di sistema
description: Risolvi gli identificatori universali deterministici dei metadati che Twenty predispone automaticamente su ogni oggetto, così la tua app può farvi riferimento senza doverli codificare in modo statico.
icon: gears
---
Ogni oggetto in Twenty include **metadati di sistema** che non dichiari mai direttamente, come ad esempio un insieme di campi e una vista elenco principale con le sue colonne. Il server crea tutto questo quando l'oggetto viene predisposto e l'insieme cresce man mano che Twenty cresce.
Poiché non lo dichiari, non esiste alcuna costante `universalIdentifier` da importare. Invece, il server **deriva** ogni identificatore in modo deterministico e `twenty-sdk` espone la stessa derivazione affinché il tuo manifest possa risolvere il valore esatto utilizzato dal server.
## Campi di sistema
I campi scalari presenti su ogni oggetto, nessuno dei quali dichiari con [`defineField()`](/l/it/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Quindi, come fai a fare riferimento a `createdAt` come colonna in una [vista](/l/it/developers/extend/apps/layout/views)?
### Il problema
A partire da Twenty 2.19, l'identificatore universale di un campo di sistema viene derivato in modo deterministico dal server sulla base di tre input: l'identificatore universale dell'applicazione, l'identificatore universale dell'oggetto e il nome del campo. Inventare un id e hardcodarlo non funziona: non corrisponde a nulla sul server e la sincronizzazione rifiuta il riferimento orfano:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### La soluzione
<Note>
`getFieldUniversalIdentifier` è disponibile da `twenty-sdk` 2.21 in poi.
</Note>
Usa `getFieldUniversalIdentifier` per ottenere esattamente lo stesso valore utilizzato dal server. Accetta i tre input e restituisce l'identificatore universale del campo:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` è l'identificatore della tua app, quello che passi a [`defineApplication()`](/l/it/developers/extend/apps/config/application).
* `objectUniversalIdentifier` è l'identificatore dell'oggetto a cui il campo appartiene.
* `name` è il nome del campo di sistema, uno dei valori elencati sopra.
### Esempio: una colonna createdAt in una vista
Il caso tipico è aggiungere una colonna `createdAt` a una vista di uno dei tuoi oggetti personalizzati. Risolvi l'id del campo e usalo come qualsiasi altro `fieldMetadataUniversalIdentifier`:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Lo stesso id risolto funziona ovunque ci si aspetti un `fieldMetadataUniversalIdentifier`: campi di vista, filtri, ordinamenti, raggruppamenti e widget di layout di pagina.
<Note>
Risolvi l'id, non hardcodarlo. Poiché il server deriva il valore
dall'id dell'applicazione, dall'id dell'oggetto e dal nome del campo, chiamare
`getFieldUniversalIdentifier` mantiene corretto il riferimento anche se questi
input cambiano ed evita discrepanze se la modalità di derivazione dovesse mai evolvere.
</Note>
### Campi di relazione di sistema
<Note>
`getSystemRelationFieldUniversalIdentifier` è disponibile da `twenty-sdk`
2.23 in poi e richiede un server Twenty dalla versione 2.23 o successiva.
</Note>
Oltre ai campi scalari di sistema sopra indicati, il server fornisce anche quattro **campi di relazione di sistema** su ogni oggetto: `timelineActivities`, `attachments`, `noteTargets` e `taskTargets`, ciascuno dei quali punta al corrispondente oggetto di relazione standard.
Questi campi non vengono risolti con `getFieldUniversalIdentifier`: il loro identificatore è derivato **in modo indipendente dal nome**, dall'oggetto che ospita il campo e dall'oggetto a cui il campo punta. In questo modo, rinominare un oggetto non modifica mai gli identificatori dei suoi campi di relazione.
Usa `getSystemRelationFieldUniversalIdentifier` per risolverli:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` è l'oggetto che **ospita** il campo.
* `relationTargetObjectUniversalIdentifier` è l'oggetto a cui il campo **punta**.
La direzione è codificata dall'ordine degli argomenti. Per risolvere il lato inverso (ad esempio `attachment.targetRocket`, il campo morph che il server crea sull'oggetto di relazione standard), scambia i due:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Come per i campi di sistema scalari, l'id risolto funziona ovunque ci si aspetti un `fieldMetadataUniversalIdentifier`.
## Viste di sistema
<Note>
`getSystemViewUniversalIdentifier` e `getSystemViewFieldUniversalIdentifier`
sono disponibili da `twenty-sdk` 2.26 in poi e richiedono un server Twenty
dalla versione 2.26 o successiva.
</Note>
Il server predispone anche una **vista di sistema** su ogni oggetto: la vista elenco principale (`All {objectLabelPlural}`, con chiave `ViewKey.INDEX` (derivata con `SYSTEM_VIEW_KEYS.INDEX`)), con una colonna per ogni campo visualizzabile. Come per i campi di relazione di sistema, i loro identificatori sono derivati senza dipendere dai nomi, quindi rinominare un oggetto o un campo non li modifica mai.
Usa `getSystemViewUniversalIdentifier` per risolvere la vista:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` è l’applicazione proprietaria dell’**oggetto**, che è ciò in base a cui viene definito lo spazio dei nomi della vista.
* `objectUniversalIdentifier` è l’oggetto che la vista elenca.
* `viewKey` è il discriminatore della vista di sistema: `SYSTEM_VIEW_KEYS.INDEX` per la vista elenco principale, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` per la vista del widget dei campi della pagina del record. Determina la derivazione; solo `INDEX` è anche memorizzato sulla riga della vista.
L’id risolto funziona ovunque ci si aspetti un `viewUniversalIdentifier`, ad esempio in una voce della sidebar [`NavigationMenuItemType.VIEW`](/l/it/developers/extend/apps/layout/navigation-menu-items). Per aprire semplicemente la lista principale di un oggetto, è preferibile usare `NavigationMenuItemType.OBJECT` con `targetObjectUniversalIdentifier`: non richiede alcuna derivazione.
`getSystemViewFieldUniversalIdentifier` risolve una singola **colonna** su una vista di sistema, a partire dalla vista e dal campo che visualizza:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Nota il primo argomento: una colonna appartiene allo spazio dei nomi dell’applicazione proprietaria del **campo che visualizza**, non di quella proprietaria della vista. Un campo che la tua app aggiunge a un oggetto standard ottiene la propria colonna derivata sotto la tua applicazione, su una vista di proprietà di Twenty.
<Warning>
Le viste di sistema e le loro colonne sono **di proprietà del server**: risolvi i loro identificatori per fare riferimento ad esse, mai per dichiararle. `key` su
[`defineView()`](/l/it/developers/extend/apps/layout/views) è deprecato e
ignorato, quindi una vista del manifest non può mai rivendicare la chiave `INDEX`, e il server predispone già una colonna per ogni campo che aggiungi, pertanto dichiarare un tuo
`defineViewField()` per quello stesso campo su una vista di sistema entra in conflitto con essa.
</Warning>
## Oggetti standard di Twenty
Per un oggetto **standard** di Twenty (Person, Company, Opportunity, …), non hai bisogno di derivare nulla: gli identificatori sono costanti pre-calcolate che puoi importare direttamente, sia per i campi che per le viste.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Usa gli helper sopra quando l’oggetto è uno che **la tua app** definisce con [`defineObject()`](/l/it/developers/extend/apps/data/objects), per cui non esiste alcuna costante di questo tipo.
<Note>
`name` è un campo **predefinito**, non un campo di sistema. Mantiene un proprio identificatore universale hardcoded e non viene risolto tramite
`getFieldUniversalIdentifier`. Sugli oggetti che definisci tu, fai riferimento al campo
`name` tramite l'identificatore che gli hai assegnato in `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Tipi Di Attività Timeline
description: Definire gli eventi di audit automatico e gli eventi espliciti delle app che vengono visualizzati in tempi record.
icon: clock-rotate-left
---
Un tipo di attività della timeline definisce il vocabolario stabile per un evento mostrato nella timeline di un record. Oggetti standard e oggetti app utilizzano lo stesso contratto: un tipo ha un'etichetta e un'icona, può opzionalmente dichiarare quando viene emesso, e può opzionalmente renderizzare attraverso uno dei [componenti anteriori](/l/it/developers/extend/apps/layout/front-components).
<Note>
I tipi di attività temporali sono in beta e stanno arrivando presto a Twenty 2.34. L'API
può evolvere mentre impariamo dai casi d'uso degli sviluppatori di app.
</Note>
Creane uno con il ponteggio:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
O definirlo direttamente:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Eventi automatici ed espliciti
Aggiungi `emit` quando Twenty dovrebbe creare questo tipo automaticamente. `emit.on` supporta `created`, `updated`, `deleted`, `restored`, `linked`, e `unlinked`, mentre `emit.objectUniversalIdentifier` identifica l'oggetto sorgente. Solo un tipo efficace può gestire la stessa chiave di emissione — `on`, oggetto e opzionale attraverso la relazione — in uno spazio di lavoro.
Senza `emit.through`, l'evento è scritto sulla timeline del record di origine. Per adattarlo ai record correlati, imposta `emit.through. elationFieldUniversalIdentifier` o a una relazione diretta tra molti o a una [relazione di giunzione uno-a-molto](/l/it/developers/extend/apps/data/relations#junction-relations) sull'oggetto sorgente. Amante delle relazioni morph ad ogni membro del loro gruppo morph, in modo da una singola dichiarazione può colpire diversi tipi di oggetti.
Per una relazione diretta, la creazione o il ripristino del record di origine produce `linked`, eliminando produce `unlinked`, e ripuntando la relazione produce `unlinked` sul target precedente più `linked` sul nuovo obiettivo. Altri aggiornamenti di origine non producono eventi di collegamento. Questo è il contratto utilizzato dagli allegati, il cui obiettivo è una relazione diretta morfologica.
Per una relazione di giunzione, `universalSettings.junctionTargetFieldUniversalIdentifier` identifica la relazione dall'oggetto di giunzione al bersaglio. La creazione o l'eliminazione di una riga di giunzione produce `linked` o `unlinked`. Il riorientamento di entrambi i lati di una riga di giunzione produce anche un evento di collegamento; gli aggiornamenti di campi di giunzione non correlati non.
`linked` e `unlinked` gli eventi richiedono `emit.through`, perché il loro trigger è una modifica al rapporto diretto o di giunzione configurato.
Ad esempio, questo è lo stesso contratto generico utilizzato da note, compiti, messaggi e eventi di calendario:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Per impostazione predefinita, viene emesso un evento `updated` through per ogni aggiornamento della sorgente. Imposta `emit.through.triggerFieldUniversalIdentifiers` quando solo le modifiche ai campi sorgente selezionati devono apparire nelle timeline di destinazione.
Ometti `emit` per un evento di dominio esplicito creato dalla tua funzione logica. In questo modo si evita di produrre sia una riga di audit automatica sia una riga esplicita per la stessa operazione.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Crea eventi espliciti con [`createTimelineActivity()`](/l/it/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). Il codice dell'app usa identificatori universali stabili; Twenty risolve gli ID dei metadati specifici dell'installazione.
## Rendering personalizzato
Senza un componente front, Twenty esegue il rendering di una riga generica nativa a partire dall'etichetta del tipo, dall'icona e dai metadati dell'oggetto collegato. Questo funziona per oggetti standard e personalizzati senza un renderer specifico dell'oggetto.
Per dettagli personalizzati, imposta `frontComponentUniversalIdentifier` su un componente front appartenente alla stessa app:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
La riga nativa rimane la presentazione compressa. Twenty monta il componente front solo dopo che l'utente espande quella riga, evitando una sandbox e un worker per ogni evento visibile. All'interno del componente, chiama `useTimelineActivityId()` da `twenty-sdk/front-component` per leggere l'ID della riga e recuperare tutti i dati necessari alla presentazione. Restituisce `null` quando il componente viene sottoposto a rendering al di fuori di una riga della timeline.
## Sovrascrivere la presentazione di un'altra applicazione
Un'app possiede contratti automatici della timeline per i propri oggetti. Per personalizzare un evento su un oggetto posseduto da un'altra app, dichiara esplicitamente il tipo esistente con `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Una sostituzione deve includere `emit`, perché sostituisce uno slot di emissione automatica esistente anziché un tipo solo esplicito. Il tipo a cui viene fatto riferimento deve appartenere all'applicazione dell'oggetto di destinazione e descrivere la stessa azione e lo stesso percorso. La rimozione dell'app che effettua la sostituzione ripristina il tipo di base; la rimozione del tipo di base disabilita la sostituzione.
## Sostituzioni e disattivazione dell'area di lavoro
Gli amministratori dell'area di lavoro possono modificare la presentazione di un tipo o disattivare i relativi eventi automatici ed espliciti senza effettuare un fork dell'applicazione. Usa la mutazione `updateTimelineActivityType` dell'API dei metadati con l'ID del tipo specifico dell'installazione:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label` e `icon` vengono archiviati come sostituzioni dell'area di lavoro, pertanto gli aggiornamenti successivi dell'applicazione non sovrascrivono le scelte dell'amministratore. Imposta `isActive` su `false` per interrompere l'emissione automatica e rifiutare nuovi eventi espliciti di quel tipo. Le righe esistenti rimangono visibili.
Ripristina le impostazioni predefinite dell'applicazione, incluso lo stato attivo, con:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| ------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- |
| `universalIdentifier` | Sì | UUID stabile per il tipo tra installazioni e aggiornamenti |
| `name` | Sì | Nome programmatico locale dell'app; deve essere univoco all'interno dell'app |
| `label` | Sì | Testo dell'azione rivolto all'utente usato dal renderer nativo |
| `icon` | No | Nome dell'icona Twenty visualizzata accanto all'evento |
| `emit` | No | Dichiarazione di emissione automatica; ometti per i tipi solo espliciti |
| `emit.on` | Con emit | Azione di audit che causa la scrittura di questo tipo |
| `emit.objectUniversalIdentifier` | Con emit | Oggetto i cui record emettono questo tipo |
| `emit.through.relationFieldUniversalIdentifier` | No | Relazione diretta o di giunzione utilizzata per ventilare le timeline correlate |
| `emit.through.triggerFieldUniversalIdentifiers` | No | Campi sorgente che possono attivare un evento `updated` through |
| `frontComponentUniversalIdentifier` | No | Componente anteriore di proprietà dell'app montato quando la riga è espansa |
| `replacesTimelineActivityTypeUniversalIdentifier` | No | Tipo di emissione esistente sostituito; richiede `emit` |
Venti istantanee identità semantica e fallback durevoli della presentazione quando viene creato un evento. Le righe storiche mantengono la loro azione originale e il significato dell'oggetto. Mentre il tipo è installato, la sua attuale etichetta, icona e componente frontale rendono live; dopo la disinstallazione, lo snapshot fallback mantiene la riga leggibile. La risoluzione utilizza l'identificatore universale, quindi la presentazione sopravvive anche reinstallando l'app.

View File

@@ -0,0 +1,102 @@
---
title: Concetti
description: Come funzionano le app Twenty — modello di entità, sandboxing e ciclo di vita dell’installazione.
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à dell’app, ruolo predefinito, variabili | [Configurazione dell’applicazione](/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 all’interno di Twenty | [Componenti front-end](/l/it/developers/extend/apps/layout/front-components) |
| **Dipendenze condivise** | Dipendenze condivise dai componenti front-end dell'app, dichiarate in package.json | [Dipendenze condivise](/l/it/developers/extend/apps/layout/front-components#sharing-dependencies-across-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, che agisce come la persona che ha attivato l'esecuzione o come l'app stessa. Consulta [L'accesso utilizzato da una chiamata](/l/it/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **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. Vengono inseriti due token di runtime, entrambi derivati dal ruolo dichiarato con `defineApplicationRole()`; quello predefinito è ulteriormente limitato a ciò che la persona che attiva l'esecuzione può fare quando qualcuno è responsabile dell'esecuzione.
## 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] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ 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 e disinstallazione** — funzioni opzionali che vengono eseguite durante l’installazione o subito prima della rimozione. 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à dell’applicazione, ruolo predefinito e hook di installazione e disinstallazione.
</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>

View File

@@ -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>

View File

@@ -0,0 +1,79 @@
---
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
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.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/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Una pagina di benvenuto iniziale: un front component renderizzato da un page layout autonomo, raggiungibile dalla sidebar. |
| `src/__tests__/` | Un test unitario più un test di integrazione (con il relativo setup globale) che sincronizza l'app con un server reale. |
| `public/` | Asset statici (immagini, font) serviti insieme alla tua app. |
| `AGENTS.md` / `CLAUDE.md` | Linee guida per gli agenti di codice AI che lavorano sull'app. |
| `CHANGELOG.md` / `SETUP.md` | Changelog delle modifiche importanti e istruzioni di configurazione per lo sviluppo locale. |
<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.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
Lo scaffolder blocca `twenty-sdk` e `twenty-client-sdk` alla propria versione — mantieni i due allineati durante l'aggiornamento.
* **`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 dev: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`. Mantieni l'albero snello: le `dependencies` vengono installate in un livello di runtime con un limite di 250 MB non compressi e una sincronizzazione o un'installazione non va a buon fine con un errore di dimensione delle dipendenze oltre tale soglia — le librerie UI e gli strumenti di sviluppo appartengono a `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Avvio rapido
icon: rocket
description: Crea la tua prima app Twenty in pochi minuti.
---
## Prerequisiti
* **Node.js 24.5+** — [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 docker:start` | 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
```
Lo scaffolder è non interattivo: il nome della directory diventa il nome dell'app. Passa `--display-name` e `--description` per personalizzare i metadati generati (puoi anche modificarli in seguito in `src/constants/universal-identifiers.ts`). Questo genera un progetto TypeScript in `my-twenty-app/` con un `application-config.ts` iniziale, un ruolo predefinito, workflow CI/CD 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 scaffolder avvia un'istanza per te: con Docker in esecuzione, scarica l'immagine `twentycrm/twenty-app-dev`, la avvia sulla porta `2020` e autentica la CLI sullo spazio di lavoro demo prepopolato (`tim@apple.dev`) — non è necessario effettuare l'accesso.
Per connetterti invece a un server Twenty esistente, passa `--url \<your-server-url>`. I server remoti eseguono l'autenticazione con OAuth: si apre un browser così puoi effettuare l'accesso e fare clic su **Authorize**, concedendo alla CLI l'accesso al tuo spazio di lavoro. (Puoi anche scegliere di utilizzare OAuth in locale con `--authentication-method oauth` — accedi con `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Schermata di accesso di Twenty" />
</div>
<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
Usa `plan` e `apply` per eseguire la stessa pipeline una volta, senza watcher:
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| 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 apply` | Singola build + sincronizzazione, termina con codice `0` in caso di successo, `1` in caso di errore. Richiede conferma per le modifiche distruttive (passa `--force` per saltarla). | CI, hook pre-commit, agenti IA, flussi di lavoro scriptati. |
| `yarn twenty plan` | Crea e stampa le modifiche ai metadati **senza applicarle**. | Ispezionare quali modifiche verrebbero apportate da una sincronizzazione prima di confermarla. |
Tutte le modalità richiedono un remoto autenticato. Vedi [Sincronizzazione e ripristino](/l/it/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) per maggiori informazioni su `plan`.
<Note>
`yarn twenty dev --once` e `yarn twenty dev --once --dry-run` sono alias deprecati di `yarn twenty apply` e `yarn twenty plan`.
</Note>
### Opzioni della modalità di sviluppo
| Opzione | Descrizione |
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
| `--force` | Applica modifiche distruttive (eliminazioni) senza conferma. |
| `--debounceMs \<ms>` | Imposta il ritardo di debounce delle modifiche ai file in millisecondi (predefinito: `1000`). |
| `--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 e disinstallazione, 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>

View File

@@ -0,0 +1,63 @@
---
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 all’uso 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` |
| Scheda layout di pagina | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Voce del menu comandi | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Campo vista | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Provider di connessione | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Tipo di attività della timeline | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<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
```

View File

@@ -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.5+ (`engines.node: ^24.5.0`). 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 dev: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).

View File

@@ -0,0 +1,118 @@
---
title: Voci del menu comandi
description: Esponi i componenti front-end come azioni rapide e voci del menu comandi (Cmd+K) con defineCommandMenuItem.
icon: terminal
---
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',
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 | **Deprecato** — ignorato a favore dell'icona dell'applicazione; la build emette un avviso se impostato |
| `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), `'GLOBAL_OBJECT_CONTEXT'` (solo sulle pagine con un contesto oggetto — pagine indice e pagine record), `'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: un componente headless renderizza `<Command execute={...} />` (vedi [l'esempio completo](/l/it/developers/extend/apps/layout/front-components#sdk-command-components)), e la voce di menu del comando punta ad esso:
```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',
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 |

View File

@@ -0,0 +1,849 @@
---
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 viene eseguito all'interno di un iframe con origine opaca e in sandbox, ma la sua interfaccia utente continua a essere renderizzata in modo nativo nella pagina invece di essere confinata in quell'iframe.
<Warning>
I componenti Front sono ancora in fase di sviluppo attivo. Il tuo codice viene eseguito su un DOM parziale, non su una vera pagina del browser, quindi gli utilizzi avanzati possono fallire, spesso in modo silenzioso. Vedi [Limitazioni attuali](#current-limitations).
</Warning>
## Dove possono essere utilizzati i componenti front-end
I componenti front-end possono essere renderizzati in tre 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.
* **Impostazioni dell'app** — Definito con [`defineSettingsFrontComponent()`](#custom-settings-component), il componente front-end viene renderizzato come una sezione all'interno della scheda **Settings** dell'app, al posto dell'interfaccia utente predefinita per la configurazione delle variabili.
Un componente front da solo non è raggiungibile dall'interfaccia utente: devi renderlo visibile. I tre 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.
* **Definirlo con [`defineSettingsFrontComponent()`](#custom-settings-component)** — lo renderizza come una sezione all'interno della scheda **Settings** dell'app, al posto dell'interfaccia utente predefinita per la configurazione delle variabili.
## 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 nell’angolo 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',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Dopo la sincronizzazione con `yarn twenty dev` (o eseguendo una volta sola `yarn twenty apply`), 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.
## Componente delle impostazioni personalizzato
Per sostituire l'interfaccia utente di configurazione delle variabili generata automaticamente nella scheda **Settings** della tua app con il tuo componente, definiscilo con `defineSettingsFrontComponent` invece di `defineFrontComponent`. Utilizza gli stessi [campi di configurazione](#configuration-fields) (tranne `isHeadless`, che non è accettato poiché un componente delle impostazioni renderizza sempre un'interfaccia utente visibile) e inoltre contrassegna il componente come l'interfaccia delle impostazioni dell'app.
Il componente viene renderizzato come una sezione **all'interno** della scheda Settings, non come una sostituzione dell'intera scheda. Le sezioni gestite dal sistema di Twenty — aggiornamento automatico, URL dell'app e connessioni — vengono sempre renderizzate sopra di essa e non possono essere sovrascritte dall'app.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
È consentito un solo componente di front-end delle impostazioni per app; dichiararne più di uno fa fallire la build. Quando presente, la scheda **Settings** dell'app renderizza questo componente al posto dell'interfaccia utente di configurazione delle variabili predefinita.
## 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 { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
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/front-component`:
* **`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 pagina del pannello laterale. Le props dipendono da `page` — ad esempio `ViewRecord` accetta `recordId` + `objectNameSingular` (più un id `tab` opzionale per aprire il record in una scheda specifica), altre pagine accettano `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/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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',
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/front-component';
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,
});
```
Ed ecco un esempio che usa `CommandOpenSidePanelPage` per aprire il record corrente nel pannello laterale in una scheda specifica. `tab` è un id di scheda di layout della pagina (i layout predefiniti usano id come `company-tab-emails` o `company-tab-timeline`; i layout personalizzati usano l'id della scheda stessa). Se l'id non esiste nel layout del record, viene invece aperta la scheda predefinita:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Chiamare una funzione logica
I componenti front vengono eseguiti lato browser in un Web Worker in sandbox all'interno di un iframe con origine opaca, 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` è raggiungibile tramite HTTP al relativo percorso della route. `RestApiClient` tratta i percorsi che iniziano con `/s/` come route dell’app, li risolve nell’URL da cui vengono servite le tue funzioni e li autentica con `TWENTY_APP_ACCESS_TOKEN`.
> **Su Twenty Cloud, le funzioni logiche attivate tramite HTTP sono servite su un dominio dedicato per ogni workspace** in `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Per i chiamanti esterni, copia l’URL esatto dalle impostazioni del **trigger HTTP** della funzione o dalla scheda **Settings** dell’applicazione.
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 { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().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 a `RestApiClient` è la proprietà `httpRouteTriggerSettings.path` della funzione di logica, con prefisso `/s`. Mantieni `isAuthRequired: true`; il `TWENTY_APP_ACCESS_TOKEN` che Twenty genera per il tuo componente autentica la richiesta:
```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_APP_ACCESS_TOKEN` viene inserito automaticamente — vedi [Variabili dell’applicazione](#application-variables). Poiché le variabili di applicazione segrete non vengono mai esposte ai front component, mantieni le chiavi API e altra logica sensibile all’interno della funzione logica, non nel front component.
</Note>
### Chiamare le Twenty REST API
Per chiamare le route HTTP dell’app o leggere e scrivere record di Twenty da un front component, usa `RestApiClient` da `twenty-client-sdk/rest`. Invia i percorsi `/s/...` alla base URL delle funzioni del tuo workspace e tutti gli altri percorsi, inclusi `/rest/...`, a `TWENTY_API_URL`.
Agisce sempre come la persona che visualizza la pagina. `runAs: 'application'` è un'opzione esclusiva delle funzioni logiche: un componente non riceve mai il token della tua applicazione, quindi richiederlo qui genera un errore. Inserisci il lavoro che richiede l'accesso dell'app dietro una funzione logica e chiama invece quella.
| 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 |
| `resolveUrl(path, options?)` | Risolve un percorso nel suo URL completo senza inviare una richiesta (per i link) |
`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://myworkspace.twenty.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 people = await client.get('/rest/people', {
query: { limit: 10 },
});
} 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,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
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 |
| `useTimelineActivityId()` | `string` o `null` | L'ID dell'attività della cronologia corrente durante il rendering di una riga della cronologia personalizzata |
| `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>
`getApplicationVariable` restituisce sempre una **stringa** (o `undefined`), indipendentemente dal `type` dichiarato della variabile. La stringa viene serializzata in modo coerente in base al tipo (booleani come `"true"` / `"false"`, numeri come stringhe decimali, array / oggetti come JSON), lo stesso formato usato per la logic-function `process.env` — esegui il parsing manualmente (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Vedi [Tipi di variabili](/l/it/developers/extend/apps/config/application#variable-types).
Le seguenti variabili di sistema sono sempre disponibili tramite `process.env`:
| Variabile | Descrizione |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | URL di base dell'API Core di Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token di breve durata limitato al ruolo della persona che ha effettuato l'accesso, intersecato con quello della tua app, quindi un componente non può mai fare più di quanto possa fare la persona che lo visualizza |
### `TWENTY_FUNCTIONS_URL`
Twenty inserisce anche `TWENTY_FUNCTIONS_URL` nei front component e nelle funzioni di logica: la base URL da cui vengono servite le funzioni di logica attivate tramite HTTP della tua app.
Esiste perché quell’URL non coincide sempre con il server di Twenty stesso. Su Twenty Cloud, le route dell’app sono servite su un dominio dedicato per ogni workspace (`https://\<your-workspace-subdomain>.withtwenty.com`, oppure il dominio pubblico primario dell’applicazione quando è configurato) in modo che le risposte create dall’app vengano eseguite in un’origine isolata invece che nell’origine dell’app Twenty. Le istanze self-hosted e locali servono le route dell’app con il prefisso `/s` direttamente sul server e potrebbero non impostare affatto la variabile. Dato che la base URL varia per workspace e per istanza, il tuo codice non può codificarla in modo statico — il server inserisce il valore corretto a runtime.
Raramente devi leggerla direttamente. Chiama le tue route tramite `RestApiClient` con un percorso con prefisso `/s/` e il client risolve l’URL per te: rimuove il prefisso `/s` e punta a `TWENTY_FUNCTIONS_URL`, usando `\<TWENTY_API_URL>/s` come fallback quando la variabile non è impostata. Usa `resolveUrl('/s/\<path>')` per ottenere l’URL assoluto senza inviare una richiesta, ad esempio per un link. Leggi direttamente la variabile solo quando costruisci manualmente un URL:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## 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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Carica un `Blob` in un campo FILES; restituisce `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` oppure `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` |
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 { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
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,
});
```
### Archiviazione
`localStorage` e `sessionStorage` funzionano come in una pagina normale, con l’API sincrona standard. Le tue chiavi sono limitate all’installazione della tua app e all’utente connesso: nessun’altra app può leggerle e un altro utente che effettua l’accesso dallo stesso browser parte da un archivio vuoto. I valori scritti in `localStorage` rimangono sul dispositivo tra un ricaricamento e l’altro; `sessionStorage` dura per l’intera sessione del browser.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty memorizza i valori per conto della tua app, quindi una scrittura viene applicata immediatamente in locale e salvata in background. Le letture non attendono mai l’host. Non viene sincronizzato nulla: i valori non seguono l'utente su un altro browser o macchina, quindi utilizza il [key-value store](/l/it/developers/extend/apps/logic/key-value-store) di una logic function per qualsiasi cosa che debba sopravvivere a un cambio di dispositivo.
Le scritture sono limitate e ogni limite conta i caratteri anziché i byte: le chiavi sono lunghe al massimo 512 caratteri, un singolo valore al massimo 262.144 caratteri e ogni archivio al massimo 1.048.576 caratteri per app e utente. Una scrittura che supera un limite genera un `QuotaExceededError`, come nell'API del browser.
### 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 } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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,
});
```
Mostralo con una [voce del menu dei comandi](/l/it/developers/extend/apps/layout/command-menu-items) limitata alle selezioni di record:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## 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.
## Condivisione delle dipendenze tra front component
Per impostazione predefinita, ogni front component include nel bundle la propria copia delle librerie che importa, quindi un'app con cinque componenti distribuisce React cinque volte. Dichiara le dipendenze condivise nel `package.json` della tua app per creare il bundle di queste librerie una sola volta e fare in modo che ogni componente dell'app le carichi da un unico file in cache:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Ogni componente importa quindi le sue dipendenze esattamente come prima — non cambia nulla nel codice del tuo componente:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Alcune cose da sapere:
* **Un solo bundle di dipendenze condivise per app.** Il bundle è costruito a partire dalle dipendenze della tua app, così mantieni il pieno controllo delle versioni che distribuisci.
* **Elenca gli specificatori esatti che importi.** `twenty-ui/input` e `twenty-ui/display` sono due voci; il solo nome del pacchetto non copre i suoi sottopercorsi. L'elenco di `react` copre automaticamente `react/jsx-runtime`.
* **Condividi `react-dom/client` insieme a `react`.** Ogni componente effettua il rendering tramite `createRoot`, quindi ometterlo significa che ogni componente include ancora nel bundle React DOM.
* **Il bundle è in cache.** È servito tramite un URL con content-hash con una cache immutabile a lunga durata, quindi viene scaricato una sola volta e riutilizzato in tutti i componenti dell'app finché una delle sue dipendenze non cambia.
* **I componenti che non importano nessuno dei pacchetti condivisi non lo scaricano mai.**
## Stile
I componenti front-end supportano diversi approcci di styling. Puoi usare:
* **Stili inline** — `style={{ color: 'red' }}`
* **Componenti di Twenty UI** — la libreria di componenti di Twenty; vedi [Uso dei componenti di Twenty UI](#using-twenty-ui-components) di seguito
* **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
## Uso dei componenti di Twenty UI
Twenty distribuisce la sua libreria di componenti come pacchetto [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). I componenti front-end possono usarlo per pulsanti, tag, pillole di stato, chip, avatar, icone, tipografia e token del tema che si adattano automaticamente al tema chiaro e scuro dell’area di lavoro.
### Installazione
Aggiungi il pacchetto alla tua app, bloccato alla versione con cui la tua istanza di Twenty viene distribuita:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` è incluso nel tuo componente front-end in fase di build, quindi deve essere solo una dipendenza della tua app: non c’è nulla da configurare a runtime.
### Importare i componenti
Importa dal sottopercorso corrispondente invece che dalla root del pacchetto, in modo che solo i componenti che usi finiscano nel tuo bundle:
| Sottopercorso | Cosa esporta |
| --------------------------- | ------------------------------------------------ |
| `twenty-ui/input` | `Button` e campi di input del form |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` e altro |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` e altro |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` e altro |
| `twenty-ui/icon` | Componenti `Icon*` (ad es. `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### Icone
Importa le singole icone da `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Ogni icona nominata è sottoposta a tree-shaking, quindi importarne alcune aggiunge poco al tuo bundle. Evita `IconsProvider`, `useIcons` e `iconsState`: includono l’intero set di icone Tabler (diversi MB).
### Temi e token del tema
I componenti Twenty UI si adattano automaticamente al tema chiaro e scuro dell’area di lavoro: il renderer applica lo schema di colori attivo sull’host e i componenti risolvono i loro colori rispetto ad esso.
Per usare gli stessi design token nei tuoi stili inline, chiama l’hook `useTheme()`. Restituisce i token del tema di Twenty (spaziatura, colori, raggi, font) collegati al tema attivo, senza bisogno di configurare `ThemeProvider` nel tuo componente:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Poiché `useTheme()` è un hook, leggi i token all’interno del corpo del componente, così i valori riflettono sempre il tema attivo in tempo reale. La stessa mappa di token è anche esportata come costante `themeCssVariables`, ma nei componenti front-end è preferibile usare `useTheme()`: una costante a livello di modulo che dereferenzia `themeCssVariables` può essere undefined mentre il manifest dell’app viene estratto.
Per diramare esplicitamente in base allo schema attivo, leggilo con `useColorScheme()` da `twenty-sdk/front-component`, che restituisce `'light'` o `'dark'`.
## Limitazioni attuali
I componenti Front sono in fase di sviluppo attivo. Il rendering, lo styling, la gestione degli eventi, la misurazione degli elementi e lo storage del browser funzionano bene. Qualsiasi cosa che vada *oltre* questi aspetti (chiamare un metodo del DOM su un ref, osservare il ridimensionamento degli elementi, creare un portale al di fuori del tuo albero) oggi manca o è incompleta, e la maggior parte di queste operazioni fallisce silenziosamente: nessuna eccezione e nessun errore TypeScript, poiché lo scaffold è tipizzato contro il DOM completo del browser.
Se una di queste limitazioni ti blocca, [apri una issue](https://github.com/twentyhq/twenty/issues/new/choose) così la sua priorità aumenta.
### Layout e misurazione
Gli elementi possono misurare sé stessi: l'host rispecchia la geometria nella sandbox, quindi le letture vengono gestite localmente ma possono essere non aggiornate fino a un frame, e la prima lettura di un elemento mai misurato restituisce zeri. Dopo la scrittura, rileggi in una callback di `requestAnimationFrame` o in un effetto.
| API | Cosa succede |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funzionano, dallo specchio; assegnare `scrollTop` / `scrollLeft` non ha effetto (no-op) |
| `getClientRects()`, `window.matchMedia()` | Genera un’eccezione |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funzionano, riportando il **viewport del browser**; la dimensione del tuo widget è `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Restituisce solo gli stili inline dell’elemento, mai la cascata calcolata dall’host |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (le verifiche con `typeof` funzionano) |
| `MutationObserver` | Funziona, incluso `subtree`, `attributeFilter`, i vecchi valori e `takeRecords()` |
Il posizionamento tramite `getBoundingClientRect` ora funziona, ma qualsiasi cosa che osservi i cambi di dimensione tramite `ResizeObserver` (il `ResponsiveContainer` di recharts, l’`autoUpdate` di Floating UI) continua a non funzionare. Preferisci comunque il CSS per il layout: il tuo stylesheet raggiunge la pagina reale, quindi flexbox, grid, `aspect-ratio`, `clamp()` e `@container` si comportano normalmente, senza lag del frame.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` e `queueMicrotask` funzionano senza il prefisso `window.`. Solo `window.requestAnimationFrame(...)` e simili generano un’eccezione.
</Note>
### Accesso al DOM
Un `ref` ti restituisce un elemento sandbox, non un `HTMLElement`.
| Cosa scrivi | Cosa succede | Usa invece |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Genera un’eccezione | Componenti controllati; leggi i valori da `event.target` |
| `element.classList.add(...)` | Genera un’eccezione (`classList` è `undefined`) | Costruisci tu stesso la stringa `className` |
| `document.createTreeWalker()` | Genera un’eccezione | `querySelector()` / `querySelectorAll()` e `getElementById()` funzionano; anche `getElementsByClassName()` funziona, ma restituisce una collezione statica, non una `HTMLCollection` live |
| `document.activeElement` | Sempre `undefined` | Tieni traccia del focus con `onFocus` / `onBlur` |
| `canvas` | Non renderizza nulla, nessun errore | SVG, oppure disegna fuori dallo schermo e mostra il risultato in un elemento `img` |
| `createPortal(node, document.body)` | Non renderizza nulla, mentre `isConnected` segnala successo | Overlay inline con `position: absolute`, oppure passa alla libreria un tuo elemento container |
Il gap del portale è il motivo per cui i popover di Radix, Headless UI, MUI e react-select non renderizzano nulla per impostazione predefinita. La maggior parte accetta una prop container; indirizzala a un elemento che hai renderizzato.
### Eventi
Mouse, puntatore, touch, drag, tastiera, focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` e `animationend`/`transitionend` passano all'host, più alcuni per elemento: `load`/`error` su `img`, appunti e composizione su `input`/`textarea`, media su `video`/`audio`, `toggle` su `details`/`dialog`. Tutto il resto (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, acquisizione del puntatore, `onLoad` su `img`) viene scartato senza alcun avviso.
`document.addEventListener()` e `window.addEventListener()` si registrano senza errori e non vengono mai attivati, motivo per cui un drag si interrompe non appena il puntatore lascia l'elemento da cui è partito. `event.preventDefault()` non viene propagato nemmeno; l'invio dei form, `dragover`/`drop` e i clic sui link sono già protetti per te.
### Attributi e stile
Ogni elemento inoltra le proprie proprietà al DOM host (`href` su `a`, `src`/`alt` su `img`, `value`/`placeholder`/`disabled` su `input`, e così via), più un insieme comune su ogni elemento: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` e qualsiasi attributo `aria-*` / `data-*` (con trattino, quindi `ariaLabel` viene scartato). Qualsiasi cosa al di fuori di ciò viene silenziosamente scartata, quindi esprimi lo stato personalizzato come `data-*`.
Il CSS del componente, che provenga da `import './styles.css'`, da CSS-in-JS o da un elemento `style`, viene iniettato nell'`head` della pagina host **senza ambito**. Quindi i nomi delle classi entrano in conflitto con quelli di Twenty (aggiungi un prefisso e non scrivere mai `div { ... }` come selettori), e `@media` corrisponde alla finestra del browser piuttosto che al tuo widget (usa `@container` con il tuo `container-type`). Le prop `style` inline non sono interessate.
### Storage e rete
`localStorage` e `sessionStorage` sono forniti da Twenty anziché dal browser: il componente viene eseguito in un worker con un'origine opaca, quindi l'host memorizza i valori per conto della tua app. Vedi [storage](#storage) per il loro ambito e i relativi limiti. IndexedDB, i cookie, la Cache API e `BroadcastChannel` restano non disponibili. Per rendere persistente lo stato tra i dispositivi, chiama una [logic function](/l/it/developers/extend/apps/logic/logic-functions) e utilizza il suo [key-value store](/l/it/developers/extend/apps/logic/key-value-store).
`fetch` funziona, con alcune avvertenze:
* Le chiamate alla Twenty API e alle route della tua app sono proxyate dall'host, quindi preferisci [`RestApiClient`](#calling-the-twenty-rest-api). Nelle chiamate proxyate, `AbortSignal` e le altre opzioni di `RequestInit` vengono scartate, e sono supportati solo body di tipo `string` e `URLSearchParams`.
* Le altre origini escono dalla sandbox con `Origin: null`, quindi un'API di terze parti risponde solo se invia `Access-Control-Allow-Origin: *`. Invece, chiamala da una logic function.
* `fetch('/rest/people')` non viene mai associato alla Twenty API, perché la sandbox non ha un URL di pagina rispetto a cui risolvere un percorso relativo.
### Acquisizione multimediale
`navigator.mediaDevices.getUserMedia()` e `MediaRecorder` funzionano all'interno dei front components tramite sandbox polyfill, quindi il codice di registrazione standard viene eseguito senza modifiche e `MediaRecorder.isTypeSupported` restituisce risultati per le combinazioni comuni di contenitore/codec. Gli oggetti di constraint dettagliati per `getUserMedia` vengono accettati ma non inoltrati — l'host acquisisce con i propri valori predefiniti per i tipi richiesti — e può essere attiva una sola acquisizione alla volta tra le applicazioni. Archivia un `Blob` registrato con la funzione host `uploadFile`.
### Altre lacune
* **Contenuti dei file.** Un `input` di tipo `file` fornisce al tuo gestore solo i metadati del file, non i byte, quindi `FileReader` non è disponibile. Per caricare un `Blob` già a disposizione del tuo codice — ad esempio uno prodotto da `MediaRecorder` — usa la funzione host `uploadFile`.
* **Payload di drag-and-drop.** Gli eventi di drag vengono attivati, ma `event.dataTransfer` è `undefined`.
* **Built-in di Node.** `fs`, `path` e `node:crypto` fanno fallire la build, quindi sposta quel lavoro in una [logic function](/l/it/developers/extend/apps/logic/logic-functions). Web Crypto, `fetch`, `TextEncoder` e `URL` sono disponibili.
* **`iframe`** viene sempre nuovamente messo in sandbox senza `allow-same-origin`, quindi un embed che dipende dalla propria sessione viene renderizzato come disconnesso. Non ha nemmeno `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Voci del menu di navigazione
description: Aggiungi voci personalizzate alla barra laterale dell’area 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 un’etichetta | `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 l’ordinamento nella barra laterale.
* L'enum contiene anche `NavigationMenuItemType.RECORD`, utilizzato internamente per i preferiti impostati dall'utente sui record — non è utilizzabile dal manifest di un'app (non esiste alcun campo per fare riferimento a un record).
* `icon` e `color` sono opzionali e personalizzano l’aspetto della voce.
* `folderUniversalIdentifier` è inoltre disponibile su qualsiasi elemento per annidarlo all’interno di un genitore di tipo `FOLDER`.
<Note>
**Problema comune:** creare un oggetto senza una voce del menu di navigazione associata rende quell’oggetto irraggiungibile dalla barra laterale. A meno che non si tratti di un oggetto tecnico/interno, ogni oggetto personalizzato dovrebbe avere una voce nella barra laterale che punti ad esso.
</Note>

View File

@@ -0,0 +1,59 @@
---
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` |
| **Impostazioni dell'app** | Una sezione di configurazione personalizzata all'interno della scheda Impostazioni dell'app, al posto dell'interfaccia utente predefinita delle variabili | `defineSettingsFrontComponent` |
| **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.
Sono ancora in fase di sviluppo attivo: la sandbox implementa un DOM parziale, quindi gli usi avanzati possono non funzionare. Vedi [Limitazioni attuali](/l/it/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
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 l’intero 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 l’intera 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.VERTICAL_LIST,
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` è uno tra `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` o `'STANDALONE_PAGE'`. Usa `'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 `title`, `position` e `layoutMode`: `VERTICAL_LIST` per le pagine dei record e le pagine autonome, `GRID` per le dashboard e `CANVAS` per un singolo widget che deve riempire il viewport della tab. Una tab `VERTICAL_LIST` dispone i widget verticalmente. I widget integrati che gestiscono il proprio scorrimento, come cronologie, file, note, attività e flussi di lavoro, riempiono un viewport; campi, componenti front, grafici e altri widget fit-content vengono visualizzati all'altezza del contenuto o configurata. Una tab `GRID` dispone sempre i widget come schede su una griglia di 12 colonne. Un widget `CANVAS` non ha una posizione esplicita; se una scheda canvas contiene più widget, questi vengono renderizzati alla loro altezza del contenuto anziché riempire il viewport.
* Imposta esplicitamente `layoutMode`. Ometterlo imposta `VERTICAL_LIST` su una `STANDALONE_PAGE` e `GRID` altrove, il che raramente è ciò che vuoi per la pagina di un record.
* 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.
* Un widget `FRONT_COMPONENT` può impostare `headerCommandMenuItemUniversalIdentifiers` su un array ordinato di identificatori universali degli elementi del menu di comandi della stessa app. Queste azioni vengono visualizzate come pulsanti con icona nell'intestazione della scheda del widget e mantengono la disponibilità a livello di comando e i controlli delle autorizzazioni. Gli identificatori devono essere univoci e devono essere risolti quando l'app viene installata.
* `position` sulle schede controlla il loro ordine. Usa valori più alti (ad es., 50) per posizionare le schede personalizzate dopo quelle integrate.
### Widget dei campi
Un widget `FIELD` esegue il rendering di un campo del record. Per i campi di relazione può anche incorporare un elenco di record correlati:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` accetta l'identificatore universale di un campo sull'oggetto del layout.
* `fieldDisplayMode` può essere uno tra `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` o `'TABLE'`. `TABLE` incorpora una vista che elenca i record di un campo di relazione uno-a-molti.
* `nestedRelationFieldMetadataId` è facoltativo e accetta l'identificatore universale di un campo di relazione uno-a-molti sull'oggetto di destinazione della relazione, per elencare i record a due salti di relazione di distanza (ad es. una pagina Company che elenca le opportunità delle persone dell'azienda, o una pagina Person che elenca le opportunità dell'azienda della persona). Il primo salto può essere un campo di relazione uno-a-molti o molti-a-uno, il secondo deve essere uno-a-molti (le relazioni di giunzione non sono supportate) e richiede `fieldDisplayMode: 'TABLE'` — combinarlo con qualsiasi altra modalità di visualizzazione genera un errore di convalida, poiché un widget annidato viene sempre renderizzato come una vista incorporata.
## 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.VERTICAL_LIST,
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 dell’installazione — oppure un layout standard di Twenty o uno definito dalla tua app. I riferimenti tra app a layout di proprietà di un’altra app installata non sono attualmente supportati. Quando il layout di pagina padre manca, l’installazione 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 l’intero layout.

View File

@@ -0,0 +1,113 @@
---
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 fornire viste preconfigurate con la tua app.
Ogni oggetto ottiene automaticamente la sua vista elenco principale (la tabella "All ..." che un elemento di navigazione `OBJECT` apre) fornita dal server — inclusi gli oggetti che la tua app crea, con una colonna per ogni campo dichiarato dalla tua app. Le viste che definisci con `defineView()` sono sempre viste **aggiuntive** rispetto a quella predefinita.
```ts src/views/example-view.ts
import { defineView } 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',
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.
* La vista principale dell'elenco dell'oggetto è di proprietà del server: `key` è deprecato e ignorato, quindi una vista del manifest non potrà mai rivendicarla. Distribuisci un elemento di navigazione `VIEW` se vuoi avere la tua vista a un clic di distanza nella barra laterale.
* `fields` controlla quali colonne compaiono e in quale ordine. Ogni campo fa riferimento a un `fieldMetadataUniversalIdentifier`. Per fare riferimento a un campo di sistema creato automaticamente come `createdAt`, vedi [Targeting System Metadata](/l/it/developers/extend/apps/data/system-fields).
* Puoi anche definire `filters`, `filterGroups`, `sorts`, `groups` e `fieldGroups` per configurazioni più avanzate.
* `position` controlla l'ordinamento quando esistono più viste per lo stesso oggetto.
## Proprietà opzionali
| Proprietà | Valori | Descrizione |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (predefinito), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Come sono disposti i record. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` e `CALENDAR_WIDGET` esistono anche ma sono usati internamente dai widget di layout di pagina.) |
| `visibility` | `ViewVisibility.WORKSPACE` (predefinito), `ViewVisibility.UNLISTED` | Se la vista è elencata per l'intero workspace o nascosta dai selettori. |
| `openRecordIn` | deprecato | Non più letto: il luogo in cui vengono aperti i record è ora una proprietà dell'[oggetto](/l/it/developers/extend/apps/data/objects) (`openRecordIn` in `defineObject()`); in assenza, si applica la preferenza di ciascun membro. |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordinamento predefinito. |
| `isCompact` | `boolean` | Visualizzazione compatta delle righe. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Raggruppa i record (ad es. colonne kanban) per un campo. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Aggregazioni e dimensionamento delle colonne kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Viste calendario: layout, il campo data che posiziona i record e un campo data di fine opzionale. |
Tutti gli enum sopra sono esportati da `twenty-sdk/define`.
## 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-sdk/define';
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 modello canonico: ogni oggetto personalizzato riceve una vista indice predefinita generata automaticamente al momento della creazione, quindi devi solo distribuire la voce della barra laterale che la apre.

View File

@@ -0,0 +1,197 @@
---
title: Job in background
description: Affida il lavoro lungo o soggetto a limitazioni di frequenza ai worker di Twenty accodando un'altra esecuzione di una logic function invece di fare tutto in linea.
icon: layer-group
---
L'esecuzione di una logic function è limitata dal suo `timeoutSeconds` (massimo 900 secondi). Qualsiasi attività che non può essere completata in tale intervallo — una risincronizzazione completa, un fan-out per record, un'API di terze parti che applica limitazioni di frequenza — deve essere suddivisa in esecuzioni più piccole.
`enqueueJobs` fa esattamente questo: chiede ai worker di Twenty di eseguire in un secondo momento una delle logic function della tua app, una volta per payload, ogni esecuzione nel proprio processo con il proprio budget di timeout. Il chiamante restituisce immediatamente.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Mettere in coda esecuzioni
Importa `enqueueJobs` da `twenty-sdk/logic-function`, puntalo all'`universalIdentifier` della logic function da eseguire e passa un payload per ogni esecuzione da mettere in coda.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Ogni esecuzione riceve il proprio payload come argomento dell'handler, esattamente come qualsiasi altro trigger. La destinazione deve appartenere alla **stessa applicazione** del chiamante — l'inserimento in coda della funzione di un'altra app viene rifiutato con `Logic function not found` e non viene inserito nulla in coda. Una singola chiamata accetta fino a `200` payload.
<Note>
`enqueueJobs` restituisce non appena i job sono stati accettati, non quando sono stati eseguiti. Non restituisce i risultati delle destinazioni: fai scrivere a ciascuna destinazione ciò che produce nel [key-value store](/l/it/developers/extend/apps/logic/key-value-store) o in un record dello spazio di lavoro se hai bisogno di leggerlo di nuovo.
</Note>
<Note>
Il precedente helper `enqueueJob`, che mette in coda un singolo job per chiamata, è deprecato. Usa invece `enqueueJobs` con un elenco `payloads` di un solo elemento.
</Note>
## Opzioni del job
Le opzioni si applicano a ogni esecuzione nel batch.
| Opzione | Predefinito | Intervallo | Cosa fa |
| ------------ | ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Tentativi aggiuntivi complessivi nella coda. I nuovi tentativi richiesti dall'applicazione sono limitati a `3`. Aumenta questo valore solo per gli handler che possono essere eseguiti due volte in sicurezza. |
| `delayMs` | `0` | `0`–`604800000` (7 giorni) | Attendi questo intervallo prima che le esecuzioni diventino idonee. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**La priorità non è ancora configurabile.** I job in coda vengono sempre eseguiti alla priorità più bassa, così il lavoro della piattaforma non viene mai ritardato rispetto ai job dell'applicazione. Il controllo sulla priorità sarà disponibile a breve.
</Note>
L'esecuzione in coda eredita l'utente attivo della funzione che l'ha messa in coda, quindi agisce con le stesse autorizzazioni.
## Riprova un errore transitorio
Twenty non ritenta ogni eccezione del codice applicativo. Un normale errore generato viene trattato come un errore permanente. In caso di errore transitorio, una funzione logica in coda può richiedere fino a tre tentativi generando `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Genera direttamente `RetryableLogicFunctionError` quando possibile. Se la estendi, non sostituire il relativo `name`: Twenty riconosce il nome serializzato `RetryableLogicFunctionError` in tutti i runtime di esecuzione.
`retryCount` è `0` per l'esecuzione iniziale e aumenta solo quando il codice applicativo richiede un nuovo tentativo. `maxRetries` è al massimo `3` e può essere inferiore quando il job in coda ha un limite complessivo di tentativi inferiore. Gli errori della piattaforma non aumentano `retryCount`, sebbene consumino comunque il budget di sicurezza complessivo della coda.
La coda ritarda i tentativi con backoff esponenziale e jitter. Il ritardo esatto non è intenzionalmente garantito, quindi il codice applicativo non deve dipendere dal fatto che un nuovo tentativo avvenga in un momento preciso. Una volta raggiunto `maxRetries`, un ulteriore `RetryableLogicFunctionError` viene registrato come errore finale dell'applicazione senza un'altra esecuzione.
<Warning>
I nuovi tentativi rieseguono l'intero gestore e possono verificarsi dopo che alcuni effetti collaterali sono riusciti. Rendi idempotente il gestore prima di richiedere nuovi tentativi.
</Warning>
## Usalo per scorrere una lunga sincronizzazione a pagine
La forma classica è una funzione che mette in coda *se stessa* con il cursore successivo. Ogni esecuzione gestisce una pagina di lavoro ben all'interno del proprio timeout, e la catena si interrompe quando non resta più nulla.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Suddivisione per record
Quando il lavoro è naturalmente per elemento, metti in coda un job per elemento in una singola chiamata e lascia che i worker li elaborino in parallelo invece di ciclare in linea.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Buone pratiche per il lavoro di lunga durata
Due regole coprono quasi ogni job lungo: **usa la ricorsione invece del ciclo** e **elabora un blocco limitato per esecuzione**.
Un'esecuzione che prova a fare tutto è una modalità di errore: raggiunge il timeout e, con un nuovo tentativo, ricomincia tutto da zero. Invece, dimensiona un blocco in modo che finisca comodamente entro `timeoutSeconds`, conserva la tua posizione e metti in coda l'esecuzione successiva.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
Cosa rende tutto questo solido:
* **Dimensiona il blocco a partire dall'elemento più lento, non dalla media.** `CHUNK_SIZE × worst-case item time` deve rientrare in `timeoutSeconds` con un certo margine, altrimenti la parte finale di un blocco va persa quando l'esecuzione viene interrotta.
* **Rendi esplicita la condizione di terminazione.** Usa la ricorsione solo finché torna un blocco completo. Una catena che si interrompe solo su "nessun risultato" continuerà all'infinito se la fonte restituisce mai una pagina corta a metà percorso.
* **Conserva i progressi prima di mettere in coda l'esecuzione successiva,** così un anello fallito riparte dall'ultimo blocco completato invece che dall'inizio.
* **Mantieni ogni blocco idempotente.** Rielaborare un blocco dopo un ritentativo non deve causare scritture duplicate: usa chiavi di scrittura sul record o sull'id esterno che stai elaborando.
* **Preferisci una catena a blocchi a un'unica enorme suddivisione parallela** quando il lavoro colpisce una terza parte con limitazione di velocità: una catena con `delayMs` si autoregola, mentre migliaia di job messi in coda in una volta sola diventano tutti idonei immediatamente.

View File

@@ -0,0 +1,269 @@
---
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',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```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="Esegui una funzione logica al momento della connessione" description="Reagisci non appena viene stabilita una connessione">
Alcuni provider ti forniscono dati al momento della connessione che devi memorizzare prima che la connessione sia utilizzabile — l'esempio classico è Slack, dove la risposta OAuth identifica il `team_id` dell'area di lavoro che fungerà da chiave per gli eventi in ingresso. Imposta `onConnectLogicFunction` per fare riferimento a una funzione logica nella stessa app (tramite il suo `universalIdentifier`), e questa verrà eseguita subito dopo la creazione di `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
L'hook viene eseguito **in modo asincrono nell'area di lavoro che si sta connettendo** (viene messo in coda, non atteso), quindi un hook lento o che fallisce non blocca né interrompe mai il callback OAuth — rendilo idempotente e fai in modo che gestisca autonomamente i propri tentativi di ripetizione. Il gestore riceve:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Da lì usa `getConnection(connectedAccountId)` per leggere il token di accesso aggiornato e chiamare l'API del provider (ad es. Slack `auth.test`) o salvare una mappatura con il [key-value store](/l/it/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Esegui una funzione logica alla disconnessione" description="Effettua la pulizia quando una connessione viene rimossa">
Qualsiasi elemento che un'app rivendica al momento della connessione deve essere rilasciato quando la connessione viene meno. Un'integrazione Slack che, ad esempio, rivendica un `team_id` al momento della connessione deve rilasciare tale rivendicazione, in modo che un'altra area di lavoro possa connettere lo stesso team Slack. Imposta `onDisconnectLogicFunction` per fare riferimento a una funzione logica nella stessa app, e questa verrà eseguita subito dopo l'eliminazione di `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
Come l'hook on-connect, viene eseguito **in modo asincrono nell'area di lavoro in fase di disconnessione** e non blocca mai la disconnessione. L'handler riceve la stessa struttura del payload:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
Il `ConnectedAccount` è già stato rimosso quando l'hook viene eseguito, quindi `getConnection(connectedAccountId)` non restituisce più alcun risultato. Tutto ciò di cui la procedura di cleanup ha bisogno (un `team_id`, un id di sottoscrizione esterna) deve essere stato scritto nello [store chiave-valore](/l/it/developers/extend/apps/logic/key-value-store) al momento della connessione, con chiave `connectedAccountId`.
L'hook viene attivato quando una connessione viene rimossa autonomamente. La disinstallazione dell'app elimina le sue connessioni tramite una cascata del database, quindi in questo caso l'hook non viene eseguito. Dichiara una `uninstallLogicFunction` su `defineApplication` per quel percorso: viene eseguita prima che i metadati dell'app vengano eliminati, così può ancora chiamare `listConnections` ed eseguire il cleanup di ciò che resta.
</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` | Il nome visualizzato della connessione (derivato automaticamente al callback OAuth, rinominabile dall'utente) |
| `handle` | L'email dell'account upstream connesso, aggiornata su ogni riconnessione; risolto dal file `id_token` di OIDC restituito al token exchange, tornando all'email dell'utente di Twenty di connessione quando il provider non ne restituisce una |
| `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>

View File

@@ -0,0 +1,110 @@
---
title: Archivio chiave-valore
description: Rendi persistenti i risultati intermedi, memorizza nella cache i dati e condividi lo stato tra le esecuzioni delle funzioni di logica con l'archivio chiave-valore integrato dell'applicazione.
icon: database
---
Le funzioni di logica vengono eseguite in sandbox in processi Node.js di breve durata — una volta terminata un'esecuzione, nulla di ciò che era in memoria sopravvive. Quando devi **ricordare qualcosa tra un'esecuzione e l'altra** (memorizzare nella cache una costosa risposta di un'API, archiviare un cursore per sincronizzazioni incrementali, applicare un debounce al lavoro o passare lo stato da una funzione all'altra), conservalo nell'archivio chiave-valore integrato.
Ogni applicazione dispone del proprio namespace isolato: le voci sono associate all'app autenticata, quindi le tue chiavi non potranno mai entrare in conflitto con — o essere lette da — un'altra applicazione.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Get, set, delete
Importa `kv` da `twenty-sdk/logic-function`. I valori possono essere qualsiasi payload serializzabile in JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Ambiti
Ogni voce ha un ambito, passato come opzione a ogni chiamata. L'impostazione predefinita è `WORKSPACE`.
* **`WORKSPACE`** (predefinito) — la voce è privata per l'installazione corrente della tua app nel workspace. Ogni workspace che installa l'app ottiene il proprio set indipendente di chiavi. Questo è ciò che ti serve per cache, cursori e stato per workspace.
* **`SERVER`** — la voce è condivisa tra **ogni installazione** della tua app sul server. Le voci del server si comportano come **claim**: il valore memorizzato corrisponde sempre al workspaceId che ha richiesto la chiave (ometti `value` su `set` per richiedere la chiave per il workspace corrente) e solo quel workspace può sovrascriverla o eliminarla. Qualsiasi installazione può leggere la voce.
I claim del server esistono per l'instradamento tra workspace. Un [server-route resolver](/l/it/developers/extend/apps/logic/logic-functions#server-route-trigger) viene eseguito nel workspace del proprietario della registrazione dell'applicazione, ma un webhook in ingresso di solito contiene solo un id account esterno — non un Twenty workspaceId. Fai in modo che ogni workspace richieda il proprio id esterno al momento della connessione, quindi risolvilo nella route:
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Poiché una chiave server può essere richiesta solo per il workspace del chiamante e non può mai essere sovrascritta da un altro, un workspace non può dirottare una mappatura che appartiene a qualcun altro. `kv.set` genera un'eccezione quando la chiave è già stata richiesta da un altro workspace.
## Usalo: metti in cache una chiamata costosa
Un uso tipico consiste nel mettere in cache una risposta lenta o soggetta a limitazione di frequenza da parte di terzi, così che le esecuzioni ripetute la riutilizzino invece di sostenerne il costo ogni volta.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Pattern e suggerimenti
* **Namespace.** Anteponi prefissi alle chiavi per tenere separate le varie aree di interesse — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Scadenza (TTL).** Lo store non ha una scadenza integrata. Archivia un timestamp all'interno del valore (come nell'esempio di cache) e verificalo in lettura oppure cancella le chiavi obsolete da una [funzione attivata da cron](/l/it/developers/extend/apps/logic/logic-functions).
* **Cosa archiviare.** Qualsiasi valore serializzabile in JSON — numeri, stringhe, array, oggetti. Mantieni ridotte le dimensioni delle voci; questo meccanismo serve per coordinamento e caching, non per grandi blob o file. Per i file, usa un campo `FILES` e [`uploadFile`](/l/it/developers/extend/apps/logic/logic-functions#uploading-files).
* **Visibilità.** Le voci risiedono nel database dell'istanza, non come record di workspace: non vengono mai visualizzate nell'interfaccia utente del workspace, non fanno parte del modello dati della tua app e non richiedono ruoli o autorizzazioni sugli oggetti.
## Alternativa: un oggetto store interrogabile
L'archivio integrato è volutamente opaco: le voci non sono record, quindi non puoi sfogliarle nell'interfaccia utente, collegarle ad altri oggetti o filtrarle con query sui record. Quando ti serve una di queste funzionalità — ad esempio un log di sincronizzazione visibile o uno stato per record — definisci invece un piccolo **oggetto tecnico** con un campo `key` univoco e un campo `value` `RAW_JSON`, e interrogalo tramite il [client API tipizzato](/l/it/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Vedi [Objects](/l/it/developers/extend/apps/data/objects) per la reference di `defineObject` e [Data → Unique indexes](/l/it/developers/extend/apps/data/overview#unique-indexes) per applicare l'univocità delle chiavi.
* **Definire l'ambito rispetto a un record.** Aggiungi una [relation](/l/it/developers/extend/apps/data/relations) dall'oggetto store all'oggetto di destinazione invece di codificare l'id nella chiave.
* **Visibilità e autorizzazioni.** Le righe risiedono nel database dello spazio di lavoro come qualsiasi altro record, quindi sono interrogabili tramite l'API e rispettano il [ruolo](/l/it/developers/extend/apps/config/roles) della tua app. Per tenere lo store fuori dall'interfaccia principale, escludilo dal tuo [navigation menu](/l/it/developers/extend/apps/layout/navigation-menu-items).
<Note>
A differenza dell'archivio integrato, un oggetto personalizzato è sempre limitato a un solo workspace — non può condividere voci tra installazioni come fanno le chiavi `SERVER`.
</Note>

View File

@@ -0,0 +1,908 @@
---
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. Nel codice dell'app, anteponi al percorso della route `/s/` quando utilizzi `RestApiClient`; l'URL distribuito utilizza la base `TWENTY_FUNCTIONS_URL` iniettata (o `\<server-url>/s` quando non è impostata).
<Note>
Per richiamare, da un componente front-end (headless), una funzione logica attivata da una rotta, vedi [Chiamare una funzione logica](/l/it/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron**: Esegue la tua funzione secondo una pianificazione utilizzando un'espressione CRON.
* **databaseEvent**: Viene eseguito sugli eventi del ciclo di vita degli oggetti dello spazio di lavoro. Quando l'operazione dell'evento è `updated`, è possibile specificare campi specifici da monitorare nell'array `updatedFields`. Se lasciato non definito o vuoto, qualsiasi aggiornamento attiverà la funzione.
> ad es. `person.updated`, `*.created`, `company.*`
* **serverRoute**: espone una singola route HTTP con ambito di registrazione. Una funzione di **resolver** (dichiarata con `serverRouteTriggerSettings`) viene eseguita nel workspace proprietario e restituisce una `Response` sincrona, oppure il workspace di destinazione e la funzione logica di destinazione da mettere in coda; nel percorso di messa in coda la piattaforma conferma con `202` ed esegue tale **destinazione** nella coda dei worker. Vedi [Trigger route del server](#server-route-trigger).
<Note>
Puoi anche eseguire manualmente una funzione utilizzando la CLI:
```bash filename="Terminal"
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>
#### Risposte di errore della piattaforma
Oltre alle risposte specifiche del tuo handler, la piattaforma risponde direttamente alle chiamate di route in alcune situazioni: `404` quando la route o la funzione non esiste, `403` quando l'applicazione è arrestata, `429` quando viene raggiunto il rate limit di esecuzione e `422` quando le `dependencies` dell'applicazione in produzione sono troppo grandi per essere installate — vedi [limiti di dimensione delle dipendenze](/l/it/developers/extend/apps/getting-started/project-structure#dependencies).
#### Trigger route del server
`httpRouteTriggerSettings` espone una funzione sotto `/s/` e risolve lo spazio di lavoro dall'host della richiesta — il che funziona quando ogni spazio di lavoro ha il proprio dominio. I provider di terze parti, tuttavia, inviano gli eventi di ogni tenant a **un** URL. Per questo caso, usa `serverRouteTriggerSettings`.
Il trigger ha due parti:
1. Una funzione logica di **resolver** — dichiarata con `serverRouteTriggerSettings` — viene eseguita nel tuo **workspace proprietario** (il workspace che possiede la registrazione dell'applicazione). Analizza la richiesta in ingresso e restituisce:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la piattaforma mette in coda tale destinazione nel workspace risolto e conferma con `202 { queued: true }`, oppure
* una `Response` da `twenty-sdk/logic-function` — la piattaforma restituisce tale risposta HTTP **sincronicamente** e **non** mette in coda alcuna destinazione (usa questo per challenge handshake come Slack `url_verification`).
Il resolver è l'unico punto di autorizzazione: l'URL contiene solo l'identificatore del resolver. **Questo è il punto preferenziale per verificare le firme delle richieste**: il resolver viene eseguito prima di qualsiasi effetto collaterale, ha accesso al `rawBody` originale e agli header inoltrati, e può rifiutare senza toccare la destinazione.
2. Una funzione logica di **destinazione** — una normale funzione logica per-workspace — viene quindi eseguita nel workspace risolto con il payload restituito dal resolver (o il payload originale della richiesta se il resolver non lo ha trasformato). Il suo valore di ritorno **non** viene osservato dal chiamante HTTP quando il resolver ha scelto il percorso di messa in coda.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
L'endpoint è raggiungibile all'indirizzo:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
L'identificatore è il `universalIdentifier` del resolver dal tuo manifest. Registra quell'URL presso il provider.
**Risposta a una verifica GET.** Alcuni provider verificano un endpoint prima di inviarvi dati, inviando un `GET` contenente una verifica allo stesso URL a cui in seguito invieranno eventi tramite `POST` — WhatsApp Cloud API di Meta è uno di questi. Una route del server risponde solo a `POST`, a meno che non venga specificato diversamente, quindi dichiara entrambi i metodi:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
La verifica arriva in `event.queryStringParameters` e la restituzione di un `Response` la rimanda al provider nella stessa richiesta. Un corpo stringa viene inviato come `text/plain`, che è ciò che questi provider si aspettano:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` sostituisce l'impostazione predefinita anziché aggiungersi a essa, quindi `['GET']` da solo fa sì che la route rifiuti `POST`. Sono supportati solo `GET` e `POST`. Lascialo non impostato a meno che il provider non richieda il secondo verbo: una route che dichiara `GET` avrà il proprio resolver eseguito da qualsiasi chiamante non autenticato, inclusi i crawler e gli strumenti di anteprima dei link che inviano `GET` senza richiesta. A tutto ciò per cui la piattaforma non dispone di un metodo viene restituito `405` senza che il resolver venga mai eseguito.
<Note>
**L'applicazione deve essere rivendicata e installata nel workspace del proprietario.** Poiché il resolver viene eseguito nel **workspace del proprietario** (il workspace che possiede la registrazione dell'applicazione), un server route trigger funziona solo quando l'applicazione è stata *rivendicata*, cioè ha un workspace del proprietario, **e** quell'applicazione è **installata nel workspace del proprietario**. Finché entrambe non sono vere, il resolver non ha dove essere eseguito, quindi la route non può essere gestita. Un'applicazione che espone una funzione logica `serverRouteTriggerSettings` quindi non può essere elencata nel marketplace finché non è stata rivendicata e installata nel workspace del proprietario.
</Note>
**Contratto del resolver.** Il tipo `LogicFunctionConfig` dell'SDK impone questo a tempo di compilazione: non appena imposti `serverRouteTriggerSettings`, il tuo handler è vincolato a restituire una `Response`, oppure `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o una `Promise` di uno dei due). Nel percorso di dispatch, il `workspaceId` deve essere un workspace in cui la funzione di destinazione è installata, altrimenti la richiesta viene rifiutata con `404`. Un risultato che non corrisponde a nessuna delle due forme — incluso uno i cui identificatori non sono UUID — viene rifiutato con `502`.
| Campo | Tipo | Note |
| ---------------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
| `workspaceId` | `string` | UUID del workspace in cui verrà eseguita la destinazione. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` della funzione logica da invocare in quel workspace. |
| `payload` | `object` (opzionale) | Se impostato, sostituisce il body della richiesta inviato alla destinazione. |
<Warning>
**La verifica della firma è una tua responsabilità — eseguila nel resolver.** La piattaforma non verifica le firme delle richieste. Il resolver è il punto consigliato in cui farlo: viene eseguito per primo, con accesso a `event.rawBody` e agli header che hai elencato in `forwardedRequestHeaders`, e un errore lanciato (o qualsiasi `workspaceId` non corrispondente) interrompe l’inoltro prima che la destinazione venga invocata. Se invece spingi la verifica verso la destinazione, la destinazione deve fare attenzione a non perdere `rawBody` e gli header — cioè il resolver non deve restituire un `payload`. Verifica sempre **prima** di qualsiasi effetto collaterale e usa un confronto a tempo costante.
</Warning>
Per le firme delle richieste, la maggior parte dei provider firma con HMAC-SHA256; le parti che differiscono sono il nome dell'header, la codifica del digest e la stringa del payload firmato. Alcuni esempi:
| Provider | Header da inoltrare | Stringa firmata | Digest |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (il segreto è in base64 dopo aver rimosso `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | esadecimale |
| GitHub | `x-hub-signature-256` | `{rawBody}` | esadecimale (prefissato con `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | esadecimale (prefissato con `v0=`) |
L'esempio di resolver sopra mostra già il flusso HMAC-SHA256 di GitHub — adatta il nome dell'header, la codifica del digest e la stringa del payload firmato in base al provider con cui ti stai integrando.
<Note>
Quando il resolver restituisce un oggetto di dispatch, la route risponde con `202 { queued: true }` e la funzione di destinazione viene eseguita nella coda dei worker — il chiamante non osserva mai la latenza, il risultato o gli errori della funzione di destinazione (che vengono registrati nei log di esecuzione). Questo impedisce che le nuove consegne da parte del mittente amplifichino i rallentamenti nell'elaborazione, che è esattamente ciò che si desidera per l'acquisizione dei webhook.
Quando il chiamante deve leggere il corpo della risposta sulla stessa richiesta (challenge handshake, acknowledgement interattivi), restituisci invece una `Response` dal **resolver**. La piattaforma lo riecheggia in modo sincrono e salta la coda; i suoi header passano attraverso la stessa allow-list delle risposte delle route HTTP. Mantieni il resolver veloce — alcuni provider (ad es. Slack) vanno in timeout in pochi secondi. Poiché il resolver è raggiungibile come endpoint pubblico, proteggilo con rate limiting al tuo edge.
</Note>
#### Payload del trigger di evento del database
Quando un trigger di evento del database invoca la tua funzione logica, questa riceve un `DatabaseEventPayload` per ogni record modificato. Il payload combina i metadati sull’area di lavoro e sull’oggetto di origine con l’evento a livello di record.
```ts
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,
};
};
```
#### Contesto di esecuzione
Ogni gestore riceve un secondo argomento che descrive l'esecuzione stessa, indipendentemente da cosa l'abbia attivata. Mentre il primo argomento cambia struttura in base all'attivatore, questo no:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Campo | Tipo | Descrizione |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | Lo spazio di lavoro a cui appartiene l'esecuzione. |
| `userWorkspaceId` | `string \| null` | La persona che ha attivato l'esecuzione, in quanto membro di questo spazio di lavoro. |
| `workspaceMemberId` | `string \| null` | La stessa persona come record del membro dello spazio di lavoro, a cui si collegano i record. |
| `retryCount` | `number` | `0` al primo tentativo. Vedi [Processi in background](/l/it/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limite massimo per i tentativi richiesti dall'applicazione. |
`userWorkspaceId` e `workspaceMemberId` sono `null` quando nessuno ha attivato l'esecuzione: le pianificazioni cron, gli hook di installazione e i webhook non autenticati non hanno una persona alle spalle. Sono inoltre `null` quando la persona non ha un record del membro dello spazio di lavoro o quando il suo è stato eliminato.
<Note>
**Il contesto indica chi ha attivato l'esecuzione.** Ciò che l'esecuzione può *fare* è una dichiarazione separata, riportata di seguito.
</Note>
#### Quale accesso utilizza una chiamata
Ogni client — `CoreApiClient`, `MetadataApiClient` e `RestApiClient` — agisce come la persona che ha attivato l'esecuzione: il suo ruolo intersecato con quello della tua applicazione, quindi la chiamata non può mai fare più di quanto possa fare ciascuno dei due. Questo è il comportamento predefinito e significa che una persona non può mai usare la tua app per superare le proprie autorizzazioni.
Quando nessuno ha attivato l'esecuzione, non c'è nessuna persona per conto della quale agire, quindi lo stesso client usa come fallback l'accesso della tua applicazione: le pianificazioni cron, gli hook di installazione e i webhook non autenticati non richiedono una gestione speciale.
Alcune chiamate necessitano legittimamente dell'accesso della tua applicazione anche quando una persona *è* alle spalle dell'esecuzione — per leggere i record di configurazione della tua app o per eseguire un'attività che quella persona non potrebbe svolgere autonomamente. Crea un secondo client per questi casi:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Crea entrambi una sola volta, nell'ambito del modulo, e ogni punto di chiamata indica quindi quale accesso utilizza in base al client che chiama. `RestApiClient` accetta la stessa opzione:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**Un'esecuzione che nessuno ha attivato agisce come la tua applicazione.** Le pianificazioni cron, gli hook di installazione e i webhook non autenticati non hanno una persona alle spalle, quindi il client predefinito usa come fallback l'accesso della tua applicazione e continua a funzionare. `runAs: 'application'` è necessario solo quando vuoi tale accesso in un'esecuzione che una persona *ha* attivato.
Controlla `context.workspaceMemberId` quando una funzione si comporta diversamente a seconda che ci sia qualcuno alle sue spalle, ad esempio per attribuire un record.
</Note>
Gli helper dell'SDK che raggiungono le risorse della tua applicazione utilizzano sempre il suo accesso e ignorano `runAs`: l'[archivio chiave-valore](/l/it/developers/extend/apps/logic/key-value-store), le [connessioni](/l/it/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` e l'addebito dei crediti.
#### Esporre una funzione come strumento di IA o come azione del flusso di lavoro
Le funzioni logiche possono essere esposte su due superfici, ciascuna con il proprio trigger:
* **`toolTriggerSettings`** — rende la funzione individuabile dalle funzionalità di IA di Twenty (chat, MCP, function calling). Usa lo standard JSON Schema, il formato che gli LLM comprendono nativamente.
* **`workflowActionTriggerSettings`** — fa apparire la funzione come un passaggio nel builder visivo dei flussi di lavoro. Usa il ricco `InputSchema` di Twenty affinché il builder possa visualizzare correttamente editor di campi, selettori di variabili ed etichette.
Una funzione può optare per uno, l'altro o entrambi. Si affiancano a `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — stesso schema, stessa struttura.
<Note>
**Relazione con l’azione Code del workflow.** L’azione **Code** incorporata nel generatore di workflow è essa stessa una funzione logica — Twenty ne crea una per ogni passaggio Code e ne espone l’editor inline. `workflowActionTriggerSettings` è il modo in cui trasformi quel codice inline una tantum in un’azione **riutilizzabile**: definisci la funzione una volta nella tua app e diventa selezionabile in qualsiasi workflow, invece di essere copiata e incollata in ogni passaggio Code. Consulta l’[azione Code](/l/it/user-guide/workflows/capabilities/workflow-actions#code) nella guida utente per la vista dell’utente finale.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
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'],
},
},
});
```
Per dichiarare i parametri **una sola volta** e servire entrambi i contesti, definisci un unico JSON Schema (`InputJsonSchema`) e convertilo per l'azione del flusso di lavoro con `jsonSchemaToInputSchema` da `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` accetta direttamente il JSON Schema, mentre `workflowActionTriggerSettings.inputSchema` si aspetta l'`InputSchema` di Twenty:
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Un esempio completo di azione di workflow
`workflowActionTriggerSettings` accetta quattro campi:
| Campo | Scopo |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Nome mostrato per l’azione nel selettore di passaggi del generatore di workflow. Il valore predefinito è il `name` della funzione. |
| `icon` | Icona mostrata accanto all’azione (un nome `tabler-icons`, ad esempio `IconBuilding`). |
| `inputSchema` | Il ricco `InputSchema` di Twenty: ciò che il generatore visualizza come campi configurabili (con selettori di variabili). Opzionale; quando omesso viene ricavato dall’handler. |
| `outputSchema` | Dichiara la struttura restituita dall’handler, così **i passaggi successivi possono mappare ai suoi campi di output**. Opzionale; senza di esso, l’output è esposto come singolo valore opaco. |
Mettendo tutto insieme: una funzione esposta come azione di workflow, con un output dichiarato così che i passaggi successivi possano fare riferimento a `taskId`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Una volta che l’app è installata, **Enrich Company** appare nel selettore di azioni del generatore di workflow. Il generatore visualizza `companyName` e `domain` come campi di input (ognuno in grado di prelevare valori dai passaggi precedenti), e i passaggi a valle possono fare riferimento ai risultati `taskId` ed `enriched` del passaggio.
<Note>
**Scrivi una buona `description`.** Gli agenti IA fanno affidamento sul campo `description` della funzione per decidere quando usare lo strumento. Sii specifico su cosa fa lo strumento e quando dovrebbe essere invocato.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Helper di runtime.** `twenty-sdk/utils` riesporta piccoli helper di runtime in modo che gli handler non importino mai direttamente da `twenty-shared`. Per esempio, `isDefined(value)` restituisce `false` sia per `null` che per `undefined` — usalo per restringere in modo sicuro gli input opzionali degli handler, che possono arrivare come `null` a runtime anche quando sono tipizzati come `T | undefined`:
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Hook di installazione** — i gestori di pre-installazione, post-installazione e disinstallazione — condividono questo runtime, ma sono dichiarati con le proprie funzioni di definizione e non accettano impostazioni dei trigger. Consulta [Hook di installazione](/l/it/developers/extend/apps/config/install-hooks) per `definePreInstallLogicFunction`, `definePostInstallLogicFunction` e `defineUninstallLogicFunction`.
</Note>
## Crea un'attività della cronologia di eventi.
Usa `createTimelineActivity()` per pubblicare un evento di dominio esplicito da una funzione logica. Definisci prima l'evento come [tipo di attività della cronologia](/l/it/developers/extend/apps/data/timeline-activity-types), quindi fai riferimento al tipo e agli oggetti tramite i relativi identificatori universali stabili:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty risolve gli identificatori universali negli ID dei metadati specifici dell'installazione, verifica che il tipo di attività della cronologia appartenga all'app chiamante e acquisisce un'istantanea dei relativi metadati di presentazione nella nuova attività.
Gli input obbligatori sono `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` e `targetRecordId`. Puoi anche fornire `happensAt`, `properties` e `workspaceMemberId`. `happensAt` controlla la posizione e l'ora visualizzata dell'evento nella cronologia; per impostazione predefinita corrisponde all'ora di creazione.
Per associare un altro record all'evento, fornisci insieme `linkedRecordId` e `linkedObjectMetadataUniversalIdentifier`. Puoi inoltre fornire `linkedRecordCachedName` come alternativa di visualizzazione storica:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Il ruolo della funzione logica deve disporre dell'autorizzazione di scrittura sull'oggetto standard `timelineActivity`. Mantieni i tipi di evento espliciti non associati a un'`action`; un tipo associato a un'azione riceve già eventi di audit automatici e altrimenti produrrebbe righe duplicate.
## Client API tipizzati (twenty-client-sdk)
Il pacchetto `twenty-client-sdk` fornisce due client GraphQL tipizzati per interagire con l'API di Twenty dalle tue funzioni logiche e dai componenti front-end.
| Client | Importa | Endpoint | Generato? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------ | -------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — dati dello spazio di lavoro (record, oggetti) | Sì, in fase di dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurazione dello spazio di lavoro, caricamenti di file | No, fornito pronto all'uso |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Esegui query e modifica i dati dello spazio di lavoro (record, oggetti)">
`CoreApiClient` è il client principale per interrogare e modificare i dati dello spazio di lavoro. Viene **generato dallo schema del tuo spazio di lavoro** durante `yarn twenty dev` o `yarn twenty dev:build`, quindi è completamente tipizzato per corrispondere ai tuoi oggetti e campi.
```ts
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. Accetta la stessa opzione `runAs` di `CoreApiClient` — vedi [Quale accesso utilizza una chiamata](#whose-access-a-call-uses).
```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 per l'accesso predefinito: il ruolo di una persona intersecato con quello della tua applicazione quando qualcuno è alle spalle dell'esecuzione, il ruolo della tua applicazione quando non c'è nessuno. La persona è chiunque abbia attivato l'esecuzione in una funzione logica, oppure chiunque stia visualizzando la pagina in un componente front-end.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Chiave a breve durata con ambito esclusivamente al ruolo della tua applicazione. Solo funzioni logiche, sempre inserita lì, ed è ciò che utilizza `runAs: 'application'`.
**Non** devi passare questi elementi ai client — vengono letti automaticamente da `process.env` e [Quale accesso utilizza una chiamata](#whose-access-a-call-uses) spiega come scegliere tra di essi. Le autorizzazioni della tua applicazione sono determinate dal ruolo dichiarato con `defineApplicationRole()` (o referenziato tramite `defaultRoleUniversalIdentifier` in `application-config.ts`); un'esecuzione che agisce come una persona non può mai superare né tale ruolo né il proprio.
</Note>

View File

@@ -0,0 +1,61 @@
---
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>
<Card title="Archivio chiave-valore" icon="database" href="/l/it/developers/extend/apps/logic/key-value-store">
Mantieni lo stato tra le esecuzioni delle funzioni di logica — cache, cursori e asserzioni tra spazi di lavoro.
</Card>
<Card title="Job in background" icon="layer-group" href="/l/it/developers/extend/apps/logic/background-jobs">
Metti in coda l’esecuzione di una funzione di logica sui worker per superare il timeout per esecuzione.
</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>

View File

@@ -0,0 +1,186 @@
---
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()`. Passa una stringa `prompt` oppure una cronologia di conversazione `messages`, ma non entrambe:
```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.',
});
```
Per bot multi-turno (Slack, Discord, Teams, …), passa la cronologia del thread come `messages` invece di un singolo `prompt`:
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Punti chiave:
* Fornisci **esattamente uno** tra `prompt` (string) o `messages` (da 1 a 100 elementi
di `{ role: 'user' | 'assistant', content: string }`).
* 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>
### Esecuzione per conto di un membro dello spazio di lavoro
Passa `runAsWorkspaceMemberId` quando l'esecuzione viene attivata da una persona — ad esempio un chatbot che risponde a un messaggio — in modo che l'agente agisca come quel membro invece che come l'app:
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
L’esecuzione quindi agisce con **il ruolo proprio del membro**: può fare tutto ciò che
quel membro può fare e niente di più, i record che crea sono attribuiti a lui e
si applicano i suoi permessi a livello di riga. Il ruolo dell’agente non partecipa — è l’impostazione predefinita dell’app per le esecuzioni senza nessuno dietro. Ometti il campo per
le esecuzioni autonome (attività pianificate, trigger di eventi del database): queste mantengono il ruolo proprio dell’agente. L’applicazione che agisce rimane collegata al contesto dell’esecuzione
per la tracciabilità, senza restringere i permessi del membro.
La tua app è responsabile di mappare la persona che ha attivato l'esecuzione a un membro dello spazio di lavoro. Per specificarne uno è necessario un token di accesso dell'app e ciò che un token può specificare dipende dal fatto che sia o meno associato a un utente:
* Un token senza alcun utente associato può specificare qualsiasi membro. Una funzione logica viene eseguita con uno, e così anche i token generati tramite `client_credentials` o da una chiave API.
* Un token emesso per conto di un utente, come quello che riceve un front component, può specificare solo il membro dell'utente stesso.
Qualsiasi altra cosa — una semplice sessione utente, una chiave API senza un token dell'app — non può specificare affatto un membro.
<Warning>
`runAgent()` genera un errore quando il membro dello spazio di lavoro non può essere risolto — un
membro sconosciuto o rimosso, o uno senza ruolo. Non ricorre mai al
ruolo proprio dell'agente, poiché ciò concederebbe più accesso di quanto richiesto dal chiamante.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
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: terminal
---
La CLI `yarn twenty` è la tua interfaccia per tutto ciò che riguarda le app. Elenco completo dei comandi:
| Comando | Cosa fa | Documentato in |
| ----------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `dev` | Monitora i file sorgente e sincronizza in tempo reale le modifiche | [Guida rapida](/l/it/developers/extend/apps/getting-started/quick-start) |
| `piano` | Visualizza in anteprima le modifiche ai metadati senza applicarle | [Sincronizzazione e ripristino](/l/it/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `applica` | Applica le modifiche ai metadati dopo aver mostrato il piano | [Sincronizzazione e ripristino](/l/it/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Compila l'app e genera il client API (`--tarball` per creare un `.tgz`) | [Pubblicazione](/l/it/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | Esegui il controllo dei tipi TypeScript | [Test](/l/it/developers/extend/apps/operations/testing) |
| `dev:add` | Genera lo scaffold per una nuova entità | [Scaffolding](/l/it/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Rigenera il client API tipizzato | questa pagina |
| `dev:function:exec` / `dev:function:logs` | Esegui le funzioni e trasmetti in streaming i relativi log | questa pagina |
| `dev:translations-extract` | Estrai le stringhe traducibili nei cataloghi in `locales/` | [Traduzioni](/l/it/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Attiva la sincronizzazione del catalogo del marketplace | [Pubblicazione](/l/it/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Ciclo di vita delle release | [Pubblicazione](/l/it/developers/extend/apps/operations/publishing) e questa pagina |
| `docker:*` | Gestisci il container del server Twenty locale | [Server locale](/l/it/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Gestisci le connessioni al server | questa pagina |
Ogni comando accetta `-r, --remote \<name>` per puntare a un remoto specifico anziché a quello predefinito.
## 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 install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## 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>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Le tue credenziali sono archiviate in `~/.twenty/config.json`.

View File

@@ -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="compass" 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>

View File

@@ -0,0 +1,293 @@
---
title: Pubblicazione
icon: upload
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
Le app in formato tarball non sono elencate nel marketplace pubblico, quindi altri spazi di lavoro sullo stesso server non le troveranno navigando. Per condividere un'app distribuita:
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 installato l'app e abilitato l'aggiornamento automatico (nella scheda Impostazioni dell'app) vengono aggiornati automaticamente in secondo piano; gli altri vedranno l'aggiornamento disponibile nelle loro 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 tre workflow di GitHub Actions pronti all'uso, nella cartella `.github/workflows/`. L'esecuzione della CI non richiede alcuna configurazione, la CD richiede un solo secret e la pubblicazione su npm richiede una configurazione una tantum di npm trusted-publisher.
### 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.
### Pubblica — `publish.yml`
Pubblica la tua app su npm con attestazione di provenienza quando esegui il push di un tag di versione (ad es. `v1.0.0`), oppure quando esegui manualmente il workflow dalla scheda Actions.
**Cosa fa:**
1. Clona la tua app, configura Node.js e aggiorna npm (la pubblicazione attendibile richiede npm 11.5.1 o versioni successive).
2. Esegue `yarn twenty app:publish`, che crea la build dell'app e pubblica `.twenty/output` su npm. In CI aggiunge automaticamente `--provenance` e `--access public`, quindi non sono necessari flag nel workflow.
**Impostazione una tantum:**
Su npmjs.com apri il tuo pacchetto > **Settings → Trusted Publisher** e registra questo repository con il workflow `publish.yml` (consulta la [documentazione su npm trusted publishing](https://docs.npmjs.com/trusted-publishers)). La pubblicazione con provenance certifica quale repository GitHub ha creato il pacchetto ed è anche il modo in cui dichiari la proprietà della tua app in un marketplace Twenty.
<Note>
npm accetta la provenienza solo da repository di origine **pubblici**. Se esegui la pubblicazione da un repository privato, npm rifiuta il bundle di provenienza OIDC con un `E422 ... Errore "Unsupported GitHub Actions source repository visibility: \"private\"`." Per pubblicare da un repository privato, escludi la provenienza impostando `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` nella `env` dello step di pubblicazione (nel `publish.yml` generato è incluso un suggerimento commentato):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Bloccare le azioni riutilizzabili
I workflow `ci.yml` e `cd.yml` 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 `logo` e `galleryImages` per fare riferimento alle immagini nella cartella `public/`:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'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 delle immagini della galleria
Il marketplace visualizza le `galleryImages` in un contenitore `8:5` fisso (ad esempio, `1600×1000 px`).
<Note>
Le immagini della galleria di qualsiasi rapporto d'aspetto vengono visualizzate per intero e non vengono mai ritagliate, ma tutto ciò che è significativamente più alto o più stretto di `8:5` mostrerà bande vuote ai lati.
</Note>
#### Limite dimensione immagine
Il file `logo` e ciascun file di `galleryImages` non devono superare **10 MB**. I file più grandi vengono ignorati quando il marketplace ripubblica le tue risorse pubblicate, quindi non verranno visualizzati.
### 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 mostrati nel marketplace provengono dalla configurazione di `defineApplication()` — vedi [Metadati del marketplace](#marketplace-metadata) sopra.
<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
Il workflow `publish.yml` generato dallo scaffold descritto sopra pubblica automaticamente su npm sui tag di versione, con provenance. Poiché `yarn twenty app:publish` aggiunge `--provenance` e `--access public` per te quando viene eseguito in CI, il workflow non ha bisogno di flag npm — serve solo l'impostazione una tantum del trusted publisher.
Per altri sistemi CI (GitLab CI, CircleCI, ecc.), esegui `yarn install` e poi `yarn twenty app:publish`. La provenance viene emessa quando l'ambiente può generare un token OIDC e, in caso contrario, viene saltata automaticamente.
<Note>
La **npm 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. È anche ciò che ti permette di dichiarare la proprietà della tua app in un marketplace Twenty. Consulta la [documentazione su npm provenance](https://docs.npmjs.com/generating-provenance-statements) per maggiori dettagli.
</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 l’installazione, rispecchiando le regole del deploy:
* L’installazione della stessa versione già installata nel tuo spazio di lavoro viene rifiutata con un errore `APP_ALREADY_INSTALLED`.
* L’installazione 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>

View File

@@ -0,0 +1,126 @@
---
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: compass
---
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 l’iterazione 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 apply` | Esegue una build + sincronizzazione, poi termina. Aggiungi `--force` per saltare la conferma delle modifiche distruttive. |
| Visualizzare in anteprima le modifiche **senza applicarle** | `yarn twenty plan` | 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. |
<Note>
`yarn twenty dev --once` e `yarn twenty dev --once --dry-run` funzionano ancora come alias deprecati per `yarn twenty apply` e `yarn twenty plan`.
</Note>
### 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 `plan`), in stile Terraform — un blocco per entità con i relativi attributi, quindi una riga di riepilogo:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
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).
Le modifiche distruttive (`to destroy`) sono elencate con ciò che eliminano (ad es. `objectMetadata "auditNote" — drops the table and all its rows`) e richiedono una conferma interattiva, oppure `--force` negli script.
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 (plan)
`yarn twenty plan` 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 plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Un piano:
* **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 piano 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 apply`. Le sincronizzazioni sono idempotenti — rieseguire un manifest pulito è sicuro e spesso risolve un problema temporaneo.
2. **Visualizza in anteprima il piano.** Esegui `yarn twenty plan` 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 dimensione delle dipendenze** — la sincronizzazione o l'installazione non riesce perché le `dependencies` di produzione dell'app sono troppo grandi per essere installate come layer di runtime (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`); sposta nei `devDependencies` i pacchetti che le tue funzioni logiche non importano a runtime (librerie UI, strumenti di sviluppo).
* **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.

View File

@@ -0,0 +1,264 @@
---
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 le loro 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. I pacchetti importati vengono incorporati nel bundle, ad eccezione di quelli elencati di seguito.
**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 i pacchetti npm che funzionano in un ambiente browser. Nota che la sandbox implementa un DOM *parziale*, quindi un pacchetto può compilarsi correttamente e comunque fallire in fase di esecuzione; vedi [Limitazioni attuali](/l/it/developers/extend/apps/layout/front-components#current-limitations).
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.
I componenti front-end possono anche condividere dipendenze tramite un [bundle di dipendenze condivise](/l/it/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). I pacchetti elencati nel campo `frontComponentSharedDependencies` del package.json dell'app vengono compilati una sola volta in un file separato e caricati da lì a runtime invece di essere incorporati in ogni componente, quindi un componente che importa solo pacchetti condivisi contiene quasi nessun codice di dipendenza proprio.
## 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';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Crea un file di configurazione globale che verifichi che il server sia raggiungibile, scriva una configurazione di test per l'SDK (`~/.twenty/config.test.json`) e sincronizzi l'app prima dell'esecuzione dei test:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## 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 |
| `appDevOnce` | Compila e sincronizza l'app una volta (come `yarn twenty apply`) |
| `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` sul file `tsconfig.json` della tua app e riporta eventuali errori di tipo. Le app generate dallo scaffolder includono anche uno script `yarn typecheck` che copre anche i file di test (`tsconfig.spec.json`).
## CI con GitHub Actions
Lo strumento di scaffolding genera un workflow pronto all'uso in `.github/workflows/ci.yml`. A ogni push su `main` e a ogni pull request, avvia un server Twenty effimero nel runner (tramite l'azione `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`), quindi esegue `yarn lint`, `yarn typecheck`, `yarn test:unit` e `yarn test` con `TWENTY_API_URL` / `TWENTY_API_KEY` che puntano a quel server. Non sono necessari segreti e puoi fissare la versione del server tramite la variabile di ambiente `TWENTY_VERSION` all'inizio del workflow.
Vedi [Pubblicazione → CI/CD automatizzato](/l/it/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) per una spiegazione completa dei tre workflow generati dallo scaffolder (`ci.yml`, la pipeline di deploy `cd.yml` e `publish.yml` per la pubblicazione su npm).

View File

@@ -0,0 +1,144 @@
---
title: Traduzioni
description: Distribuisci la tua app in più lingue — traduci le etichette del manifest e le stringhe dell'interfaccia utente tramite un unico catalogo delle localizzazioni.
icon: language
---
Le app di Twenty sono scritte in **inglese**: le stringhe nel tuo sorgente e nel manifest
sono il testo sorgente in inglese, `en` è la locale di origine da cui traduci e
qualsiasi locale lasciato non tradotto usa quella come fallback.
La tua app ha due tipi di testo traducibile, e entrambi passano attraverso lo stesso
catalogo `locales/`:
* **Etichette del manifest** — nomi di oggetti e campi, titoli delle viste, voci di menu e
altre stringhe dichiarate nei metadati della tua app.
* **String dei front-component** — il testo dell’interfaccia utente renderizzato dai tuoi front component React.
Contrassegni le stringhe traducibili, le estrai in cataloghi per locale,
traduci tali cataloghi e la build serve la lingua corretta per l’utente
corrente — senza cablaggi aggiuntivi.
## Contrassegnare le stringhe dei front-component
Importa gli helper di traduzione da `twenty-sdk/front-component`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Quando usare cosa
* **`Trans`** — testo statico in JSX. Usa le prop `message` e `values` per
l'interpolazione, come in `Trans message="Hi {name}" values={{ name }}`;
l'interpolazione direttamente nei figli non è estraibile staticamente.
* **`useTranslate().t`** — stringhe dinamiche all’interno di un componente. Esegue il re-render quando
l’utente cambia lingua. Preferiscilo all’interno del render.
* **`t(...)`** (importato direttamente) — traduzione eager utilizzabile **ovunque**,
inclusi gestori di eventi, helper e nello scope del modulo — non solo all’interno del render.
* **`msg(...)`** — un descrittore lazy per stringhe dichiarate come dati (costanti,
configurazione). Risolvilo in un secondo momento con `t(descriptor)`.
### Contesto
Passa `context` per distinguere stringhe sorgente identiche che si traducono
in modo diverso:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Le stringhe contestuali compaiono nei cataloghi delle impostazioni locali come un gruppo indicizzato dal loro
contesto: `"\<context>": { "\<message>": "\<translation>" }`. Le etichette del manifest sono
contestuali per costruzione — ognuna porta con sé il ruolo di metadato che svolge
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), quindi
la stessa parola può essere tradotta in modo diverso come nome di oggetto e come nome di vista.
## Estrazione e traduzione
Esegui il comando di estrazione dalla directory della tua app:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
L'estrazione raccoglie sia le etichette del manifest sia le stringhe `t()`/`msg()`/`Trans`
dal tuo sorgente front-component in `locales/\<locale>.json`, indicizzate per
stringa sorgente. Compila le traduzioni:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Le stringhe prive di contesto sono voci piatte; le stringhe contestuali — inclusa ogni
etichetta del manifest — sono annidate sotto la loro chiave di contesto.
I segnaposto come `{name}` vengono sostituiti a runtime — mantienili nella
traduzione. Qualsiasi stringa lasciata vuota fa fallback al testo sorgente.
## Come viene eseguito
`twenty dev:build` compila i cataloghi e serve la lingua corretta per l’utente
corrente: le etichette del manifest vengono risolte lato server e i cataloghi dei
front-component vengono inclusi in ogni bundle di componente. A runtime un componente legge la
locale dal proprio contesto di esecuzione (la lingua corrente dell’host) e risolve
ogni stringa rispetto al proprio catalogo, facendo fallback alla sorgente quando
una traduzione manca. Cambiare lingua nell'host esegue nuovamente il render delle stringhe `Trans` e
`useTranslate().t` in tempo reale.
Le etichette del manifest vengono compilate a ogni sincronizzazione — `twenty dev:build`, `twenty apply`
e anche il monitoraggio continuo di `twenty dev` — quindi è sufficiente modificare un file di lingua e
sincronizzare per vedere il nuovo testo. I cataloghi dei componenti Front vengono incorporati in
ogni bundle di componenti al momento della build, quindi modificarne uno significa comunque
rieseguire `twenty dev:build` (e ridistribuire).
I figli di testo di `Trans` possono estendersi su più righe — gli spazi bianchi vengono compressi
nello stesso modo in cui JSX li comprime, quindi entrambe queste varianti vengono estratte con la chiave `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,81 @@
---
title: 5. An AI agent
icon: robot
description: Lascia che un agente generi documenti da una chat, usando il tuo strumento.
---
Poiché `generate-document` è esposto come **strumento**, un agente AI può chiamarlo.
Aggiungiamo un agente e un'abilità in modo che gli utenti possano solo dire *"generare una proposta per
Jeffery Griffin"*.
## L'abilità
A [skill](/l/it/developers/extend/apps/logic/skills-and-agents) è riutilizzabile
istruzioni — conoscenza che si lega agli agenti. Il nostro insegna al modello come usare
lo strumento.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## L'agente
Un [agent](/l/it/developers/extend/apps/logic/skills-and-agents) coppia un prompt con un modello
. Imposta esplicitamente `responseFormat` per evitare un avviso di generazione.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
L'agente può chiamare lo strumento solo se il suo ruolo lo permette. Abbiamo già impostato
`canAccessAllTools: true` e `canBeAssignedToAgents: true` sul ruolo dell'app in
[Capitolo 2](/l/it/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Provalo
Apri una chat con **Document Assistant** e chiedi di redigere un documento per una persona
nel tuo CRM. Trova il record, chiama `documento generato`, e riporta
indietro il documento che ha creato — che ora appare nella tua vista **Documenti**,
esattamente come il menu di comando e i percorsi del flusso di lavoro.
Questo è il payoff della logica di esposizione come uno strumento: **una funzione, molte porte d'ingresso** —
menu di comando, HTTP, passo del flusso di lavoro, e ora il linguaggio naturale.
**Dopo questo passaggio:** l'app è completa e genuinamente utile. Tempo per
spedirlo.
<Card title="Successivo: pubblicazione →" icon="rocket" href="/l/it/developers/extend/apps/tutorials/document-generator/publishing">
Aggiungi metadati di mercato e pubblica.
</Card>

View File

@@ -0,0 +1,306 @@
---
title: 4. Costruire l'interfaccia utente
icon: table-columns
description: Viste, navigazione della barra laterale, un comando e componenti anteriori.
---
In questo momento gli oggetti sono raggiungibili solo attraverso le Impostazioni. Diamo all'app una presenza
reale nell'UI: viste elenco, voci sidebar, un comando con un solo clic
**Genera documento** un componente frontale della record-page per **anteprima** un documento
e una scheda **editor** di testo ricco nativo per i modelli.
## Visualizzazioni e navigazione
Un [view](/l/it/developers/extend/apps/layout/views) è una lista salvata di un dato oggetto.
Una [voce del menu di navigazione](/l/it/developers/extend/apps/layout/navigation-menu-items)
mette quella vista nella barra laterale. Il server predispone automaticamente la vista predefinita "Tutti ..." per ogni oggetto, quindi le viste definite da un'app sono aggiuntive e curate.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Aggiungi la stessa coppia per i modelli. Entrambi ora mostrano nella barra laterale:
<Frame caption="Documenti e Modelli nella barra laterale, con il documento generato elencato.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Vista documenti con un documento generato" />
</Frame>
## Un componente anteriore
Un [componente anteriore](/l/it/developers/extend/apps/layout/front-components) è un componente React
sabbiato all'interno di Twenty. La nostra legge il record selezionato, carica i modelli di persona
tramite `CoreApiClient`, e POSTs sul percorso dall'ultimo capitolo
.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Stile con variabili CSS in linea (`var(--t-color-blue)`), non valori importati da
`twenty-ui`. Gli SDK mocks che il pacchetto durante la build, quindi le importazioni a livello di modulo di costanti del tema
sarebbero `indefinite`. Vedi il [componente completo]
(https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Un comando per aprirlo
Una [voce del menu di comando](/l/it/developers/extend/apps/layout/command-menu-items) con
`disponibilitàTipo: 'RECORD_SELECTION'` viene visualizzata quando una persona è selezionata, e
apre il componente nel pannello laterale.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Prova l'intero flusso
Aprire **Persone**, spuntare una persona e premere <kbd>국K</kbd> / <kbd>Ctrl K</kbd>.
"Genera documento" appare, taggato con la tua app:
<Frame caption="Il comando viene visualizzato quando una persona è selezionata.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu comandi con Genera documento" />
</Frame>
Eseguire — il componente si apre nel pannello laterale. Scegli un modello, fai clic su
**Genera**, e un nuovo record atterra in **Documenti**.
<Frame caption="Il componente anteriore, il caricamento dei modelli e la generazione al clic.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Genera pannello laterale documento" />
</Frame>
Ogni documento generato registra la tua app come suo autore:
<Frame caption="Creato da Generatore di documenti, stato generato.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Un documento generato" />
</Frame>
## Anteprima di un documento nella sua pagina di record
Un componente frontale non è solo per i menu di comando: puoi montarne uno come \*\*scheda su una pagina di record
\*\*. Aggiungiamo una scheda *Anteprima* al record di documenti che rende il corpo
Markdown come una pagina lucida e stampabile.
Il componente legge l'id del record corrente dal suo contesto di esecuzione, carica il documento
e lo rende. I componenti anteriori vengono eseguiti in una **sandbox** che permette solo una whitelist
di tag HTML — iniezione HTML grezza (`dangerouslySetInnerHTML`) e
`\<style>` sono bloccati — quindi rendiamo Markdown come elementi React con stili inline
tramite un piccolo [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
helper.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Monta con un [layout pagina](/l/it/developers/extend/apps/layout/page-layouts). Un layout
`RECORD_PAGE` aggiunge schede alla vista del record di un oggetto; una scheda con un singolo widget
`FRONT_COMPONENT` ospita il componente a tutta larghezza:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Apri qualsiasi documento — una scheda **Anteprima** lo rende splendido, con link alla pagina web
condivisibile e il PDF:
<Frame caption="La scheda Anteprima rende il documento con stili in linea, più collegamenti rapidi.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Componente frontale del visualizzatore documenti in una scheda record-page" />
</Frame>
## Modifica un modello con l'editor di testo
I modelli non hanno bisogno di un componente personalizzato. Perché il `body` è un campo
`RICH_TEXT`, Venti fornisce già un editor di testo completo per esso — l'
stesso degli oggetti Note e Attività standard utilizzati. Lo superficiamo solo sulla pagina di record di modello
.
Aggiungi una scheda con un widget `FIELD` in modalità di visualizzazione `EDITOR`, puntando sul campo `body`
tramite `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Un campo `RICH_TEXT` memorizza sia il blocco dell'editor JSON che una proiezione Markdown
. La pipeline di generazione legge che Markdown proiezione, quindi
segnaposti, il PDF, e la pagina web condivisibile tutti continuano a lavorare invariato —
vedere il completo
[`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
I widget `FIELD` supportano anche altre modalità di visualizzazione e opzioni, incluso l'elenco dei record di una relazione annidata a due passaggi di distanza — vedi il [riferimento ai widget field](/l/it/developers/extend/apps/layout/page-layouts#field-widgets).
Ora gli editori scrivono i modelli in un corretto editor di testo ricco:
<Frame caption="La scheda Modello: Editor nativo di testo ricco di venti legato al campo corpo.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Template record con la scheda nativa editor di testo ricco" />
</Frame>
**Dopo questo passaggio:** i documenti in anteprima magnificamente e i modelli sono modificabili
in-app. Poi, lasciare che un agente AI li generi da una chat.
<Card title="Il prossimo: un agente AI →" icon="robot" href="/l/it/developers/extend/apps/tutorials/document-generator/ai-agent">
Aggiungi un agente e un'abilità che chiama il tuo strumento.
</Card>

View File

@@ -0,0 +1,135 @@
---
title: 1. Modello dati
icon: database
description: Modelli di documenti e modelli con oggetti, campi e una relazione.
---
La nostra app ha bisogno di due oggetti personalizzati: **modelli di documenti** (cosa scrivere) e
**documenti** (il risultato generato). Le definiamo.
Scaffold each entity file with the CLI — genera un UUID valido e la cartella
giusta per te:
```bash filename="Terminal"
yarn twenty dev:add object
```
Di seguito mostriamo i file finiti.
<Note>
Ogni costante `*_UNIVERSAL_IDENTIFIER` vive in
`src/constants/universal-identifiers.ts` ed è importata dove utilizzata. Gli snippet
qui sotto omettono quelle importazioni per brevità — tenerli nei propri file.
</Note>
## L'oggetto del modello
Un modello ha un `name`, un `body` con `{{placeholders}}`, e un `target` che
dice se è scritto per una persona o un'azienda. Il `body` è un campo
`RICH_TEXT`, quindi Twenty gli dà un editor completo di testo ricco.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
L'opzione `SELECT` **values** deve essere `UPPER_CASE` (`PERSON`, non `person`), e il `defaultValue`
è racchiuso in virgolette extra: `` `'PERSON'` ``. Il `label` è quello che vedono gli utenti
.
</Warning>
## L'oggetto del documento
Il documento generato memorizza il `content` renderizzato e un `status`. Definisci
allo stesso modo, con una selezione `status` di `DRAFT` / `GENERATED`. File completo:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Collegarli con una relazione
Ogni documento deve riportare il modello da cui proveniva. Le relazioni sono
**bidirezionali** — si definiscono entrambi i lati, ciascuno nel proprio file di campo.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
L'altro lato (`template-documents-relation.field.ts`) è un campo
`RelationType.ONE_TO_MANY` chiamato `documents` che punta il senso opposto.
Vedi [Relations](/l/it/developers/extend/apps/data/relations) per il modello completo.
## Vedere in venti
Con `yarn venti dev` in esecuzione, apri **Impostazioni → Modello dati**. Entrambi gli oggetti
appariranno, etichettati con la tua app.
<Frame caption="Entrambi gli oggetti personalizzati, di proprietà dell'app Generatore di documenti.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Impostazioni del modello di dati che mostrano i modelli di documenti e documenti" />
</Frame>
Crea un modello da testare con — chiamalo *Proposta vendite*, imposta **Obiettivo** su
*Persona*, e incolla un corpo con alcuni segnaposti:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Un record di modello. Il corpo mantiene i segnaposto finché non viene generato un documento.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Un record modello di proposta di vendita con corpo segnaposto" />
</Frame>
**Dopo questo passaggio:** hai oggetti `documentTemplate` e `document`, collegati da
una relazione e un modello da cui generare. Successivamente, la logica che lo riempie.
<Card title="Successivo: generare documenti →" icon="bolt" href="/l/it/developers/extend/apps/tutorials/document-generator/generating-documents">
Scrivi la funzione logica che riempie il modello.
</Card>

View File

@@ -0,0 +1,239 @@
---
title: 2. Generazione documenti
icon: bolt
description: Una funzione logica, esposta come strumento AI e un'azione di flusso di lavoro.
---
Ora il core: una [funzione logica](/l/it/developers/extend/apps/logic/logic-functions)
che carica un modello e un record, riempie i segnaposti, e salva un nuovo documento
.
Scriveremo la logica aziendale una volta come **handler**, quindi esponendola attraverso
diversi trigger. Questo capitolo ne collega due — uno **strumento di intelligenza artificiale** e un'azione di workflow
\*\*.
## L'helper del rendering
Mantenere la logica pura nel proprio file in modo che sia facile da unit-test. Questo appiattisce un record
in `{{dot.path}}` token e li sostituisce.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
Poiché questo file non ha effetti collaterali, puoi coprirlo con i test veloci di unità
(`yarn test:unit`). Vedi [Testing](/l/it/developers/extend/apps/operations/testing).
</Tip>
## Il gestore
Il gestore utilizza il [`CoreApiClient`](/l/it/developers/extend/apps/logic/logic-functions)
generato per leggere e scrivere i dati CRM. Carica il modello, carica il record di destinazione, riempie
il corpo e crea un `documento`.
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues` esegue una query diversa per una Persona contro una Società e appiattisce
il risultato — vedi
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## Esporre come strumento e come azione del flusso di lavoro
Un singolo `defineLogicFunction` può portare diversi trigger. Qui, `toolTriggerSettings`
lo rende chiamabile da agenti AI, e `workflowActionTriggerSettings` lo trasforma in un passaggio
nel costruttore di flussi di lavoro visivi. Entrambi descrivono il loro input con uno schema JSON.
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
Lo schema di input è uno schema JSON semplice che descrive `templateId` e `recordId` —
vedi [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## Concedi l'accesso
Le funzioni Logica vengono eseguite come ruolo dell'app. Ha bisogno di leggere i modelli e registra
e creare documenti, in modo da consentire che in `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE` permette alla funzione di caricare il PDF generato nella prossima sezione.
Vedi [Roles](/l/it/developers/extend/apps/config/roles) per i permessi a grana più fine.
## Allega un file PDF reale
Un campo di testo renderizzato è utile, ma gli utenti vogliono un documento reale. Generiamo un file
**PDF** e memorizzalo sul record come file scaricabile.
Innanzitutto, dai al `document` object un campo `FILES` per tenere il PDF. Le app caricano
nei loro **propri** campi di file, quindi questo campo è quello che il caricamento:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
Ora renderizza quel PDF. Un'app è un vero progetto Node, quindi puoi aggiungere qualsiasi pacchetto npm
che ti serve e importarlo come altrove. Usiamo **[pdf-lib](https://pdf-lib.js.org/)**
per disegnare il PDF e **[marked](https://marked.js.org/)** per analizzare il corpo Markdown
— il CLI li installa nel runtime della funzione per te:
```bash filename="Terminal"
yarn add pdf-lib marked
```
L'helper completo è
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Analizza il Markdown in token con `marked. exer`, poi li mette fuori con
pdf-lib: intestazioni reali, **bold**/*italic* runs, proiettile e liste numerate,
blockquotes e regole — un rendering A4 lucido e multi-pagina del modello
stesso, piuttosto che un muro di testo.
<Frame caption="Il PDF generato: la tipografia reale e la formattazione Markdown, rendendo il corpo del modello.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF generato lucido e commerciabile" />
</Frame>
<Note>
pdf-lib's built-in font utilizzare WinAnsi encoding, così gli accenti occidentali europei rendono
fuori dalla scatola; le mappe helper citazioni intelligenti e trattini e lascia i caratteri
non possono codificare. Rendering non latino script (Cinese, Arabo, Cirillico) significherebbe
incorporare un carattere Unicode.
</Note>
Quindi caricarlo e memorizzare il riferimento sul record. `uploadFile` percorre bytes
nel campo dei file di proprietà dell'app; il `id` restituito è quello che salvi:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
Il documento generato ora contiene un PDF scaricabile:
<Frame caption="Il PDF generato, memorizzato nel campo File del documento.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Un record di documento con un file PDF generato" />
</Frame>
<Note>
`uploadFile` si rivolge solo ai campi di file **app-owned** (quindi i caricamenti richiedono sempre un'app
che possiede il campo, più il flag ruolo `UPLOAD_FILE`). Ecco perché il PDF
atterra sul campo `file` del record - lo stesso modello utilizzato dall'app
[call-recorder](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
per le registrazioni.
</Note>
**Dopo questo passaggio:** ogni documento generato ha un PDF reale e scaricabile. Ma
niente può *chiamare* il generatore dall'interfaccia utente ancora — per questo abbiamo bisogno di un percorso HTTP.
<Card title="Il prossimo: percorsi HTTP →" icon="globo" href="/l/it/developers/extend/apps/tutorials/document-generator/http-route">
Servire la funzione su HTTP e rendere i documenti come pagine web.
</Card>

View File

@@ -0,0 +1,150 @@
---
title: 3. Percorsi HTTP
icon: globe
description: Attivare la funzione su HTTP e rendere i documenti come pagine web.
---
Lo stesso gestore può anche rispondere alle richieste HTTP. Aggiungeremo due percorsi:
* un endpoint **POST** per generare un documento, e
* un endpoint pubblico **GET** che rende un documento come una pagina web stampabile.
Entrambi usano `httpRouteTriggerSettings`. Le route dell'app sono accessibili tramite il percorso `/s/`; nelle istanze locali
e self-hosted, tale percorso è gestito dal tuo server Twenty
(ad es. `http://localhost:2020/s/documents/generate`). Su Twenty Cloud, `RestApiClient`
risolve il percorso tramite il dominio delle funzioni dell'area di lavoro.
## Percorso POST — generare su richiesta
Questo riutilizza `generateDocumentHandler`, quindi non c'è alcuna logica da ripetere — solo un sottile adattatore
che legge il corpo della richiesta.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
Il gestore condiviso restituisce un suggerito `status` al fallimento, quindi il percorso può rispondere
con un corretto codice `4xx`/`5xx`. `isAuthRequired: true` significa che il chiamante
deve presentare un token valido — il componente anteriore nel capitolo successivo passa automaticamente il token di accesso dell'utente
.
## OTTIENI il percorso — renderizza come pagina web
Per restituire HTML invece di JSON, avvolgi il corpo in un `Response` con un'intestazione
`Content-Type`. Questo percorso è pubblico (`isAuthRequired: false`) così un documento generato
può essere condiviso come link.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` rende il corpo Markdown in HTML (con [marked](https://marked.js.org/),
sanitizzato) e lo lascia in un pulito, pagina stampabile che mostra solo il contenuto del template
— lo stesso aspetto del PDF e l'anteprima in-app.
[Vedi l'helper](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Provalo
Con un modello e una persona nel tuo workspace, chiama il percorso (prendi un token da
**Impostazioni → API & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Apri il documento restituito nel tuo browser:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="Il percorso GET pubblico rende il documento come una pagina stampabile.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Una pagina web di documento renderizzato" />
</Frame>
<Tip>
Puoi anche trasmettere i log di una funzione durante il test con
`yarn venti dev:function:logs`, o invocarlo direttamente con
`yarn venti dev:function:exec`.
</Tip>
**Dopo questo passaggio:** l'app può generare documenti su HTTP e servirli come pagine web
. Ora rendiamo utilizzabile senza `curl`.
<Card title="Il prossimo: costruire l'interfaccia utente →" icon="table-columns" href="/l/it/developers/extend/apps/tutorials/document-generator/building-the-ui">
Viste, navigazione, un comando e un componente anteriore.
</Card>

View File

@@ -0,0 +1,66 @@
---
title: "Tutorial: Generatore di documenti"
icon: wand-magic-sparkles
description: Crea una vera app Twenty che genera documenti personalizzati a partire dai dati del tuo CRM.
---
In questo tutorial creerai **Document Generator**, un'app che trasforma modelli riutilizzabili in documenti personalizzati usando i dati già presenti nel tuo CRM.
Scrivi un modello una sola volta con `{{placeholders}}`, quindi genera un documento compilato per qualsiasi Persona o Azienda con un solo clic — dal menu dei comandi, da un agente IA o da un workflow.
<Frame caption="Un modello, generato per una persona specifica, aperto come pagina stampabile.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Un documento di proposta di vendita generato" />
</Frame>
## Cosa imparerai
Ogni capitolo aggiunge una funzionalità. Alla fine avrai esplorato gran parte dell’SDK.
| Capitolo | Funzionalità | Riferimento |
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [1. Modello dati](/l/it/developers/extend/apps/tutorials/document-generator/data-model) | Oggetti, campi e una relazione | [Dati](/l/it/developers/extend/apps/data/overview) |
| [2. Generare documenti](/l/it/developers/extend/apps/tutorials/document-generator/generating-documents) | Una funzione logica (strumento IA + azione del workflow) che compila un modello Markdown e allega un PDF rifinito | [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) |
| [3. Rotte HTTP](/l/it/developers/extend/apps/tutorials/document-generator/http-routes) | Servire JSON e una pagina HTML condivisibile dalle rotte | [Funzioni logiche](/l/it/developers/extend/apps/logic/logic-functions) |
| [4. Creare l’interfaccia utente](/l/it/developers/extend/apps/tutorials/document-generator/building-the-ui) | Viste, navigazione, menu dei comandi e componenti front-end che mostrano l’anteprima di un documento e modificano un modello | [Layout](/l/it/developers/extend/apps/layout/overview) |
| [5. Un agente IA](/l/it/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + abilità | [Abilità e agenti](/l/it/developers/extend/apps/logic/skills-and-agents) |
| [6. Pubblicazione](/l/it/developers/extend/apps/tutorials/document-generator/publishing) | Pubblicala nel marketplace | [Pubblicazione](/l/it/developers/extend/apps/operations/publishing) |
## Prerequisiti
Dovresti aver completato il [Quick Start](/l/it/developers/extend/apps/getting-started/quick-start):
un server Twenty locale in esecuzione sulla porta `2020` e la CLI autenticata ad esso.
In caso contrario, esegui ora lo scaffold e avvialo:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Preferisci leggere il codice finito? L’app completa si trova in
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Ogni snippet qui sotto è copiato da lì.
</Note>
## Come è strutturata l’app
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Un modello con segnaposto viene trasformato in un documento rifinito con un PDF, attivato dal menu dei comandi, da un agente IA, da un workflow o da un link condivisibile" />
</Frame>
Scrivi un **modello** una sola volta in un editor rich text, con `{{placeholders}}`. Scegliendo un
modello e un record del CRM si compilano i segnaposto e si salva un
**documento** rifinito (con un file PDF). Tutto il resto — il menu dei comandi, l’agente IA,
lo step del workflow, il link condivisibile — è solo un modo diverso di attivare quell’unico generatore.
## Mantieni attivo questo ciclo
Lascia `yarn twenty dev` in esecuzione in un terminale per l’intero tutorial. Ogni volta che
aggiungi o modifichi un file sotto `src/`, viene nuovamente sincronizzato con il tuo server entro pochi
secondi, così puoi vedere ogni funzionalità comparire nell’interfaccia utente mentre la costruisci.
<Card title="Inizia a sviluppare →" icon="database" href="/l/it/developers/extend/apps/tutorials/document-generator/data-model">
Capitolo 1: modella documenti e modelli.
</Card>

View File

@@ -0,0 +1,136 @@
---
title: 6. Pubblicazione
icon: rocket
description: Aggiungi metadati di mercato e pubblica la tua app.
---
La tua app funziona. L'ultimo passo è descriverlo per il mercato e pubblicarlo.
## Aggiungi metadati di mercato
La [application config](/l/it/developers/extend/apps/config/application) contiene l'identità
che appare nel marketplace: autore, categoria, logo e supporta i link
. Metti un logo in `public/` e fai riferimento con `logo`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/it/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
Il ruolo predefinito viene dichiarato con `defineApplicationRole()` nel proprio file — non passi più `defaultRoleUniversalIdentifier` qui.
</Tip>
Aggiungi anche la parola chiave `twenty-app` a `package.json` così l'app è scopribile:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Aggiungi screenshot galleria
Una quotazione di mercato si vende con screenshot. Trascina alcuni PNG in `public/gallery/` di
e li referenzia con `galleryImages` — rendono come una galleria
nella pagina di elenco, in ordine array.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Lead with the payoff: make the first screenshot the finished result (un generated
document), then show how it's triggered and authored. Usa cattura
croccanti e ad alta risoluzione — sono la prima cosa che un utente vede.
</Tip>
Dare `README.md` lo stesso trattamento — è la prima pagina su npm e GitHub.
Apri con la proposizione del valore e uno screenshot, elenca le caratteristiche del titolo,
quindi tieni i dettagli della build sotto la piega.
## Controllare prima di spedire
Eseguire lo stesso cancelli CI fa:
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
Il piano mostra esattamente cosa verrebbe modificato sul server senza applicare effettivamente le modifiche —
un buon controllo finale di coerenza. Vedi
[Testing](/l/it/developers/extend/apps/operations/testing) e
[Sincronizzazione & recupero](/l/it/developers/extend/apps/operations/sync-and-recovery).
## Pubblica
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` costruisce e pubblica a npm per impostazione predefinita; `--private` carica un tarball
su un registro privato di Twenty server. Per superficiare un'app pubblicata
nel marketplace di un'istanza, attiva una sincronizzazione del catalogo:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Dettagli completi e la lista di controllo rilascio:
[Publishing](/l/it/developers/extend/apps/operations/publishing).
## Hai costruito un'app 🎉
In sei capitoli hai usato la maggior parte della superficie SDK:
* **Oggetti, campi e una relazione** per modellare i dati
* Una **funzione logica** esposta come **strumento di intelligenza artificiale**, un **workflow action**, e **HTTP routes**
* **Viste, navigazione, un comando e un componente frontale** per l'interfaccia utente
* Un **agente + abilità** per la generazione di linguaggio naturale
* **Marketplace metadata** e il flusso di pubblicazione
L'app completata è su
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Dove andare avanti
<CardGroup cols={2}>
<Card title="Riferimento dei dati" icon="database" href="/l/it/developers/extend/apps/data/overview">
Ogni tipo di campo, relazione e opzione indice.
</Card>
<Card title="Riferimento logico" icon="bolt" href="/l/it/developers/extend/apps/logic/overview">
Cron e database-event triggers, il key-value store, connessioni OAuth.
</Card>
<Card title="Riferimento del layout" icon="table-columns" href="/l/it/developers/extend/apps/layout/overview">
Layout di pagina, widget dashboard e più superfici dell'interfaccia utente.
</Card>
<Card title="Operazioni" icon="rocket" href="/l/it/developers/extend/apps/operations/overview">
CLI, prove, telecomandi e IC.
</Card>
</CardGroup>