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: Importați datele despre produse în Twenty
description: Sincronizați datele catalogului de produse dintr-un depozit de date în CRM-ul dvs., conform unui program.
---
Folosiți acest model pentru a menține Twenty sincronizat cu datele despre produse din depozitul dvs. de date (de ex., Snowflake, BigQuery, PostgreSQL).
## Structura fluxului de lucru
1. **Declanșator**: Conform unui program
2. **Code**: Interogați depozitul de date
3. **Code** (opțional): Formatați datele ca tablou
4. **Iterator**: Parcurgeți fiecare produs
5. **Upsert Record**: Creați sau actualizați în Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Pasul 1: Programați declanșatorul
Configurați fluxul de lucru să ruleze la o frecvență care corespunde nevoilor dvs. privind actualitatea datelor:
* La fiecare 5 minute pentru sincronizare aproape în timp real
* La fiecare oră pentru date mai puțin critice
* Zilnic pentru actualizări în lot
## Pasul 2: Interogați depozitul dvs. de date
Adăugați o acțiune **Code** pentru a prelua datele recente:
```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ți după `updated_at >= last X minutes` pentru a prelua doar înregistrările modificate recent. Acest lucru menține sincronizarea eficientă.
</Tip>
## Pasul 3: Formatați datele (opțional)
Dacă depozitul dvs. de date returnează date într-un format care necesită transformare, adăugați o altă acțiune **Code**. Transformările uzuale includ conversii de tip, redenumirea câmpurilor și curățarea datelor.
### Exemplu: Date despre utilizatori cu câmpuri booleene și de stare
```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),
})),
};
};
```
### Exemplu: Date de produs cu conversii de tip
```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
}))
};
};
```
### Exemplu: Formatarea datelor calendaristice și a valorilor monetare
```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, " ")
}))
};
};
```
### Transformări uzuale
| Format sursă | Format țintă | Cod |
| -------------------- | ---------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | Dată ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Pasul 4: Parcurgeți produsele
Adăugați o acțiune **Iterator**:
* Intrare: `{{code.products}}`
Aceasta parcurge fiecare produs din array.
## Pasul 5: Efectuați upsert pentru fiecare înregistrare
În interiorul iteratorului, adăugați o acțiune **Upsert Record**:
| Setare | Valoare |
| ------------------ | -------------------------------------- |
| **Obiect** | Obiectul dvs. Product personalizat |
| **Potrivire după** | ID extern sau SKU (identificator unic) |
| **Nume** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Preț** | `{{iterator.item.price}}` |
<Tip>
Utilizați **Upsert** (actualizare sau creare) în loc să construiți ramuri separate pentru creare vs. actualizare. Se construiește mai rapid și este mai ușor de depanat.
</Tip>
## Exemple de cazuri de utilizare
| Sursa | Date |
| --------------------------- | ------------------------------------------- |
| **Sistem ERP** | Catalog de produse, prețuri, stocuri |
| **Platformă de e-commerce** | Comenzi, clienți, actualizări de produse |
| **Depozit de date** | Metrici agregate, date îmbogățite |
| **Sistem de inventar** | Niveluri de stoc, alerte de reaprovizionare |
## Conexe
* [Declanșatoare ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-triggers)
* [Acțiuni ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-actions)
* [Gestionați array-urile în acțiunile Code](/l/ro/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Aduceți răspunsurile din Typeform în Twenty
description: Gestionați payload-ul webhook al Typeform pentru a crea potențiali clienți din răspunsurile la formular.
---
Pentru configurarea standard a unui webhook, consultați [Configurați un declanșator Webhook](/l/ro/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Acest articol acoperă gestionarea specifică necesară pentru structura payload-ului personalizat al Typeform.
### Pasul 1: Creați un flux de lucru Webhook
1. Accesați **Setări → Fluxuri de lucru**
2. Faceți clic pe **+ Flux de lucru nou**
3. Selectați **Webhook** ca declanșator
4. Copiați URL-ul webhook
### Pasul 2: Configurați Typeform
1. În Typeform, deschideți formularul dvs.
2. Accesați **Connect → Webhooks**
3. Lipiți URL-ul webhook al Twenty
4. Salvează
### Pasul 3: Înțelegeți payload-ul Typeform
Typeform trimite o structură JSON îmbricată. Iată un exemplu simplificat:
```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" }
}
]
}
}
```
Lucruri importante de reținut:
* Datele formularului sunt îmbricate sub `form_response`
* **Răspunsurile sunt returnate ca un array**, nu ca câmpuri denumite
* Fiecare răspuns include tipul și titlul câmpului pentru referință
### Pasul 4: Extrageți câmpurile din array-ul răspunsurilor
Deoarece `answers` este un array, în pașii următori puteți selecta doar întregul array — nu câmpuri individuale. Adăugați o acțiune **Code** pentru a extrage câmpurile de care aveți nevoie:
```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
}
};
};
```
Acum, în pașii următori, puteți selecta `contact.firstName`, `contact.email` etc. din selectorul de variabile.
<Tip>
Pentru mai multe detalii despre gestionarea array-urilor în acțiunile Code, consultați [Gestionați array-urile în acțiunile Code](/l/ro/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Pasul 5: Creați înregistrarea
Adăugați o acțiune **Create Record**:
| Câmp | Valoare |
| ------------------- | ----------------------------------------------------- |
| **Obiect** | Persoane |
| **Prenume** | `{{code.contact.firstName}}` |
| **Nume de familie** | `{{code.contact.lastName}}` |
| **Email** | `{{code.contact.email}}` |
| **Companie** | Căutați sau creați pe baza `{{code.contact.company}}` |
### Pasul 6: Testați și activați
1. Trimiteți un răspuns de test în Typeform
2. Verificați rularea fluxului de lucru pentru a confirma că datele au fost capturate
3. Activați fluxul de lucru
## Conexe
* [Configurați un declanșator Webhook](/l/ro/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Gestionați array-urile în acțiunile Code](/l/ro/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Generați un PDF din Twenty
description: Creați un flux de lucru pentru a genera și atașa un PDF (de exemplu, o ofertă) la o înregistrare.
---
Generați sau preluați automat un PDF și atașați-l la o înregistrare în Twenty. Aceasta este utilizată frecvent pentru a crea oferte, facturi sau rapoarte care sunt asociate cu Companii, Oportunități sau alte obiecte.
## Prezentare generală
Acest flux de lucru folosește un **Manual Trigger** pentru ca utilizatorii să poată genera un PDF la cerere pentru orice înregistrare selectată. O **funcție logică** se ocupă de:
1. Descărcarea PDF-ului dintr-un URL (de la un serviciu de generare PDF)
2. Încărcarea fișierului în Twenty
3. Crearea unui atașament asociat înregistrării
## Cerințe
Înainte de a configura fluxul de lucru:
1. **Creați o cheie API**: Accesați **Settings → APIs** și creați o nouă cheie API. Vei avea nevoie de acest jeton pentru Funcția Logică.
2. **Configurați un serviciu de generare PDF** (opțional): Dacă doriți să generați dinamic fișiere PDF (de ex., oferte), utilizați un serviciu precum Carbone, PDFMonkey sau DocuSeal pentru a crea PDF-ul și a obține un URL de descărcare.
## Configurare pas cu pas
### Pasul 1: Configurați declanșatorul
1. Accesați **Workflows** și creați un nou flux de lucru
2. Selectați **Manual Trigger**
3. Alegeți obiectul la care doriți să atașați fișiere PDF (de ex., **Companie** sau **Oportunitate**)
<Tip>
Cu un declanșator manual, utilizatorii pot rula acest flux de lucru folosind un buton care apare în dreapta sus odată ce o înregistrare este selectată — aceasta rulează fluxul de lucru pentru a genera și atașează un PDF.
</Tip>
### Pasul 2: Adăugați o funcție logică
1. Adauga o actiune **Codul** (Functie Logica)
2. Creați o funcție nouă cu codul de mai jos
3. Configurați parametrii de intrare
#### Parametri de intrare
| Parametru | Valoare |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Dacă atașați la un alt obiect (Persoană, Oportunitate etc.), redenumiți parametrul în mod corespunzător (de ex., `personId`, `opportunityId`) și actualizați funcția logică.
</Note>
#### Codul funcției logice
```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;
};
```
### Pasul 3: Personalizați pentru cazul dvs. de utilizare
#### Pentru a atașa la un alt obiect
Înlocuiți `targetCompanyId` cu câmpul corespunzător:
| Obiect | Nume câmp |
| ------------------- | -------------------------- |
| Companie | `targetCompanyId` |
| Persoană | `targetPersonId` |
| Oportunitate | `targetOpportunityId` |
| Obiect personalizat | `targetYourCustomObjectId` |
Actualizați atât parametrul funcției, cât și obiectul `variables.data` în mutația de atașare.
#### Pentru a utiliza un URL PDF dinamic
Dacă utilizați un serviciu de generare PDF, puteți:
1. Mai întâi creați o acțiune HTTP Request pentru a genera PDF-ul
2. Transmiteți URL-ul PDF returnat către funcția logică ca parametru
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Pasul 4: Testați și activați
1. Salvați fluxul de lucru
2. Navigați la o înregistrare de tip Company
3. Faceți clic pe meniul **⋮** și selectați fluxul dvs. de lucru
4. Verificați secțiunea **Attachments** din înregistrare pentru a confirma că PDF-ul a fost atașat
5. Activați fluxul de lucru
## Combinare cu servicii de generare PDF
Pentru crearea de oferte sau facturi dinamice:
### Exemplu: Generați ofertă → Atașați PDF
| Pas | Acțiune | Scop |
| --- | ------------------------ | ------------------------------------------------------ |
| 1 | Manual Trigger (Company) | Utilizatorul inițiază dintr-o înregistrare |
| 2 | Căutare înregistrare | Obțineți detalii despre Opportunity sau despre poziții |
| 3 | Solicitare HTTP | Apelați API-ul de generare PDF cu datele înregistrării |
| 4 | Funcție logică | Descărcați și atașați PDF-ul generat |
### Servicii populare de generare PDF
* **Carbone** - Generare de documente pe bază de șabloane
* **PDFMonkey** - Creare dinamică de PDF-uri din șabloane
* **DocuSeal** - Platformă de automatizare a documentelor
* **Documint** - Generare de documente orientată pe API
Fiecare serviciu furnizează un API care returnează un URL PDF, pe care apoi îl puteți trece la Funcția Logică.
## Depanare
| Problemă | Soluție |
| ------------------------------------- | ---------------------------------------------------------------------------- |
| "Nu s-a reușit descărcarea PDF-ului" | Verificați că URL-ul PDF este accesibil și returnează un PDF valid |
| "Nu s-a reușit încărcarea" | Verificați că cheia API este validă și are permisiuni de scriere |
| "Nu s-a reușit crearea atașamentului" | Asigurați-vă că numele câmpului ID al obiectului corespunde obiectului țintă |
## Conexe
* [Declanșatoare ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-triggers)
* [Funcții logice](/l/ro/user-guide/workflows/capabilities/workflow-actions#code)
* [Generați o ofertă sau o factură din Twenty](/l/ro/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Generați o ofertă sau o factură din Twenty
description: Creați automat facturi în instrumente externe atunci când se închid oportunitățile.
---
Trimiteți automat datele oportunității către sistemul dvs. de facturare (Stripe, QuickBooks, Xero etc.) atunci când o oportunitate este câștigată.
## Structura fluxului de lucru
1. **Declanșator**: Înregistrarea este actualizată (Oportunitate)
2. **Filtru**: Etapă = Closed Won
3. **Căutare înregistrare**: Obțineți detaliile Companiei
4. **Cod** (opțional): Formatați payload-ul
5. **Cerere HTTP**: Trimiteți către sistemul de facturare
## Pasul 1: Configurați declanșatorul
1. Creați un nou flux de lucru
2. Selectați declanșatorul **Record is Updated**
3. Alegeți **Opportunity** ca obiect
## Pasul 2: Filtrați pentru Closed Won
Adăugați o acțiune **Filter** pentru a continua doar când oportunitatea este câștigată:
| Setare | Valoare |
| ------------ | ------------------------------------- |
| **Câmp** | Etapa |
| **Condiție** | Egal |
| **Valoare** | `CLOSED_WON` (sau numele etapei dvs.) |
<Tip>
Declanșatorul se activează la orice actualizare a unei Oportunități. Filtrul asigură că fluxul de lucru continuă doar când etapa se schimbă la Closed Won.
</Tip>
## Pasul 3: Obțineți detalii despre Companie
Înregistrarea Oportunității s-ar putea să nu includă toate câmpurile Companiei de care aveți nevoie pentru factură. Adăugați o acțiune **Search Record**:
| Setare | Valoare |
| ------------------ | ----------------------------------------- |
| **Obiect** | Companie |
| **Potrivire după** | ID egal cu `{{trigger.object.companyId}}` |
Aceasta preia înregistrarea completă a Companiei cu adresa de facturare, ID fiscal etc.
## Pasul 4: Formatați payload-ul (opțional)
Dacă sistemul dvs. de facturare așteaptă un format specific, adăugați o acțiune **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
}
}
};
};
```
## Pasul 5: Trimiteți către sistemul de facturare
Adăugați o acțiune **HTTP Request**:
| Setare | Valoare |
| ------------ | ------------------------------------------------------ |
| **Metodă** | POST |
| **URL** | Punctul final al API-ului sistemului dvs. de facturare |
| **Anteturi** | `Authorization: Bearer YOUR_API_KEY` |
| **Corp** | `{{code.invoice}}` sau mapați câmpurile direct |
### Exemplu: Factură 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
```
### Exemplu: Factură QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Rezumat complet al fluxului de lucru
| Pas | Acțiune | Scop |
| --- | ------------------------------------------- | ---------------------------------------------------------- |
| 1 | Declanșator: Înregistrarea este actualizată | Se activează la orice modificare a unei Oportunități |
| 2 | Filtru | Continuați doar dacă Etapa = Closed Won |
| 3 | Căutare înregistrare | Obțineți detaliile complete ale Companiei pentru facturare |
| 4 | Cod | Formatați datele pentru API-ul de facturare |
| 5 | Solicitare HTTP | Creați factura în sistemul extern |
## Sfaturi
* **Stocați ID-urile externe**: Salvați ID-ul facturii returnat de API înapoi în Oportunitate folosind o acțiune **Update Record**
* **Gestionarea erorilor**: Adăugați o ramură pentru a trimite o notificare dacă cererea HTTP eșuează
* **Testați mai întâi**: Utilizați modul sandbox/test al sistemului dvs. de facturare înainte de lansare
## Conexe
* [Generați un PDF din Twenty](/l/ro/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — atașați PDF-uri generate la înregistrări
* [Declanșatoare ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-triggers)
* [Acțiuni ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-actions)
* [Automatizări pentru Closed Won](/l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Configurați un declanșator webhook
description: Primiți date de la servicii externe pentru a declanșa fluxuri de lucru.
---
Declanșatoarele webhook permit serviciilor externe să pornească fluxurile dvs. de lucru trimițând date către un URL unic. Folosiți-le pentru a conecta formulare, aplicații terțe și integrări personalizate.
## Când să folosiți webhook-uri
| Caz de utilizare | Exemplu |
| --------------------------- | ---------------------------------------------------------------- |
| **Formulare web** | Trimiterile din formularul de contact creează potențiali clienți |
| **Aplicații terțe** | Plată Stripe → creează o înregistrare de client |
| **Integrări personalizate** | Aplicația dvs. → automatizare în Twenty |
| **Instrumente no-code** | Conexiuni Zapier, Make, n8n |
## Configurare pas cu pas
### Pasul 1: Creați fluxul de lucru
1. Accesați **Setări → Fluxuri de lucru**
2. Faceți clic pe **+ Flux de lucru nou**
3. Denumiți-l (de ex., "Trimitere formular site")
### Pasul 2: Configurați declanșatorul webhook
1. Faceți clic pe blocul declanșator
2. Selectați **Webhook**
3. Veți primi un URL de webhook unic, de exemplu:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Copiați acest URL — veți avea nevoie de el pentru serviciul dvs. extern
### Pasul 3: Definiți structura de date așteptată
Pentru cererile **POST**, definiți structura așteptată a corpului:
1. Faceți clic pe **Define expected body**
2. Introduceți un exemplu JSON care corespunde cu ceea ce va trimite serviciul dvs.:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Faceți clic pe **Salvați** — aceasta creează variabile pe care le puteți utiliza în pașii următori
### Pasul 4: Adăugați acțiuni
Acum adăugați acțiuni care folosesc datele webhook-ului:
**Exemplu: Creați o înregistrare Person**
1. Adăugați acțiunea **Create Record**
2. Selectați obiectul **People**
3. Asociați câmpurile:
| Câmp | Valoare |
| --------------- | ----------------------------------------------------- |
| Prenume | `{{trigger.body.firstName}}` |
| Nume de familie | `{{trigger.body.lastName}}` |
| Email | `{{trigger.body.email}}` |
| Companie | Căutați sau creați pe baza `{{trigger.body.company}}` |
### Pasul 5: Testați webhook-ul
Înainte de activare, testați webhook-ul:
**Folosind 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"}'
```
**Folosind Postman sau similar**:
1. Creați o cerere POST către URL-ul webhook-ului
2. Setați antetul Content-Type la `application/json`
3. Adăugați corpul JSON de test
4. Trimiteți și verificați rulările fluxului de lucru
### Pasul 6: Activați
După testare, faceți clic pe **Activați** pentru a activa fluxul de lucru.
## Gestionarea diferitelor structuri de date
### Date îmbricate
Dacă webhook-ul trimite date îmbricate:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Faceți referire astfel: `{{trigger.body.contact.email}}`
### Tablouri
Dacă datele includ tablouri:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
Modul în care gestionați tablourile depinde de cazul dvs. de utilizare:
**Număr necunoscut de elemente → Folosiți Iterator**
Dacă trebuie să procesați fiecare element din tablou (de ex., să creați o înregistrare pentru fiecare), adăugați o acțiune **Code** pentru a analiza tabloul, apoi folosiți **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Apoi utilizați Iterator pentru a itera: `{{code.items}}`
**Câmpuri cunoscute/specifice → Extrageți în câmpuri denumite**
Dacă tabloul conține câmpuri specifice la care doriți să accesați individual (de ex., răspunsuri de formular în care poziția 0 este întotdeauna "prenume", poziția 1 este întotdeauna "nume"), adăugați o acțiune **Code** pentru a le extrage:
```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
}
};
};
```
Acum puteți selecta `product.name` și `product.qty` individual în pașii următori.
<Tip>
Pentru mai multe detalii despre gestionarea tablourilor, consultați [Gestionarea tablourilor în acțiunile Code](/l/ro/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Conexe
* [Declanșatoare ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-triggers)
* [Acțiuni ale fluxurilor de lucru](/l/ro/user-guide/workflows/capabilities/workflow-actions)