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

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

View File

@@ -0,0 +1,182 @@
---
title: Porta i dati di prodotto in Twenty
description: Sincronizza i dati del catalogo prodotti da un data warehouse nel tuo CRM in base a una pianificazione.
---
Usa questo modello per mantenere Twenty sincronizzato con i dati di prodotto dal tuo data warehouse (ad esempio, Snowflake, BigQuery, PostgreSQL).
## Struttura del flusso di lavoro
1. **Trigger**: In base a una pianificazione
2. **Code**: Interroga il tuo data warehouse
3. **Code** (opzionale): Formatta i dati come array
4. **Iterator**: Itera su ciascun prodotto
5. **Upsert Record**: Crea o aggiorna in Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Passaggio 1: Pianifica il trigger
Imposta il flusso di lavoro per l'esecuzione con una frequenza che corrisponda alle tue esigenze di aggiornamento dei dati:
* Ogni 5 minuti per una sincronizzazione quasi in tempo reale
* Ogni ora per i dati meno critici
* Ogni giorno per aggiornamenti in batch
## Passaggio 2: Interroga il tuo data warehouse
Aggiungi un'azione **Code** per recuperare i dati recenti:
```javascript
export const main = async () => {
const intervalMinutes = 10; // Match your schedule frequency
const cutoffTime = new Date(Date.now() - intervalMinutes * 60 * 1000).toISOString();
// Replace with your actual data warehouse connection
const response = await fetch("https://your-warehouse-api.com/query", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
query: `
SELECT id, name, sku, price, stock_quantity, updated_at
FROM products
WHERE updated_at >= '${cutoffTime}'
`
})
});
const data = await response.json();
return { products: data.results };
};
```
<Tip>
Filtra per `updated_at >= last X minutes` per recuperare solo i record modificati di recente. Questo mantiene efficiente la sincronizzazione.
</Tip>
## Passaggio 3: Formatta i dati (opzionale)
Se il tuo data warehouse restituisce dati in un formato che richiede una trasformazione, aggiungi un'altra azione **Code**. Le trasformazioni comuni includono conversioni di tipo, ridenominazione dei campi e pulizia dei dati.
### Esempio: Dati utente con campi booleani e di stato
```javascript
export const main = async (params: {
users: any;
}): Promise<object> => {
const { users } = params;
const usersFormatted = typeof users === "string" ? JSON.parse(users) : users;
// Convert string "true"/"false" to actual booleans
const toBool = (v: any) => v === true || v === "true";
return {
users: usersFormatted.map((user) => ({
...user,
activityStatus: String(user.activityStatus).toUpperCase(),
isActiveLast30d: toBool(user.isActiveLast30d),
isActiveLast7d: toBool(user.isActiveLast7d),
isActiveLast24h: toBool(user.isActiveLast24h),
isTwenty: toBool(user.isTwenty),
})),
};
};
```
### Esempio: Dati di prodotto con conversioni di tipo
```javascript
export const main = async (params: { products: any }) => {
const products = typeof params.products === "string"
? JSON.parse(params.products)
: params.products;
return {
products: products.map(product => ({
externalId: product.id,
name: product.name,
sku: product.sku,
price: parseFloat(product.price), // String → Number
stockQuantity: parseInt(product.stock_quantity),
isActive: product.status === "active" // String → Boolean
}))
};
};
```
### Esempio: Formattazione di date e valute
```javascript
export const main = async (params: { deals: any }) => {
const deals = typeof params.deals === "string"
? JSON.parse(params.deals)
: params.deals;
return {
deals: deals.map(deal => ({
...deal,
// Convert Unix timestamp to ISO date
closedAt: deal.closed_timestamp
? new Date(deal.closed_timestamp * 1000).toISOString()
: null,
// Ensure amount is a number (remove currency symbols)
amount: parseFloat(String(deal.amount).replace(/[^0-9.-]/g, "")),
// Normalize stage names
stage: deal.stage?.toLowerCase().replace(/_/g, " ")
}))
};
};
```
### Trasformazioni comuni
| Formato di origine | Formato di destinazione | Codice |
| -------------------- | ----------------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | Data ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Passaggio 4: Itera sui prodotti
Aggiungi un'azione **Iterator**:
* Input: `{{code.products}}`
Questo esegue un ciclo su ogni prodotto nell'array.
## Passaggio 5: Esegui l'upsert di ogni record
All'interno dell'Iterator, aggiungi un'azione **Upsert Record**:
| Impostazione | Valore |
| ---------------------- | ----------------------------------------- |
| **Oggetto** | Il tuo oggetto Prodotto personalizzato |
| **Corrispondenza per** | ID esterno o SKU (identificatore univoco) |
| **Nome** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Prezzo** | `{{iterator.item.price}}` |
<Tip>
Usa **Upsert** (aggiornare o creare) invece di creare rami separati per creazione e aggiornamento. È più veloce da costruire e più facile da eseguire il debug.
</Tip>
## Esempi di casi d'uso
| Fonte | Dati |
| -------------------------- | ------------------------------------------- |
| **Sistema ERP** | Catalogo prodotti, prezzi, inventario |
| **Piattaforma e-commerce** | Ordini, clienti, aggiornamenti dei prodotti |
| **Data warehouse** | Metriche aggregate, dati arricchiti |
| **Sistema di inventario** | Livelli di stock, avvisi di riordino |
## Correlati
* [Trigger dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-triggers)
* [Azioni dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-actions)
* [Gestire gli array nelle azioni Code](/l/it/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Importa gli invii di Typeform in Twenty
description: Gestisci il payload del webhook di Typeform per creare lead dalle risposte del modulo.
---
Per la configurazione standard del webhook, vedi [Configura un trigger webhook](/l/it/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Questo articolo tratta la gestione specifica richiesta dalla struttura di payload personalizzata di Typeform.
### Passaggio 1: Crea un flusso di lavoro webhook
1. Vai a **Impostazioni → Flussi di lavoro**
2. Fai clic su **+ Nuovo flusso di lavoro**
3. Seleziona **Webhook** come trigger
4. Copia l'URL del webhook
### Passaggio 2: Configura Typeform
1. In Typeform, apri il tuo modulo
2. Vai a **Connect → Webhooks**
3. Incolla l'URL del webhook di Twenty
4. Salva
### Passaggio 3: Comprendi il payload di Typeform
Typeform invia una struttura JSON annidata. Ecco un esempio semplificato:
```json
{
"event_type": "form_response",
"form_response": {
"form_id": "abc123",
"submitted_at": "2025-01-15T10:30:00Z",
"answers": [
{
"text": "Jane",
"type": "text",
"field": { "id": "field1", "type": "short_text", "title": "First Name" }
},
{
"text": "Smith",
"type": "text",
"field": { "id": "field2", "type": "short_text", "title": "Last Name" }
},
{
"text": "Acme Corp",
"type": "text",
"field": { "id": "field3", "type": "short_text", "title": "Company" }
},
{
"email": "jane@acme.com",
"type": "email",
"field": { "id": "field4", "type": "email", "title": "Email" }
},
{
"type": "choice",
"field": { "id": "field5", "type": "dropdown", "title": "Team Size" },
"choice": { "label": "10-50" }
}
]
}
}
```
Punti chiave da notare:
* I dati del modulo sono annidati in `form_response`
* **Le risposte vengono restituite come array**, non come campi denominati
* Ogni risposta include il tipo e il titolo del campo come riferimento
### Passaggio 4: Estrai i campi dall'array delle risposte
Poiché `answers` è un array, nei passaggi successivi puoi selezionare solo l'intero array — non i singoli campi. Aggiungi un'azione **Code** per estrarre i campi di cui hai bisogno:
```javascript
export const main = async (params: {
answers: any;
}): Promise<object> => {
const { answers } = params;
// Handle input that may come as a string or an array
const answersFormatted = typeof answers === "string"
? JSON.parse(answers)
: answers;
// Extract fields by position or by finding the field type
const firstName = answersFormatted[0]?.text || "";
const lastName = answersFormatted[1]?.text || "";
const company = answersFormatted[2]?.text || "";
const email = answersFormatted.find(a => a.type === "email")?.email || "";
const teamSize = answersFormatted.find(a => a.type === "choice")?.choice?.label || "";
return {
contact: {
firstName,
lastName,
company,
email,
teamSize
}
};
};
```
Ora, nei passaggi successivi, puoi selezionare `contact.firstName`, `contact.email`, ecc. dal selettore delle variabili.
<Tip>
Per maggiori dettagli sulla gestione degli array, vedi [Gestire gli array nelle azioni Code](/l/it/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Passaggio 5: Crea il record
Aggiungi un'azione **Create Record**:
| Campo | Valore |
| ----------- | ------------------------------------------------- |
| **Oggetto** | Persone |
| **Nome** | `{{code.contact.firstName}}` |
| **Cognome** | `{{code.contact.lastName}}` |
| **Email** | `{{code.contact.email}}` |
| **Azienda** | Cerca o crea in base a `{{code.contact.company}}` |
### Passaggio 6: Testa e attiva
1. Invia una risposta di prova in Typeform
2. Controlla l'esecuzione del flusso di lavoro per verificare che i dati siano stati acquisiti
3. Attiva il flusso di lavoro
## Correlati
* [Configura un trigger webhook](/l/it/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Gestire gli array nelle azioni Code](/l/it/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Genera un PDF da Twenty
description: Crea un flusso di lavoro per generare e allegare un PDF (ad esempio un preventivo) a un record.
---
Genera o recupera automaticamente un PDF e allegalo a un record in Twenty. Questo viene comunemente utilizzato per creare preventivi, fatture o report collegati ad Aziende, Opportunità o altri oggetti.
## Panoramica
Questo flusso di lavoro usa un **Attivatore manuale** così gli utenti possono generare un PDF su richiesta per qualsiasi record selezionato. Una **Funzione logica** gestisce:
1. Scaricare il PDF da un URL (da un servizio di generazione di PDF)
2. Caricare il file su Twenty
3. Creare un allegato collegato al record
## Prerequisiti
Prima di configurare il flusso di lavoro:
1. **Crea una chiave API**: vai su **Impostazioni → API** e crea una nuova chiave API. Avrai bisogno di questo token per la Funzione Logica.
2. **Configura un servizio di generazione PDF** (opzionale): se vuoi generare dinamicamente PDF (ad es. preventivi), usa un servizio come Carbone, PDFMonkey o DocuSeal per creare il PDF e ottenere un URL di download.
## Configurazione passo-passo
### Passaggio 1: Configura il trigger
1. Vai su **Flussi di lavoro** e crea un nuovo flusso di lavoro
2. Seleziona **Attivatore manuale**
3. Scegli l'oggetto a cui vuoi allegare i PDF (ad es. **Company** o **Opportunity**)
<Tip>
Con un Trigger manuale, gli utenti possono eseguire questo flusso di lavoro utilizzando un pulsante che appare in alto a destra una volta selezionato un record: questo esegue il flusso di lavoro per generare e allegare un PDF.
</Tip>
### Passaggio 2: aggiungi una Funzione logica
1. Aggiungi un'azione **Codice** (Funzione logica)
2. Crea una nuova funzione con il codice seguente
3. Configura i parametri di input
#### Parametri di input
| Parametro | Valore |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Se alleghi a un oggetto diverso (Person, Opportunity, ecc.), rinomina di conseguenza il parametro (ad es. `personId`, `opportunityId`) e aggiorna la funzione logica.
</Note>
#### Codice della Funzione logica
```typescript
export const main = async (
params: { companyId: string },
) => {
const { companyId } = params;
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
// Cloud: https://api.twenty.com/graphql
// Self-hosted: https://your-domain.com/graphql
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
// Replace with your API key from Settings → APIs
const authToken = 'YOUR_API_KEY';
// Replace with your PDF URL
// This could be from a PDF generation service or a static URL
const pdfUrl = 'https://your-pdf-service.com/generated-quote.pdf';
const filename = 'quote.pdf';
// Step 1: Download the PDF file
const pdfResponse = await fetch(pdfUrl);
if (!pdfResponse.ok) {
throw new Error(`Failed to download PDF: ${pdfResponse.status}`);
}
const pdfBlob = await pdfResponse.blob();
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
const fieldMetadataIdQuery = `
query FindUploadFileFieldMetadataId {
objects {
edges {
node {
nameSingular
fieldsList {
id
name
}
}
}
}
}
`;
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
const response = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`
},
body: {
query: fieldMetadataIdQuery,
}
});
const result = await response.json();
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
// Step 3: Upload the file via GraphQL multipart upload
const uploadMutation = `
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
id
}
}
`;
const uploadForm = new FormData();
uploadForm.append('operations', JSON.stringify({
query: uploadMutation,
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
}));
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
uploadForm.append('0', pdfFile);
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${authToken}` },
body: uploadForm,
});
const uploadResult = await uploadResponse.json();
if (uploadResult.errors?.length) {
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
}
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
if (!fileId) {
throw new Error('No file id returned from upload');
}
// Step 4: Create the attachment linked to the company
const attachmentMutation = `
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
createAttachment(data: $data) {
id
name
}
}
`;
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: attachmentMutation,
variables: {
data: {
name: filename,
targetCompanyId: companyId,
file: [
{
fileId: fileId,
label: filename
}
]
},
},
}),
});
const attachmentResult = await attachmentResponse.json();
if (attachmentResult.errors?.length) {
throw new Error(`Attachment creation failed: ${attachmentResult.errors[0].message}`);
}
return attachmentResult.data?.createAttachment;
};
```
### Passaggio 3: personalizza per il tuo caso d'uso
#### Per allegare a un oggetto diverso
Sostituisci `targetCompanyId` con il campo appropriato:
| Oggetto | Nome del campo |
| ---------------------- | -------------------------- |
| Azienda | `targetCompanyId` |
| Persona | `targetPersonId` |
| Opportunità | `targetOpportunityId` |
| Oggetto personalizzato | `targetYourCustomObjectId` |
Aggiorna sia il parametro della funzione sia l'oggetto `variables.data` nella mutation dell'allegato.
#### Per usare un URL PDF dinamico
Se usi un servizio di generazione PDF, puoi:
1. Per prima cosa esegui un'azione HTTP Request per generare il PDF
2. Passa l'URL PDF restituito alla funzione logica come parametro
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Passaggio 4: Testa e attiva
1. Salva il flusso di lavoro
2. Vai a un record di Company
3. Fai clic sul menu **⋮** e seleziona il tuo flusso di lavoro
4. Controlla la sezione **Attachments** nel record per verificare che il PDF sia stato allegato
5. Attiva il flusso di lavoro
## Integrazione con servizi di generazione PDF
Per creare preventivi o fatture dinamici:
### Esempio: Genera preventivo → Allega PDF
| Passaggio | Azione | Scopo |
| --------- | ---------------------------- | -------------------------------------------------------- |
| 1 | Attivatore manuale (Company) | L'utente avvia l'operazione su un record |
| 2 | Cerca record | Ottieni i dettagli di Opportunity o delle righe articolo |
| 3 | Richiesta HTTP | Chiama l'API di generazione PDF con i dati del record |
| 4 | Funzione logica | Scarica e allega il PDF generato |
### Servizi di generazione PDF più diffusi
* **Carbone** - Generazione di documenti basata su modelli
* **PDFMonkey** - Creazione dinamica di PDF da modelli
* **DocuSeal** - Piattaforma di automazione dei documenti
* **Documint** - Generazione di documenti API-first
Ogni servizio fornisce un'API che restituisce un URL PDF, che puoi poi passare alla Funzione Logica.
## Risoluzione dei problemi
| Problema | Soluzione |
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
| "Download del PDF non riuscito" | Verifica che l'URL del PDF sia accessibile e restituisca un PDF valido |
| "Caricamento non riuscito" | Verifica che la tua chiave API sia valida e abbia i permessi di scrittura |
| "Creazione dell'allegato non riuscita" | Assicurati che il nome del campo ID dell'oggetto corrisponda all'oggetto di destinazione |
## Correlati
* [Trigger dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-triggers)
* [Funzioni logiche](/l/it/user-guide/workflows/capabilities/workflow-actions#code)
* [Genera un preventivo o una fattura da Twenty](/l/it/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Genera un preventivo o una fattura da Twenty
description: Crea automaticamente fatture negli strumenti esterni quando le opportunità vengono chiuse.
---
Invia automaticamente i dati dell'opportunità al tuo sistema di fatturazione (Stripe, QuickBooks, Xero, ecc.) quando un'opportunità è chiusa come vinta.
## Struttura del flusso di lavoro
1. **Trigger**: Il Record è Aggiornato (Opportunità)
2. **Filtro**: Fase = Closed Won
3. **Search Record**: Recupera i dettagli dell'azienda
4. **Code** (opzionale): Formatta il payload
5. **HTTP Request**: Invia al sistema di fatturazione
## Passaggio 1: Configura il trigger
1. Crea un nuovo flusso di lavoro
2. Seleziona il trigger **Record is Updated**
3. Scegli **Opportunity** come oggetto
## Passaggio 2: Filtra per Closed Won
Aggiungi un'azione **Filtro** per proseguire solo quando l'opportunità è chiusa come vinta:
| Impostazione | Valore |
| -------------- | --------------------------------------- |
| **Campo** | Fase |
| **Condizione** | Uguale a |
| **Valore** | `CLOSED_WON` (o il nome della tua fase) |
<Tip>
Il trigger si attiva a ogni aggiornamento di un'Opportunità. Il filtro garantisce che il flusso di lavoro prosegua solo quando la fase cambia in Closed Won.
</Tip>
## Passaggio 3: Ottieni i dettagli dell'azienda
Il record Opportunity potrebbe non includere tutti i campi dell'azienda necessari per la fattura. Aggiungi un'azione **Search Record**:
| Impostazione | Valore |
| ---------------------- | ------------------------------------------ |
| **Oggetto** | Azienda |
| **Corrispondenza per** | ID uguale a `{{trigger.object.companyId}}` |
Questo recupera il record completo dell'azienda con indirizzo di fatturazione, partita IVA, ecc.
## Passaggio 4: Formatta il payload (opzionale)
Se il tuo sistema di fatturazione richiede un formato specifico, aggiungi un'azione **Code**:
```javascript
export const main = async (params: {
opportunity: any;
company: any;
}): Promise<object> => {
const { opportunity, company } = params;
return {
invoice: {
// Customer info from Company
customer_name: company.name,
customer_email: company.email || "",
billing_address: {
line1: company.address?.street || "",
city: company.address?.city || "",
postal_code: company.address?.postalCode || "",
country: company.address?.country || ""
},
tax_id: company.taxId || null,
// Invoice details from Opportunity
amount: opportunity.amount,
currency: opportunity.currency || "USD",
description: `Invoice for ${opportunity.name}`,
due_days: 30,
// Reference back to Twenty
metadata: {
opportunity_id: opportunity.id,
company_id: company.id
}
}
};
};
```
## Passaggio 5: Invia al sistema di fatturazione
Aggiungi un'azione **HTTP Request**:
| Impostazione | Valore |
| ---------------- | ----------------------------------------------- |
| **Metodo** | POST |
| **URL** | L'endpoint API del tuo sistema di fatturazione |
| **Intestazioni** | `Authorization: Bearer YOUR_API_KEY` |
| **Corpo** | `{{code.invoice}}` o mappa direttamente i campi |
### Esempio: Fattura Stripe
```
POST https://api.stripe.com/v1/invoices
Headers:
Authorization: Bearer sk_live_xxx
Content-Type: application/x-www-form-urlencoded
Body:
customer: {{company.stripeCustomerId}}
collection_method: send_invoice
days_until_due: 30
```
### Esempio: Fattura QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Riepilogo completo del flusso di lavoro
| Passaggio | Azione | Scopo |
| --------- | ----------------------- | --------------------------------------------------------- |
| 1 | Trigger: Record Updated | Si attiva a ogni modifica di un'Opportunità |
| 2 | Filtro | Procedi solo se Fase = Closed Won |
| 3 | Search Record | Ottieni tutti i dettagli dell'azienda per la fatturazione |
| 4 | Codice | Formatta i dati per l'API di fatturazione |
| 5 | Richiesta HTTP | Crea la fattura in un sistema esterno |
## Suggerimenti
* **Memorizza gli ID esterni**: Salva l'ID della fattura restituito dall'API nell'Opportunity utilizzando un'azione **Update Record**
* **Gestione degli errori**: Aggiungi un ramo per inviare una notifica se la richiesta HTTP non riesce
* **Prima prova**: Usa la modalità sandbox/test del tuo sistema di fatturazione prima di andare in produzione
## Correlati
* [Generare un PDF da Twenty](/l/it/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — allegare i PDF generati ai record
* [Trigger dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-triggers)
* [Azioni dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-actions)
* [Automazioni Closed Won](/l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Configura un trigger webhook
description: Ricevi dati da servizi esterni per attivare i flussi di lavoro.
---
I trigger webhook consentono ai servizi esterni di avviare i tuoi flussi di lavoro inviando dati a un URL univoco. Usali per connettere moduli, app di terze parti e integrazioni personalizzate.
## Quando utilizzare i webhook
| Caso d'uso | Esempio |
| ------------------------------- | -------------------------------------------- |
| **Moduli web** | Gli invii del modulo di contatto creano lead |
| **App di terze parti** | Pagamento Stripe → crea un record cliente |
| **Integrazioni personalizzate** | La tua app → automazione Twenty |
| **Strumenti no-code** | Connessioni Zapier, Make, n8n |
## Configurazione passo-passo
### Passaggio 1: Crea il flusso di lavoro
1. Vai a **Impostazioni → Flussi di lavoro**
2. Fai clic su **+ Nuovo flusso di lavoro**
3. Assegnagli un nome (ad es., "Invio modulo sito web")
### Passaggio 2: Configura il trigger webhook
1. Fai clic sul blocco del trigger
2. Seleziona **Webhook**
3. Riceverai un URL webhook univoco come:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Copia questo URL—ti servirà per il tuo servizio esterno
### Passaggio 3: Definisci la struttura dati attesa
Per le richieste **POST**, definisci la struttura del corpo prevista:
1. Fai clic su **Definisci corpo previsto**
2. Inserisci un JSON di esempio che corrisponda a ciò che il tuo servizio invierà:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Fai clic su **Salva**—questo crea variabili che puoi usare nei passaggi successivi
### Passaggio 4: Aggiungi azioni
Ora aggiungi azioni che utilizzano i dati del webhook:
**Esempio: Crea un record Persona**
1. Aggiungi l'azione **Crea record**
2. Seleziona l'oggetto **People**
3. Mappa i campi:
| Campo | Valore |
| ------- | ------------------------------------------------- |
| Nome | `{{trigger.body.firstName}}` |
| Cognome | `{{trigger.body.lastName}}` |
| Email | `{{trigger.body.email}}` |
| Azienda | Cerca o crea in base a `{{trigger.body.company}}` |
### Passaggio 5: Testa il webhook
Prima di attivare, testa il tuo webhook:
**Utilizzando cURL**:
```bash
curl -X POST https://api.twenty.com/webhooks/workflow/abc123... \
-H "Content-Type: application/json" \
-d '{"firstName":"Test","lastName":"User","email":"test@example.com"}'
```
**Utilizzando Postman o simili**:
1. Crea una richiesta POST verso l'URL del tuo webhook
2. Imposta l'header Content-Type su `application/json`
3. Aggiungi il body JSON di test
4. Invia e controlla le esecuzioni del flusso di lavoro
### Passaggio 6: Attiva
Una volta testato, fai clic su **Attiva** per rendere operativo il flusso di lavoro.
## Gestione di strutture dati diverse
### Dati annidati
Se il tuo webhook invia dati annidati:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Fai riferimento con: `{{trigger.body.contact.email}}`
### Array
Se i dati includono array:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
Il modo in cui gestisci gli array dipende dal caso d'uso:
**Numero di elementi sconosciuto → Usa Iterator**
Se devi elaborare ogni elemento dell'array (ad es., creare un record per ciascuno), aggiungi un'azione **Code** per analizzare l'array, quindi usa **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Quindi usa Iterator per iterare su: `{{code.items}}`
**Campi noti/specifici → Estrai in campi denominati**
Se l'array contiene campi specifici a cui vuoi accedere singolarmente (ad es., risposte di un modulo in cui la posizione 0 è sempre "nome", la posizione 1 è sempre "cognome"), aggiungi un'azione **Code** per estrarli:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return {
product: {
name: items[0]?.name || "",
qty: items[0]?.qty || 0
}
};
};
```
Ora puoi selezionare `product.name` e `product.qty` singolarmente nei passaggi successivi.
<Tip>
Per maggiori dettagli sulla gestione degli array, vedi [Gestire gli array nelle azioni Code](/l/it/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Correlati
* [Trigger dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-triggers)
* [Azioni dei flussi di lavoro](/l/it/user-guide/workflows/capabilities/workflow-actions)