i18n - docs translations (#19925)

Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-04-21 10:57:27 +02:00
committed by GitHub
parent 15938c1fca
commit cd73088be6
462 changed files with 23407 additions and 21106 deletions
@@ -1,147 +1,55 @@
---
title: API
description: Interroga e modifica i dati del tuo CRM in modo programmatico usando REST o GraphQL.
icon: plug
description: REST and GraphQL APIs generated from your workspace schema.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty è stato progettato per essere adatto agli sviluppatori, offrendo potenti API che si adattano al tuo modello di dati personalizzato. Forniamo quattro tipi distinti di API per soddisfare diverse esigenze di integrazione.
## Schema-per-tenant APIs
## Approccio incentrato sullo sviluppatore
There is no static API reference for Twenty. Each workspace has its own schema — when you add a custom object (say `Invoice`), it immediately gets REST and GraphQL endpoints identical to built-in objects like `Company` or `Person`. The API is generated from the schema, so endpoints use your object and field names directly — no opaque IDs.
Twenty genera API specifiche per il tuo modello di dati:
Your workspace-specific API documentation is available under **Settings → API & Webhooks** after creating an API key. It includes an interactive playground where you can execute real calls against your data.
* **Nessun ID lungo richiesto**: Utilizza direttamente i nomi degli oggetti e dei campi negli endpoint
* **Oggetti standard e personalizzati trattati allo stesso modo**: I tuoi oggetti personalizzati ricevono lo stesso trattamento API di quelli predefiniti
* **Endpoint dedicati**: Ogni oggetto e campo ottiene il proprio endpoint API
* **Documentazione personalizzata**: Generata specificamente per il modello di dati del tuo workspace
## Two APIs
<Note>
La documentazione personalizzata delle tue API è disponibile in **Impostazioni → API & Webhooks** dopo aver creato una chiave API. Poiché Twenty genera API che corrispondono al tuo modello di dati personalizzato, la documentazione è unica per il tuo workspace.
</Note>
**Core API** — `/rest/` and `/graphql/`
## I due tipi di API
CRUD on records: People, Companies, Opportunities, your custom objects. Query, filter, traverse relations.
### Core API
**Metadata API** — `/rest/metadata/` and `/metadata/`
Accessibile su `/rest/` o `/graphql/`
Schema management: create/modify/delete objects, fields, and relations. This is how you programmatically change your data model.
Lavora con i tuoi **record** reali (i dati):
Both are available as REST and GraphQL. GraphQL adds batch upserts and the ability to traverse relations in a single query. Same underlying data either way.
* Crea, leggi, aggiorna, elimina Persone, Aziende, Opportunità, ecc.
* Interroga e filtra i dati
* Gestisci le relazioni tra i record
## Base URLs
### Metadata API
Accessibile su `/rest/metadata/` o `/metadata/`
Gestisci il tuo **workspace e il modello di dati**:
* Crea, modifica o elimina oggetti e campi
* Configura le impostazioni del workspace
* Definisci le relazioni tra oggetti
## REST vs GraphQL
Sia le API Core che le API Metadata sono disponibili nei formati REST e GraphQL:
| Formato | Operazioni disponibili |
| ----------- | ------------------------------------------------------------------------------------- |
| **REST** | CRUD, operazioni batch, upsert |
| **GraphQL** | Stesse funzionalità + **upsert in batch**, query sulle relazioni in un'unica chiamata |
Scegli in base alle tue esigenze — entrambi i formati accedono agli stessi dati.
## Endpoint API
| Ambiente | URL di base |
| ----------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Auto-ospitato** | `https://{your-domain}/` |
| Ambiente | URL di base |
| ----------- | ------------------------- |
| Cloud | `https://api.twenty.com/` |
| Self-Hosted | `https://{your-domain}/` |
## Autenticazione
Ogni richiesta API richiede una chiave API nell'intestazione:
```
Authorization: Bearer YOUR_API_KEY
```
### Crea una chiave API
1. Vai a **Impostazioni → APIs & Webhooks**
2. Fai clic su **+ Crea chiave**
3. Configura:
* **Nome**: Nome descrittivo per la chiave
* **Data di scadenza**: Quando la chiave scade
4. Fai clic su **Salva**
5. **Copia subito** — la chiave viene mostrata una sola volta
Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Roles → Assignment tab** to limit what they can access.
<VimeoEmbed videoId="928786722" title="Creazione della chiave API" />
<Warning>
La tua chiave API concede l'accesso a dati sensibili. Non condividerla con servizi non affidabili. Se compromessa, disabilitala immediatamente e generane una nuova.
</Warning>
For OAuth-based access (external apps acting on behalf of users), see [OAuth](/l/it/developers/extend/oauth).
### Assegna un ruolo a una chiave API
## Batch operations
Per una maggiore sicurezza, assegna un ruolo specifico per limitare l'accesso:
Both REST and GraphQL support batching up to 60 records per request — create, update, or delete. GraphQL also supports batch upsert (create-or-update in one call) using plural names like `CreateCompanies`.
1. Vai a **Impostazioni → Ruoli**
2. Fai clic sul ruolo da assegnare
3. Apri la scheda **Assegnazione**
4. In **Chiavi API**, fai clic su **+ Assegna alla chiave API**
5. Seleziona la chiave API
## Rate limits
La chiave erediterà le autorizzazioni di quel ruolo. Vedi [Autorizzazioni](/l/it/user-guide/permissions-access/capabilities/permissions) per i dettagli.
### Gestisci chiavi API
**Rigenera**: Impostazioni → APIs & Webhooks → Fai clic sulla chiave → **Rigenera**
**Elimina**: Impostazioni → APIs & Webhooks → Fai clic sulla chiave → **Elimina**
## Playground API
Testa le tue API direttamente nel browser con il nostro playground integrato — disponibile sia per **REST** sia per **GraphQL**.
### Accedi al Playground
1. Vai a **Impostazioni → APIs & Webhooks**
2. Crea una chiave API (obbligatorio)
3. Fai clic su **REST API** o **GraphQL API** per aprire il playground
### Cosa ottieni
* **Documentazione interattiva**: Generata per il tuo specifico modello di dati
* **Test in tempo reale**: Esegui chiamate API reali sul tuo workspace
* **Esploratore dello schema**: Sfoglia gli oggetti, i campi e le relazioni disponibili
* **Generatore di richieste**: Crea query con completamento automatico
Il playground riflette i tuoi oggetti e campi personalizzati, quindi la documentazione è sempre accurata per il tuo workspace.
## Operazioni Batch
Sia REST che GraphQL supportano operazioni batch:
* **Dimensione batch**: Fino a 60 record per richiesta
* **Operazioni**: Creare, aggiornare, eliminare più record
**Funzionalità esclusive di GraphQL:**
* **Upsert in batch**: Crea o aggiorna in un'unica chiamata
* Usa nomi oggetto al plurale (ad es. `CreateCompanies` invece di `CreateCompany`)
## Limiti di frequenza delle API
Le richieste API sono limitate per garantire la stabilità della piattaforma:
| Limite | Valore |
| -------------------- | ---------------------- |
| **Richieste** | 100 chiamate al minuto |
| **Dimensione batch** | 60 record per chiamata |
<Tip>
Usa le operazioni batch per massimizzare il throughput — elabora fino a 60 record in una singola chiamata API invece di effettuare richieste individuali.
</Tip>
| Limite | Valore |
| ---------- | ---------------------- |
| Requests | 100 per minute |
| Batch size | 60 record per chiamata |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,434 @@
---
title: CLI & Testing
description: CLI commands, testing setup, public assets, npm packages, remotes, and CI configuration.
icon: terminal
---
## Asset pubblici (cartella `public/`)
La cartella `public/` alla radice della tua app contiene file statici — immagini, icone, font o qualsiasi altro asset di cui la tua app ha bisogno a runtime. Questi file sono inclusi automaticamente nelle build, sincronizzati durante la modalità di sviluppo e caricati sul server.
I file posizionati in `public/` sono:
* **Pubblicamente accessibili** — una volta sincronizzati sul server, gli asset sono serviti a un URL pubblico. Non è necessaria alcuna autenticazione per accedervi.
* **Disponibili nei componenti front-end** — usa gli URL degli asset per visualizzare immagini, icone o qualsiasi media all'interno dei tuoi componenti React.
* **Disponibili nelle funzioni logiche** — fai riferimento agli URL degli asset nelle email, nelle risposte API o in qualsiasi logica lato server.
* **Usati per i metadati del marketplace** — i campi `logoUrl` e `screenshots` in `defineApplication()` fanno riferimento a file di questa cartella (ad es., `public/logo.png`). Questi vengono visualizzati nel marketplace quando la tua app viene pubblicata.
* **Sincronizzati automaticamente in modalità dev** — quando aggiungi, aggiorni o elimini un file in `public/`, viene sincronizzato automaticamente con il server. Nessun riavvio necessario.
* **Inclusi nelle build** — `yarn twenty 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, getPublicAssetUrl } from 'twenty-sdk/define';
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, getPublicAssetUrl } from 'twenty-sdk/define';
export default defineFrontComponent(() => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
});
```
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.
## Uso dei pacchetti npm
Puoi installare e usare qualsiasi pacchetto npm nella tua app. Sia le funzioni logiche sia i componenti front-end vengono impacchettati con [esbuild](https://esbuild.github.io/), che incorpora tutte le dipendenze nell'output — non sono necessari i `node_modules` a runtime.
### Installazione di un pacchetto
```bash filename="Terminal"
yarn add axios
```
Quindi importalo nel tuo codice:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Lo stesso vale per i componenti front-end:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Come funziona il bundling
La fase di build usa esbuild per produrre un singolo file autonomo per ogni funzione logica e per ogni componente front-end. Tutti i pacchetti importati sono incorporati nel bundle.
**Le funzioni logiche** vengono eseguite in un ambiente Node.js. I moduli integrati di Node (`fs`, `path`, `crypto`, `http`, ecc.) sono disponibili e non necessitano di essere installati.
**I componenti front-end** vengono eseguiti in un Web Worker. I moduli integrati di Node non sono disponibili — solo le API del browser e i pacchetti npm che funzionano in un ambiente browser.
Entrambi gli ambienti hanno `twenty-client-sdk/core` e `twenty-client-sdk/metadata` disponibili come moduli preforniti — questi non vengono inclusi nel bundle ma vengono risolti a runtime dal server.
## Testare la tua app
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.
### Impostazione
L'app generata tramite scaffolding include già Vitest. Se la configuri manualmente, installa le dipendenze:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Crea un `vitest.config.ts` alla radice della tua app:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:2020',
TWENTY_API_KEY: 'your-api-key',
},
},
});
```
Crea un file di setup che verifichi che il server sia raggiungibile prima dell'esecuzione dei test:
```ts src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(async () => {
// Verify the server is running
const response = await fetch(`${TWENTY_API_URL}/healthz`);
if (!response.ok) {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Start the server before running integration tests.',
);
}
// Write a temporary config for the SDK
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
}, null, 2),
);
});
```
### API programmatiche dell'SDK
Il sottopercorso `twenty-sdk/cli` esporta funzioni che puoi chiamare direttamente dal codice di test:
| Funzione | Descrizione |
| -------------- | ----------------------------------------------- |
| `appBuild` | Compila l'app e, opzionalmente, crea un tarball |
| `appDeploy` | Carica un tarball sul server |
| `appInstall` | Installa l'app nello spazio di lavoro attivo |
| `appUninstall` | Disinstalla l'app dallo spazio di lavoro attivo |
Ogni funzione restituisce un oggetto risultato con `success: boolean` e `data` oppure `error`.
### Scrivere un test di integrazione
Ecco un esempio completo che compila, distribuisce e installa l'app, quindi verifica che compaia nello spazio di lavoro:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
### Esecuzione dei test
Assicurati che il tuo server Twenty locale sia in esecuzione, quindi:
```bash filename="Terminal"
yarn test
```
Oppure in modalità watch durante lo sviluppo:
```bash filename="Terminal"
yarn test:watch
```
### Controllo dei tipi
Puoi anche eseguire il controllo dei tipi sulla tua app senza eseguire i test:
```bash filename="Terminal"
yarn twenty typecheck
```
Questo esegue `tsc --noEmit` e riporta eventuali errori di tipo.
## Riferimento CLI
Oltre a `dev`, `build`, `add` e `typecheck`, la CLI fornisce comandi per eseguire funzioni, visualizzare i log e gestire le installazioni delle app.
### Esecuzione delle funzioni (`yarn twenty exec`)
Esegui manualmente una funzione logica senza attivarla tramite HTTP, cron o evento del database:
```bash filename="Terminal"
# Execute by function name
yarn twenty exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty exec --postInstall
```
### Visualizzazione dei log delle funzioni (`yarn twenty logs`)
Esegui lo streaming dei log di esecuzione per le funzioni logiche della tua app:
```bash filename="Terminal"
# Stream all function logs
yarn twenty logs
# Filter by function name
yarn twenty logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Questo è diverso da `yarn twenty server logs`, che mostra i log del container Docker. `yarn twenty logs` mostra i log di esecuzione delle funzioni della tua app dal server Twenty.
</Note>
### Disinstallazione di un'app (`yarn twenty uninstall`)
Rimuovi la tua app dallo spazio di lavoro attivo:
```bash filename="Terminal"
yarn twenty uninstall
# Skip the confirmation prompt
yarn twenty 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 --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote list
# Switch the active remote
yarn twenty remote switch <name>
```
Le tue credenziali sono archiviate in `~/.twenty/config.json`.
## CI con GitHub Actions
Lo strumento di scaffolding genera un workflow GitHub Actions pronto all'uso in `.github/workflows/ci.yml`. Esegue automaticamente i test di integrazione a ogni push su `main` e sulle pull request.
Il workflow:
1. Esegue il checkout del tuo codice
2. Avvia un server Twenty temporaneo utilizzando l'azione `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Installa le dipendenze con `yarn install --immutable`
4. Esegue `yarn test` con `TWENTY_API_URL` e `TWENTY_API_KEY` iniettati dagli output dell'azione
```yaml .github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: latest
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
```
Non è necessario configurare alcun secret — l'azione `spawn-twenty-docker-image` avvia un server Twenty effimero direttamente nel runner e fornisce i dettagli di connessione. Il secret `GITHUB_TOKEN` è fornito automaticamente da GitHub.
Per fissare una versione specifica di Twenty invece di `latest`, modifica la variabile d'ambiente `TWENTY_VERSION` all'inizio del workflow.
@@ -0,0 +1,494 @@
---
title: Modello dati
description: Define objects, fields, roles, and application metadata with the Twenty SDK.
icon: database
---
The `twenty-sdk` package provides `defineEntity` functions to declare your app's data model. Devi usare `export default defineEntity({...})` affinché l'SDK rilevi le tue entità. Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
<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. Raggruppare i file per tipo (ad es., `logic-functions/`, `roles/`) è solo una convenzione, non un requisito.
</Note>
<AccordionGroup>
<Accordion title="defineRole" description="Configura i permessi dei ruoli e l'accesso agli oggetti">
I ruoli incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro.
```ts restricted-company-role.ts
import {
defineRole,
PermissionFlag,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} 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,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
</Accordion>
<Accordion title="defineApplication" description="Configura i metadati dell'applicazione (obbligatorio, uno per app)">
Ogni app deve avere esattamente una chiamata a `defineApplication` che descrive:
* **Identità**: identificatori, nome visualizzato e descrizione.
* **Autorizzazioni**: quale ruolo usano le sue funzioni e i componenti front-end.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
* **(Opzionali) Funzioni di pre-installazione/post-installazione**: funzioni logiche che vengono eseguite prima o dopo l'installazione.
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
Note:
* I campi `universalIdentifier` sono ID deterministici 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 (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve fare riferimento a un ruolo definito con `defineRole()` (vedi sopra).
* Le funzioni di pre-installazione e post-installazione vengono rilevate automaticamente durante il build del manifesto — non è necessario farvi riferimento in `defineApplication()`.
#### Metadati del marketplace
Se prevedi di [pubblicare la tua app](/l/it/developers/extend/apps/publishing), questi campi opzionali controllano come appare nel marketplace:
| Campo | Descrizione |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Nome dell'autore o dell'azienda |
| `category` | Categoria dell'app per il filtraggio nel marketplace |
| `logoUrl` | Percorso al logo della tua app (ad es., `public/logo.png`) |
| `screenshots` | Array di percorsi degli screenshot (ad es., `public/screenshot-1.png`) |
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni". Se omesso, il marketplace utilizza il `README.md` del pacchetto da npm |
| `websiteUrl` | Link al tuo sito web |
| `termsUrl` | Link ai Termini di servizio |
| `emailSupport` | Indirizzo email di supporto |
| `issueReportUrl` | Link al sistema di tracciamento dei problemi |
#### Ruoli e permessi
Il `defaultRoleUniversalIdentifier` in `application-config.ts` indica il ruolo predefinito utilizzato dalle funzioni logiche e dai componenti front-end della tua app. Vedi `defineRole` sopra per i dettagli.
* Il token di runtime iniettato come `TWENTY_APP_ACCESS_TOKEN` è derivato da questo ruolo.
* Il client tipizzato è limitato ai permessi concessi a quel ruolo.
* Segui il principio del privilegio minimo: crea un ruolo dedicato con solo i permessi necessari alle tue funzioni.
##### Ruolo funzione predefinito
Quando esegui lo scaffolding di una nuova app, la CLI crea un file di ruolo predefinito:
```ts src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlags: [],
});
```
L'`universalIdentifier` di questo ruolo viene referenziato in `application-config.ts` come `defaultRoleUniversalIdentifier`:
* **\*.role.ts** definisce ciò che il ruolo può fare.
* **application-config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
Note:
* Parti dal ruolo generato dallo scaffolder, quindi restringilo progressivamente seguendo il principio del privilegio minimo.
* Sostituisci `objectPermissions` e `fieldPermissions` con gli oggetti e i campi di cui le tue funzioni hanno realmente bisogno.
* `permissionFlags` controllano l'accesso alle funzionalità a livello di piattaforma. Mantienili al minimo.
* Vedi un esempio funzionante: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
</Accordion>
<Accordion title="defineObject" description="Definisci oggetti personalizzati con campi">
Gli oggetti personalizzati descrivono sia lo schema sia il comportamento per i record nel tuo spazio di lavoro. Usa `defineObject()` per definire oggetti con convalida integrata:
```ts postCard.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:
* Usa `defineObject()` per una convalida integrata e un migliore supporto IDE.
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
* Puoi generare nuovi oggetti con `yarn twenty add`, che ti guida nella denominazione, nei campi e nelle relazioni.
<Note>
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard
come `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`.
Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati.
Puoi sovrascrivere i campi predefiniti definendo un campo con lo stesso nome nel tuo array `fields`,
ma non è consigliato.
</Note>
</Accordion>
<Accordion title="defineField — Campi standard" description="Estendi gli oggetti esistenti con campi aggiuntivi">
Usa `defineField()` per aggiungere campi a oggetti che non possiedi — come gli oggetti standard di Twenty (Person, Company, ecc.) o oggetti di altre app. A differenza dei campi inline in `defineObject()`, i campi autonomi richiedono un `objectUniversalIdentifier` per specificare quale oggetto estendono:
```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, usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` esportati da `twenty-sdk`.
* Quando definisci campi inline in `defineObject()`, **non** hai bisogno di `objectUniversalIdentifier` — viene ereditato dall'oggetto padre.
* `defineField()` è l'unico modo per aggiungere campi a oggetti che non hai creato con `defineObject()`.
</Accordion>
<Accordion title="defineField — Campi di relazione" description="Collega gli oggetti tra loro con relazioni bidirezionali">
Le relazioni collegano gli oggetti tra loro. In Twenty, le relazioni sono sempre **bidirezionali** — definisci entrambi i lati e ciascun lato fa riferimento all'altro.
Esistono due tipi di relazione:
| Tipo di relazione | Descrizione | Ha una chiave esterna? |
| ----------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Molti record di questo oggetto puntano a un record della destinazione | Sì (`joinColumnName`) |
| `ONE_TO_MANY` | Un record di questo oggetto ha molti record della destinazione | No (lato inverso) |
#### Come funzionano le relazioni
Ogni relazione richiede **due campi** che fanno riferimento l'uno all'altro:
1. Il lato **MANY_TO_ONE** — risiede sull'oggetto che detiene la chiave esterna
2. Il lato **ONE_TO_MANY** — risiede sull'oggetto che possiede la collezione
Entrambi i campi usano `FieldType.RELATION` e si riferiscono reciprocamente tramite `relationTargetFieldMetadataUniversalIdentifier`.
#### Esempio: Post Card ha molti destinatari
Supponiamo che un `PostCard` possa essere inviato a molti record `PostCardRecipient`. Ogni destinatario appartiene esattamente a una sola cartolina.
**Passaggio 1: definisci il lato ONE_TO_MANY su PostCard** (il lato "uno"):
```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 file. Il sistema di build le risolve in fase di compilazione.
</Note>
#### Relazioni con gli oggetti standard
Per creare una relazione con un oggetto Twenty integrato (Person, Company, ecc.), usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```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 |
| ------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `tipo` | Sì | Deve essere `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` dell'oggetto di destinazione |
| `relationTargetFieldMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` del campo corrispondente sull'oggetto di destinazione |
| `universalSettings.relationType` | Sì | `RelationType.MANY_TO_ONE` o `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Solo MANY_TO_ONE | Cosa accade quando il record referenziato viene eliminato: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
| `universalSettings.joinColumnName` | Solo MANY_TO_ONE | Nome della colonna del database per la chiave esterna (ad es., `postCardId`) |
#### Campi di relazione inline in defineObject
Puoi anche definire i campi di relazione direttamente all'interno di `defineObject()`. In tal caso, 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
],
});
```
</Accordion>
</AccordionGroup>
## Creazione di entità con lo scaffolding tramite `yarn twenty add`
Invece di creare manualmente i file delle entità, puoi usare lo scaffolder interattivo:
```bash filename="Terminal"
yarn twenty add
```
Questo ti chiede di scegliere un tipo di entità e ti guida attraverso i campi richiesti. Genera 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 add object
yarn twenty add logicFunction
yarn twenty add frontComponent
```
### Tipi di entità disponibili
| Tipo di entità | Comando | File generato |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------- |
| Oggetto | `yarn twenty add object` | `src/objects/\<name>.ts` |
| Campo | `yarn twenty add field` | `src/fields/\<name>.ts` |
| Funzione logica | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
| Componente front-end | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
| Ruolo | `yarn twenty add role` | `src/roles/\<name>.ts` |
| Abilità | `yarn twenty add skill` | `src/skills/\<name>.ts` |
| Agente | `yarn twenty add agent` | `src/agents/\<name>.ts` |
| Vista | `yarn twenty add view` | `src/views/\<name>.ts` |
| Voce del menu di navigazione | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Layout di pagina | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.ts` |
### Cosa genera lo scaffolder
Ogni tipo di entità ha il proprio template. Ad esempio, `yarn twenty 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 add logicFunction --path src/custom-folder
```
@@ -0,0 +1,419 @@
---
title: Componenti front-end
description: Build React components that render inside Twenty's UI with sandboxed isolation.
icon: window-maximize
---
I componenti front-end sono componenti React che vengono renderizzati direttamente all'interno della UI di Twenty. Vengono eseguiti in un **Web Worker** isolato utilizzando Remote DOM — il tuo codice è in sandbox ma viene renderizzato in modo nativo nella pagina, non in un iframe.
## Dove possono essere utilizzati i componenti front.
I componenti front possono essere renderizzati in due posizioni all'interno di Twenty:
* **Pannello laterale** — I componenti front non headless si aprono nel pannello laterale destro. Questo è il comportamento predefinito quando un componente front 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. Quando si configura una dashboard o il layout di una pagina record, gli utenti possono aggiungere un widget del componente front.
## Esempio di base
Il modo più rapido per vedere in azione un componente front-end è registrarlo come **comando**. Aggiungere un campo `command` con `isPinned: true` lo fa apparire come pulsante di azione rapida nell'angolo in alto a destra della pagina — nessun layout di pagina necessario:
```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,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
},
});
```
Dopo la sincronizzazione con `yarn twenty dev` (o eseguendo una volta sola `yarn twenty dev --once`), l'azione rapida appare nell'angolo in alto a destra della pagina:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Pulsante di azione rapida nell'angolo in alto a destra" />
</div>
Fai clic per renderizzare il componente in linea.
## Campi di configurazione
| Campo | Obbligatorio | Descrizione |
| --------------------- | ------------ | ---------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sì | ID univoco stabile per questo componente |
| `component` | Sì | Una funzione di componente React |
| `name` | No | Nome visualizzato |
| `descrizione` | No | Descrizione di ciò che fa il componente |
| `isHeadless` | No | Imposta su `true` se il componente non ha una UI visibile (vedi sotto) |
| `comando` | No | Registra il componente come comando (vedi [opzioni del comando](#command-options) sotto) |
## Posizionare un componente front-end su una pagina
Oltre ai comandi, puoi incorporare un componente front-end direttamente in una pagina di record aggiungendolo come widget in un **layout di pagina**. Vedi la sezione [definePageLayout](/l/it/developers/extend/apps/skills-and-agents#definepagelayout) per i dettagli.
## Headless vs non headless
I componenti front prevedono due modalità di rendering controllate dall'opzione `isHeadless`:
**Non headless (predefinito)** — Il componente renderizza un'interfaccia utente visibile. Quando viene avviato dal menu comandi, si apre nel pannello laterale. Questo è il comportamento predefinito quando `isHeadless` è `false` o omesso.
**Headless (`isHeadless: true`)** — Il componente viene montato in modo invisibile in background. Non apre il pannello laterale. I componenti headless sono pensati per azioni che eseguono una logica e poi si smontano — ad esempio, eseguire un'attività asincrona, navigare a una pagina o mostrare una finestra modale di conferma. Si abbinano naturalmente ai componenti Command dell'SDK descritti di seguito.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
Poiché il componente restituisce `null`, Twenty evita di renderizzare un contenitore per esso — non appare alcuno spazio vuoto nel layout. Il componente ha comunque accesso a tutti gli hook e all'API di comunicazione con l'host.
## Componenti Command dell'SDK
Il pacchetto `twenty-sdk` fornisce quattro componenti di supporto Command progettati per i componenti front headless. Ogni componente esegue un'azione al montaggio, gestisce gli errori mostrando una notifica snackbar e smonta automaticamente il componente front al termine.
Importali da `twenty-sdk/command`:
* **`Command`** — Esegue una callback asincrona tramite la prop `execute`.
* **`CommandLink`** — Naviga verso un percorso dell'app. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Apre una finestra modale di conferma. Se l'utente conferma, esegue la callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Apre una specifica pagina del pannello laterale. Props: `page`, `pageTitle`, `pageIcon`.
Ecco un esempio completo di componente front headless che usa `Command` per eseguire un'azione dal menu comandi:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
E un esempio che usa `CommandModal` per chiedere conferma prima di eseguire:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
## Accesso al contesto di runtime
All'interno del tuo componente, usa gli hook dell'SDK per accedere all'utente corrente, al record e all'istanza del componente:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useRecordId,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const recordId = useRecordId();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
Hook disponibili:
| Hook | Restituisce | Descrizione |
| --------------------------------------------- | ----------------- | --------------------------------------------------------------------- |
| `useUserId()` | `string` o `null` | L'ID dell'utente corrente |
| `useRecordId()` | `string` o `null` | L'ID del record corrente (quando posizionato su una pagina di record) |
| `useFrontComponentId()` | `string` | L'ID di questa istanza di componente |
| `useFrontComponentExecutionContext(selector)` | varia | Accedi all'intero contesto di esecuzione con una funzione selettore |
## API di comunicazione con l'host
I componenti front-end possono attivare navigazione, modali e notifiche utilizzando funzioni da `twenty-sdk`:
| Funzione | Descrizione |
| ----------------------------------------------- | ------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Naviga a una pagina dell'app |
| `openSidePanelPage(params)` | Apri un pannello laterale |
| `closeSidePanel()` | Chiudi il pannello laterale |
| `openCommandConfirmationModal(params)` | Mostra una finestra di conferma |
| `enqueueSnackbar(params)` | Mostra una notifica toast |
| `unmountFrontComponent()` | Smonta il componente |
| `updateProgress(progress)` | Aggiorna un indicatore di avanzamento |
Ecco un esempio che usa l'API host per mostrare una snackbar e chiudere il pannello laterale dopo il completamento di un'azione:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
## Opzioni del comando
Aggiungere un campo `command` a `defineFrontComponent` registra il componente nel menu comandi (Cmd+K). Se `isPinned` è `true`, compare anche come pulsante di azione rapida nell'angolo in alto a destra della pagina.
| Campo | Obbligatorio | Descrizione |
| --------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sì | ID univoco stabile per il comando |
| `etichetta` | Sì | Etichetta completa mostrata nel menu comandi (Cmd+K) |
| `shortLabel` | No | Etichetta breve visualizzata sul pulsante di azione rapida fissato |
| `icona` | No | Nome dell'icona visualizzato accanto all'etichetta (ad es. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | No | Quando `true`, mostra il comando come pulsante di azione rapida nell'angolo in alto a destra della pagina |
| `availabilityType` | No | Controlla dove compare il comando: `'GLOBAL'` (sempre disponibile), `'RECORD_SELECTION'` (solo quando sono selezionati dei record) o `'FALLBACK'` (mostrato quando nessun altro comando corrisponde) |
| `availabilityObjectUniversalIdentifier` | No | Limita il comando alle pagine di uno specifico tipo di oggetto (ad es. solo sui record Company) |
| `conditionalAvailabilityExpression` | No | Un'espressione booleana per controllare dinamicamente se il comando è visibile (vedi sotto) |
## 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:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
export default defineFrontComponent({
universalIdentifier: '...',
name: 'bulk-action',
component: BulkAction,
command: {
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
},
});
```
**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` | `numero` | 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` | `oggetto` | Autorizzazioni per il tipo di oggetto corrente |
| `targetObjectReadPermissions` | `oggetto` | Autorizzazioni di lettura per l'oggetto di destinazione |
| `targetObjectWritePermissions` | `oggetto` | Autorizzazioni di scrittura per l'oggetto di destinazione |
| `featureFlags` | `oggetto` | Flag delle funzionalità attivi |
| `objectMetadataItem` | `oggetto` | Metadati del tipo di oggetto corrente |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Indica se la vista corrente ha un filtro di soft-delete |
**Operatori** — combinano 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 |
## Asset pubblici
I componenti front-end possono accedere ai file dalla directory `public/` dell'app utilizzando `getPublicAssetUrl`:
```tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
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/cli-and-testing#public-assets-public-folder) per i dettagli.
## Stile
I componenti front-end supportano diversi approcci di styling. Puoi usare:
* **Stili inline** — `style={{ color: 'red' }}`
* **Componenti Twenty UI** — importali da `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar e altro)
* **Emotion** — CSS-in-JS con `@emotion/react`
* **Styled-components** — pattern `styled.div`
* **Tailwind CSS** — classi di utilità
* **Qualsiasi libreria CSS-in-JS** compatibile con React
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
@@ -1,12 +1,9 @@
---
title: Per iniziare
icon: rocket
description: Crea la tua prima app Twenty in pochi minuti.
---
<Warning>
Le app sono attualmente in fase alfa. La funzionalità funziona ma è ancora in evoluzione.
</Warning>
## Cosa sono le app?
Le app ti consentono di estendere Twenty con oggetti, campi, funzioni logiche, componenti front-end, competenze IA e altro ancora — il tutto gestito come codice. Invece di configurare tutto tramite l'interfaccia utente, definisci in TypeScript il modello dati e la logica e li distribuisci in uno o più spazi di lavoro.
@@ -0,0 +1,131 @@
---
title: Disposizione
description: Define views, navigation menu items, and page layouts to shape how your app appears in Twenty.
icon: table-columns
---
Layout entities control how your app surfaces inside Twenty's UI — what lives in the sidebar, which saved views ship with the app, and how a record detail page is arranged.
## Layout concepts
| Concept | What it controls | Entità |
| ------------------------ | --------------------------------------------------------------------------------- | -------------------------- |
| **View** | A saved list configuration for an object — visible fields, order, filters, groups | `defineView` |
| **Navigation Menu Item** | An entry in the left sidebar that links to a view or an external URL | `defineNavigationMenuItem` |
| **Page Layout** | The tabs and widgets that make up a record's detail page | `definePageLayout` |
Views, navigation items, and page layouts reference each other by `universalIdentifier`:
* A **navigation menu item** of type `VIEW` points at a `defineView` identifier, so the sidebar link opens that saved view.
* A **page layout** of type `RECORD_PAGE` targets an object and can embed [front components](/l/it/developers/extend/apps/front-components) inside its tabs as widgets.
<AccordionGroup>
<Accordion title="defineView" description="Definisci viste salvate per gli oggetti">
Le viste sono configurazioni salvate di come vengono visualizzati i record di un oggetto — inclusi quali campi sono visibili, il loro ordine e gli eventuali filtri o raggruppamenti applicati. Usa `defineView()` per fornire viste preconfigurate con la tua app:
```ts src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Punti chiave:
* `objectUniversalIdentifier` specifica a quale oggetto si applica questa vista.
* `key` determina il tipo di vista (ad es., `ViewKey.INDEX` per la vista elenco principale).
* `fields` controlla quali colonne compaiono e il loro ordine. Ogni campo fa riferimento a un `fieldMetadataUniversalIdentifier`.
* Puoi anche definire `filters`, `filterGroups`, `groups` e `fieldGroups` per configurazioni più avanzate.
* `position` controlla l'ordinamento quando esistono più viste per lo stesso oggetto.
</Accordion>
<Accordion title="defineNavigationMenuItem" description="Definisci i link di navigazione della barra laterale">
Le voci del menu di navigazione aggiungono elementi personalizzati alla barra laterale dello spazio di lavoro. Usa `defineNavigationMenuItem()` per collegarti a viste, URL esterni o oggetti:
```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 rimanda la voce di menu: `NavigationMenuItemType.VIEW` per una vista salvata o `NavigationMenuItemType.LINK` per un URL esterno.
* Per i link a viste, imposta `viewUniversalIdentifier`. Per i link esterni, imposta `link`.
* `position` controlla l'ordinamento nella barra laterale.
* `icon` e `color` (opzionali) personalizzano l'aspetto.
</Accordion>
<Accordion title="definePageLayout" description="Definisci layout di pagina personalizzati per le viste dei record">
I layout di pagina ti consentono di personalizzare l'aspetto di una pagina dei dettagli di un record — quali schede compaiono, quali widget sono all'interno di ciascuna scheda e come sono disposti. Usa `definePageLayout()` per fornire layout personalizzati con la tua app:
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
Punti chiave:
* `type` è in genere `'RECORD_PAGE'` per personalizzare la vista dei dettagli di un oggetto specifico.
* `objectUniversalIdentifier` specifica a quale oggetto si applica questo layout.
* Ogni `tab` definisce una sezione della pagina con un `title`, `position` e `layoutMode` (`CANVAS` per il layout libero).
* Ogni `widget` all'interno di una scheda può renderizzare un componente front-end, un elenco di relazioni o altri tipi di widget integrati.
* `position` sulle schede controlla il loro ordine. Usa valori più alti (ad es., 50) per posizionare le schede personalizzate dopo quelle integrate.
</Accordion>
</AccordionGroup>
@@ -0,0 +1,559 @@
---
title: Funzioni logiche
description: Define server-side TypeScript functions with HTTP, cron, and database event triggers.
icon: bolt
---
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
<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 { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Tipi di trigger disponibili:
* **httpRoute**: Espone la tua funzione su un percorso e metodo HTTP **sotto l'endpoint `/s/`**:
> ad es. `path: '/post-card/create'` è invocabile su `https://your-twenty-server.com/s/post-card/create`
* **cron**: Esegue la tua funzione secondo una pianificazione utilizzando un'espressione CRON.
* **databaseEvent**: Viene eseguito sugli eventi del ciclo di vita degli oggetti dello spazio di lavoro. Quando l'operazione dell'evento è `updated`, è possibile specificare campi specifici da monitorare nell'array `updatedFields`. Se lasciato non definito o vuoto, qualsiasi aggiornamento attiverà la funzione.
> ad es. `person.updated`, `*.created`, `company.*`
<Note>
Puoi anche eseguire manualmente una funzione utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Puoi osservare i log con:
```bash filename="Terminal"
yarn twenty 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`:
```ts
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
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 }` |
| `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>
#### Esporre una funzione come strumento
Le funzioni logiche possono essere esposte come **strumenti** per gli agenti di IA e i flussi di lavoro. Quando una funzione è contrassegnata come strumento, diventa individuabile dalle funzionalità di IA di Twenty e può essere utilizzata nelle automazioni dei flussi di lavoro.
Per contrassegnare una funzione logica come strumento, imposta `isTool: true`:
```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,
isTool: true,
});
```
Punti chiave:
* Puoi combinare `isTool` con i trigger — una funzione può essere sia uno strumento (invocabile dagli agenti IA) sia attivata da eventi allo stesso tempo.
* **`toolInputSchema`** (opzionale): un oggetto JSON Schema che descrive i parametri accettati dalla funzione. Lo schema viene calcolato automaticamente dall'analisi statica del codice sorgente, ma puoi impostarlo esplicitamente:
```ts
export default defineLogicFunction({
...,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
<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>
<Accordion title="definePostInstallLogicFunction" description="Definisci una funzione logica di post-installazione (una per app)">
Una funzione post-installazione è una funzione logica che viene eseguita automaticamente dopo che la tua app è stata installata in uno spazio di lavoro. Il server la esegue **dopo** che i metadati dell'app sono stati sincronizzati e il client SDK è stato generato, così lo spazio di lavoro è completamente pronto per l'uso e il nuovo schema è attivo. I casi d'uso tipici includono il popolamento di dati predefiniti, la creazione di record iniziali, la configurazione delle impostazioni dello spazio di lavoro o il provisioning di risorse su servizi di terze parti.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --postInstall
```
Punti chiave:
* Le funzioni di post-installazione utilizzano `definePostInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallPayload` con `{ previousVersion?: string; newVersion: string }` — `newVersion` è la versione in fase di installazione e `previousVersion` è la versione installata in precedenza (oppure `undefined` in caso di nuova installazione). Usa questi valori per distinguere le nuove installazioni dagli aggiornamenti e per eseguire logiche di migrazione specifiche per versione.
* **Quando viene eseguito l'hook**: solo sulle nuove installazioni, per impostazione predefinita. Passa `shouldRunOnVersionUpgrade: true` se vuoi che venga eseguito anche quando l'app viene aggiornata da una versione precedente. Se omesso, il flag è `false` per impostazione predefinita e gli aggiornamenti saltano l'hook.
* **Modello di esecuzione — asincrono per impostazione predefinita, sincrono su richiesta**: il flag `shouldRunSynchronously` controlla *come* viene eseguito il post-install.
* `shouldRunSynchronously: false` *(default)* — l'hook viene **messo in coda nella coda dei messaggi** con `retryLimit: 3` ed eseguito in modo asincrono in un worker. La risposta di installazione viene restituita non appena il job è messo in coda, quindi un handler lento o in errore non blocca il chiamante. Il worker riproverà fino a tre volte. **Usalo per job di lunga durata** — popolamento di dataset di grandi dimensioni, chiamate a API di terze parti lente, provisioning di risorse esterne, qualsiasi cosa che possa superare una finestra di risposta HTTP ragionevole.
* `shouldRunSynchronously: true` — l'hook viene eseguito **inline durante il flusso di installazione** (stesso executor del pre-install). La richiesta di installazione rimane bloccata finché l'handler non termina e, se genera un'eccezione, il chiamante dell'installazione riceve un `POST_INSTALL_ERROR`. Nessun tentativo automatico. **Usalo per attività rapide che devono completarsi prima della risposta** — ad esempio, emettere un errore di validazione all'utente, oppure un setup rapido di cui il client avrà bisogno immediatamente dopo il ritorno della chiamata di installazione. Tieni presente che la migrazione dei metadati è già stata applicata quando viene eseguito il post-install, quindi un errore in modalità sincrona **non** annulla le modifiche allo schema — si limita a far emergere l'errore.
* Assicurati che il tuo handler sia idempotente. In modalità asincrona la coda può riprovare fino a tre volte; in entrambe le modalità l'hook può essere eseguito di nuovo durante gli aggiornamenti quando `shouldRunOnVersionUpgrade: true`.
* Le variabili d'ambiente `APPLICATION_ID`, `APP_ACCESS_TOKEN` e `API_URL` sono disponibili all'interno dell'handler (come in qualsiasi altra funzione logica), quindi puoi chiamare le API di Twenty con un token di accesso applicativo con ambito sulla tua app.
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* I campi `universalIdentifier`, `shouldRunOnVersionUpgrade` e `shouldRunSynchronously` della funzione vengono associati automaticamente al manifest dell'applicazione nel campo `postInstallLogicFunction` durante la build — non è necessario referenziarli in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* **Non eseguito in modalità dev**: quando un'app è registrata in locale (tramite `yarn twenty dev`), il server salta completamente il flusso di installazione e sincronizza i file direttamente tramite il watcher della CLI — quindi il post-install non viene mai eseguito in modalità dev, indipendentemente da `shouldRunSynchronously`. Usa `yarn twenty exec --postInstall` per attivarlo manualmente su un workspace in esecuzione.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definisci una funzione logica di pre-installazione (una per app)">
Una funzione di pre-install è una funzione logica che viene eseguita automaticamente durante l'installazione, **prima che venga applicata la migrazione dei metadati del workspace**. Condivide la stessa struttura di payload del post-install (`InstallPayload`), ma è posizionata prima nel flusso di installazione così da poter preparare lo stato da cui dipenderà la migrazione imminente — usi tipici includono il backup dei dati, la validazione della compatibilità con il nuovo schema o l'archiviazione di record che stanno per essere ristrutturati o eliminati.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Punti chiave:
* Le funzioni di pre-install usano `definePreInstallLogicFunction()` — stessa configurazione specialistica del post-install, solo agganciata a uno slot di ciclo di vita diverso.
* Sia gli handler di pre- sia quelli di post-install ricevono lo stesso tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importalo una volta e riutilizzalo per entrambi gli hook.
* **Quando viene eseguito l'hook**: posizionato appena prima della migrazione dei metadati del workspace (`synchronizeFromManifest`). Prima dell'esecuzione, il server esegue una "sincronizzazione ridotta" puramente additiva che registra nei metadati del workspace la funzione di pre-install della versione **nuova** — nient'altro viene toccato — e poi la esegue. Poiché questa sincronizzazione è solo additiva, gli oggetti, i campi e i dati della versione precedente restano intatti quando il tuo handler viene eseguito: puoi leggere ed eseguire in sicurezza il backup dello stato pre-migrazione.
* **Modello di esecuzione**: il pre-install è eseguito **in modo sincrono** e **blocca l'installazione**. Se l'handler genera un'eccezione, l'installazione viene interrotta prima che vengano applicate modifiche allo schema — il workspace rimane sulla versione precedente in uno stato coerente. Questo è intenzionale: il pre-install è la tua ultima possibilità per rifiutare un aggiornamento rischioso.
* Come per il post-install, è consentita una sola funzione di pre-installazione per applicazione. Viene collegata automaticamente al manifest dell'applicazione nel campo `preInstallLogicFunction` durante la build.
* **Non eseguito in modalità dev**: come per il post-install — il flusso di installazione viene completamente saltato per le app registrate localmente, quindi il pre-install non viene mai eseguito con `yarn twenty dev`. Usa `yarn twenty exec --preInstall` per attivarlo manualmente.
</Accordion>
<Accordion title="Pre-install vs post-install: quando usare l'uno o l'altro" description="Scegliere l'hook di installazione giusto">
Entrambi gli hook fanno parte dello stesso flusso di installazione e ricevono lo stesso `InstallPayload`. La differenza è **quando** vengono eseguiti rispetto alla migrazione dei metadati del workspace, e questo modifica quali dati possono gestire in sicurezza.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Il pre-install è sempre **sincrono** (blocca l'installazione e può interromperla). Il post-install è **asincrono per impostazione predefinita** — messo in coda su un worker con retry automatici — ma può optare per l'esecuzione sincrona con `shouldRunSynchronously: true`. Vedi l'accordion `definePostInstallLogicFunction` sopra per quando usare ciascuna modalità.
**Usa `post-install` per tutto ciò che richiede l'esistenza del nuovo schema.** Questo è il caso più comune:
* Popolamento di dati predefiniti (creazione di record iniziali, viste predefinite, contenuti demo) su oggetti e campi appena aggiunti.
* Registrazione di webhook con servizi di terze parti ora che l'app ha le proprie credenziali.
* Chiamare la tua API per completare il setup che dipende dai metadati sincronizzati.
* Logica idempotente di "ensure this exists" che dovrebbe riconciliare lo stato a ogni aggiornamento — da combinare con `shouldRunOnVersionUpgrade: true`.
Esempio — eseguire il seeding di un record `PostCard` predefinito dopo l'installazione:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Usa `pre-install` quando una migrazione altrimenti distruggerebbe o corromperebbe i dati esistenti.** Poiché il pre-install viene eseguito contro lo schema *precedente* e un suo fallimento annulla l'aggiornamento, è il posto giusto per qualsiasi operazione rischiosa:
* **Eseguire il backup dei dati che stanno per essere eliminati o ristrutturati** — ad esempio, stai rimuovendo un campo nella v2 e devi copiarne i valori in un altro campo o esportarli su uno storage prima che venga eseguita la migrazione.
* **Archiviare i record che un nuovo vincolo renderebbe non validi** — ad esempio, un campo sta diventando `NOT NULL` e devi prima eliminare o correggere le righe con valori nulli.
* **Validare la compatibilità e rifiutare l'aggiornamento se i dati attuali non possono essere migrati correttamente** — genera un'eccezione dall'handler e l'installazione si interrompe senza applicare modifiche. Questo è più sicuro che scoprire l'incompatibilità a migrazione in corso.
* **Rinominare o rigenerare le chiavi dei dati** prima di una modifica dello schema che farebbe perdere l'associazione.
Esempio — archiviare i record prima di una migrazione distruttiva:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Regola generale:**
| You want to... | Usa |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Popolare dati predefiniti, configurare il workspace, registrare risorse esterne | `post-install` |
| Eseguire seeding di lunga durata o chiamate a terze parti che non dovrebbero bloccare la risposta dell'installazione | `post-install` (predefinito — `shouldRunSynchronously: false`, con retry del worker) |
| Eseguire un setup rapido di cui il chiamante farà affidamento immediatamente dopo il ritorno della chiamata di installazione | `post-install` con `shouldRunSynchronously: true` |
| Leggere o eseguire il backup dei dati che la prossima migrazione perderebbe | `pre-install` |
| Rifiutare un aggiornamento che corromperebbe i dati esistenti | `pre-install` (genera un'eccezione dall'handler) |
| Eseguire la riconciliazione a ogni aggiornamento | `post-install` con `shouldRunOnVersionUpgrade: true` |
| Eseguire un setup una tantum solo alla prima installazione | `post-install` con `shouldRunOnVersionUpgrade: false` (predefinito) |
<Note>
In caso di dubbio, usa **post-install**. Ricorri al pre-install solo quando la migrazione stessa è distruttiva e devi intercettare lo stato precedente prima che vada perso.
</Note>
</Accordion>
</AccordionGroup>
## 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 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 build`, genera un errore. La generazione avviene automaticamente — la CLI esegue l'introspezione dello schema GraphQL del tuo spazio di lavoro e genera un client tipizzato usando `@genql/cli`.
</Note>
#### Utilizzo di CoreSchema per le annotazioni di tipo
`CoreSchema` fornisce tipi TypeScript corrispondenti agli oggetti del tuo spazio di lavoro — utile per tipizzare lo stato dei componenti o i parametri delle funzioni:
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configurazione dello spazio di lavoro, applicazioni e caricamenti di file">
`MetadataApiClient` è fornito pronto all'uso con l'SDK (nessuna generazione richiesta). Interroga l'endpoint `/metadata` per la configurazione dello spazio di lavoro, le applicazioni e i caricamenti di file.
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Caricamento dei file
`MetadataApiClient` include un metodo `uploadFile` per allegare file ai campi di tipo file:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Parametro | Tipo | Descrizione |
| ---------------------------------- | -------- | ---------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Il contenuto grezzo del file |
| `filename` | `string` | Il nome del file (utilizzato per l'archiviazione e la visualizzazione) |
| `contentType` | `string` | Tipo MIME (predefinito su `application/octet-stream` se omesso) |
| `fieldMetadataUniversalIdentifier` | `string` | L'`universalIdentifier` del campo di tipo file nel tuo oggetto |
Punti chiave:
* Usa l'`universalIdentifier` del campo (non il suo ID specifico dello spazio di lavoro), quindi il tuo codice di upload funziona in qualsiasi spazio di lavoro in cui la tua app è installata.
* L'`url` restituito è un URL firmato che puoi usare per accedere al file caricato.
</Accordion>
</AccordionGroup>
<Note>
Quando il tuo codice viene eseguito su Twenty (funzioni logiche o componenti front-end), la piattaforma inietta le credenziali come variabili d'ambiente:
* `TWENTY_API_URL` — URL di base dell'API di Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Chiave a breve durata con ambito al ruolo funzione predefinito della tua applicazione
Non è **necessario** passarle ai client — vengono lette automaticamente da `process.env`. I permessi della chiave API sono determinati dal ruolo referenziato in `defaultRoleUniversalIdentifier` nel tuo `application-config.ts`.
</Note>
@@ -1,12 +1,9 @@
---
title: Pubblicazione
icon: carica
description: Distribuisci la tua app Twenty nel marketplace oppure distribuiscila internamente.
---
<Warning>
Le app sono attualmente in fase alfa. La funzionalità funziona ma è ancora in evoluzione.
</Warning>
## Panoramica
Una volta che la tua app è stata [compilata e testata localmente](/l/it/developers/extend/apps/building), hai due modalità per distribuirla:
@@ -0,0 +1,69 @@
---
title: Skill e agenti
description: Define AI skills and agents for your app.
icon: robot
---
<Warning>
Skills and agents are currently in alpha. La funzionalità funziona ma è ancora in evoluzione.
</Warning>
Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts.
<AccordionGroup>
<Accordion title="defineSkill" description="Definisci le competenze dell'agente 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.
</Accordion>
</AccordionGroup>
@@ -0,0 +1,189 @@
---
title: OAuth
icon: chiave
description: Authorization code flow with PKCE and client credentials for server-to-server access.
---
Twenty implements OAuth 2.0 with authorization code + PKCE for user-facing apps and client credentials for server-to-server access. Clients are registered dynamically via [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — no manual setup in a dashboard.
## When to Use OAuth
| Scenario | Auth Method |
| --------------------------------------- | -------------------------------------------------------------------------------- |
| Internal scripts, automation | [API Key](/l/it/developers/extend/api#authentication) |
| External app acting on behalf of a user | **OAuth — Authorization Code** |
| Server-to-server, no user context | **OAuth — Client Credentials** |
| Twenty App with UI extensions | [Apps](/l/it/developers/extend/apps/getting-started) (OAuth is handled automatically) |
## Register a Client
Twenty supports **dynamic client registration** per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). No manual setup needed — register programmatically:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**Response:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
Store the `client_secret` securely — it cannot be retrieved later.
</Warning>
## Ambiti
| Scope | Accesso |
| --------- | ---------------------------------------------------- |
| `api` | Full read/write access to the Core and Metadata APIs |
| `profilo` | Read the authenticated user's profile information |
Request scopes as a space-separated string: `scope=api profile`
## Authorization Code Flow
Use this flow when your app acts on behalf of a Twenty user.
### 1. Redirect the user to authorize
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| Parametro | Obbligatorio | Descrizione |
| ----------------------- | ------------ | ------------------------------------------------------------ |
| `client_id` | Sì | Your registered client ID |
| `response_type` | Sì | Must be `code` |
| `redirect_uri` | Sì | Must match a registered redirect URI |
| `scope` | No | Space-separated scopes (defaults to `api`) |
| `stato` | Consigliato | Random string to prevent CSRF attacks |
| `code_challenge` | Consigliato | PKCE challenge (SHA-256 hash of verifier, base64url-encoded) |
| `code_challenge_method` | Consigliato | Must be `S256` when using PKCE |
The user sees a consent screen and approves or denies access.
### 2. Handle the callback
After authorization, Twenty redirects back to your `redirect_uri`:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
Verify that `state` matches what you sent.
### 3. Exchange the code for tokens
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**Response:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. Use the access token
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. Refresh when expired
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## Client Credentials Flow
For server-to-server integrations with no user interaction:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
The returned token has workspace-level access, not tied to any specific user.
## Server Discovery
Twenty publishes its OAuth configuration at a standard discovery endpoint:
```
GET /.well-known/oauth-authorization-server
```
This returns all endpoints, supported grant types, scopes, and capabilities — useful for building generic OAuth clients.
## API Endpoints Summary
| Endpoint | Scopo |
| ----------------------------------------- | --------------------------- |
| `/.well-known/oauth-authorization-server` | Server metadata discovery |
| `/oauth/register` | Dynamic client registration |
| `/oauth/authorize` | User authorization |
| `/oauth/token` | Token exchange and refresh |
| Ambiente | URL di base |
| ----------------- | ------------------------ |
| **Cloud** | `https://api.twenty.com` |
| **Auto-ospitato** | `https://{your-domain}` |
## OAuth vs API Keys
| | API Keys | OAuth |
| ------------------ | ----------------------- | -------------------------------------- |
| **Impostazione** | Generate in Settings | Register a client, implement flow |
| **User context** | None (workspace-level) | Specific user's permissions |
| **Ideale per** | Scripts, internal tools | External apps, multi-user integrations |
| **Token rotation** | Manuale | Automatic via refresh tokens |
| **Scoped access** | Full API access | Granular via scopes |
@@ -1,11 +1,12 @@
---
title: Webhooks
description: Ricevi notifiche in tempo reale quando si verificano eventi nel tuo CRM.
icon: satellite-dish
description: Get notified when records change — HTTP POST to your endpoint on every create, update, or delete.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
I webhook inviano dati ai tuoi sistemi in tempo reale quando si verificano eventi in Twenty — senza necessità di polling. Usali per mantenere sincronizzati i sistemi esterni, attivare automazioni o inviare avvisi.
Twenty sends an HTTP POST to your URL whenever a record is created, updated, or deleted. All object types are covered, including custom objects.
## Crea un Webhook