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: Importer les données de produits dans Twenty
description: Synchronisez les données du catalogue de produits depuis un entrepôt de données vers votre CRM selon une planification.
---
Utilisez ce modèle pour garder Twenty synchronisé avec les données produits de votre entrepôt de données (par ex. Snowflake, BigQuery, PostgreSQL).
## Structure du flux de travail
1. **Déclencheur** : Selon une planification
2. **Code** : Interroger votre entrepôt de données
3. **Code** (facultatif) : Formater les données en tableau
4. **Iterator** : Parcourir chaque produit
5. **Upsert Record** : Créer ou mettre à jour dans Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Étape 1 : Planifier le déclencheur
Définissez le flux de travail pour s'exécuter à une fréquence correspondant à vos besoins d'actualisation des données :
* Toutes les 5 minutes pour une synchronisation quasi en temps réel
* Toutes les heures pour des données moins critiques
* Quotidiennement pour des mises à jour par lots
## Étape 2 : Interroger votre entrepôt de données
Ajoutez une action **Code** pour récupérer les données récentes :
```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>
Filtrez par `updated_at >= last X minutes` pour ne récupérer que les enregistrements récemment modifiés. Cela rend la synchronisation plus efficace.
</Tip>
## Étape 3 : Formater les données (facultatif)
Si votre entrepôt renvoie des données dans un format nécessitant une transformation, ajoutez une autre action **Code**. Les transformations courantes incluent les conversions de type, le renommage de champs et le nettoyage des données.
### Exemple : données utilisateur avec champs booléens et de statut
```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),
})),
};
};
```
### Exemple : données produit avec conversions de type
```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
}))
};
};
```
### Exemple : formatage des dates et des devises
```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, " ")
}))
};
};
```
### Transformations courantes
| Format source | Format cible | Code |
| -------------------- | ---------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | Date ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Étape 4 : Itérer sur les produits
Ajoutez une action **Itérateur** :
* Entrée : `{{code.products}}`
Cela parcourt chaque produit du tableau.
## Étape 5 : Effectuer un upsert pour chaque enregistrement
À l'intérieur de l'itérateur, ajoutez une action **Upsert Record** :
| Paramètre | Valeur |
| ---------------------- | -------------------------------------- |
| **Objet** | Votre objet Produit personnalisé |
| **Correspondance par** | ID externe ou SKU (identifiant unique) |
| **Nom** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Prix** | `{{iterator.item.price}}` |
<Tip>
Utilisez **Upsert** (mettre à jour ou créer) au lieu de construire des branches distinctes pour la création et la mise à jour. C'est plus rapide à construire et plus facile à déboguer.
</Tip>
## Exemples de cas d'utilisation
| Source | Données |
| --------------------------------- | ------------------------------------------------ |
| **Système ERP** | Catalogue de produits, tarification, stocks |
| **Plateforme e-commerce** | Commandes, clients, mises à jour des produits |
| **Entrepôt de données** | Indicateurs agrégés, données enrichies |
| **Système de gestion des stocks** | Niveaux de stock, alertes de réapprovisionnement |
## Articles connexes
* [Déclencheurs de workflows](/l/fr/user-guide/workflows/capabilities/workflow-triggers)
* [Actions de workflows](/l/fr/user-guide/workflows/capabilities/workflow-actions)
* [Gérer les tableaux dans les actions Code](/l/fr/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Importer les réponses de Typeform dans Twenty
description: Gérez la charge utile du webhook de Typeform pour créer des prospects à partir des réponses au formulaire.
---
Pour la configuration standard d'un webhook, voir [Configurer un déclencheur de webhook](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Cet article couvre le traitement spécifique nécessaire pour la structure de charge utile personnalisée de Typeform.
### Étape 1 : Créer un flux de travail de webhook
1. Allez dans **Paramètres → Flux de travail**
2. Cliquez sur **+ Nouveau flux de travail**
3. Sélectionnez **Webhook** comme déclencheur
4. Copiez l'URL du webhook
### Étape 2 : Configurer Typeform
1. Dans Typeform, ouvrez votre formulaire
2. Allez dans **Connect → Webhooks**
3. Collez l'URL du webhook Twenty
4. Enregistrer
### Étape 3 : Comprendre la charge utile de Typeform
Typeform envoie une structure JSON imbriquée. Voici un exemple simplifié :
```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" }
}
]
}
}
```
Points clés à noter :
* Les données du formulaire sont imbriquées sous `form_response`
* **Les réponses sont renvoyées sous forme de tableau**, et non comme des champs nommés
* Chaque réponse inclut le type et le titre du champ à titre de référence
### Étape 4 : Extraire les champs du tableau des réponses
Comme `answers` est un tableau, vous ne pouvez sélectionner que le tableau entier dans les étapes suivantes — pas des champs individuels. Ajoutez une action **Code** pour extraire les champs dont vous avez besoin :
```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
}
};
};
```
Dans les étapes suivantes, vous pouvez sélectionner `contact.firstName`, `contact.email`, etc. à partir du sélecteur de variables.
<Tip>
Pour plus de détails sur la gestion des tableaux dans les actions Code, voir [Gérer les tableaux dans les actions Code](/l/fr/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Étape 5 : Créer l'enregistrement
Ajoutez une action **Créer un enregistrement** :
| Champ | Valeur |
| ------------------ | ------------------------------------------------------------- |
| **Objet** | Personnes |
| **Prénom** | `{{code.contact.firstName}}` |
| **Nom de famille** | `{{code.contact.lastName}}` |
| **E-mail** | `{{code.contact.email}}` |
| **Entreprise** | Rechercher ou créer en fonction de `{{code.contact.company}}` |
### Étape 6 : Tester et activer
1. Soumettez une réponse de test dans Typeform
2. Vérifiez l'exécution du flux de travail pour confirmer que les données ont été capturées
3. Activez le flux de travail
## Articles connexes
* [Configurer un déclencheur de webhook](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Gérer les tableaux dans les actions Code](/l/fr/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Générer un PDF depuis Twenty
description: Créez un flux de travail pour générer et joindre un PDF (par exemple un devis) à un enregistrement.
---
Générez ou récupérez automatiquement un PDF et joignez-le à un enregistrement dans Twenty. Cette fonctionnalité est couramment utilisée pour créer des devis, des factures ou des rapports liés aux Entreprises, aux Opportunités ou à d'autres objets.
## Vue d'ensemble
Ce flux de travail utilise un **Déclencheur manuel** afin que les utilisateurs puissent générer un PDF à la demande pour tout enregistrement sélectionné. Une **fonction logique** gère :
1. Téléchargement du PDF depuis une URL (provenant d'un service de génération de PDF)
2. Téléversement du fichier dans Twenty
3. Création d'une pièce jointe liée à l'enregistrement
## Prérequis
Avant de configurer le flux de travail :
1. **Créer une clé API** : Allez dans **Settings → APIs** et créez une nouvelle clé API. Vous aurez besoin de ce jeton pour la fonction logique.
2. **Configurer un service de génération de PDF** (facultatif) : Si vous souhaitez générer des PDF dynamiquement (par exemple, des devis), utilisez un service comme Carbone, PDFMonkey ou DocuSeal pour créer le PDF et obtenir une URL de téléchargement.
## Configuration pas à pas
### Étape 1 : Configurer le déclencheur
1. Allez dans **Flux de travail** et créez un nouveau flux de travail
2. Sélectionnez **Déclencheur manuel**
3. Choisissez l'objet auquel vous souhaitez joindre des PDF (par exemple, **Entreprise** ou **Opportunité**)
<Tip>
Avec un déclencheur manuel, les utilisateurs peuvent exécuter ce workflow en utilisant un bouton qui apparaît en haut à droite une fois qu'un enregistrement est sélectionné — cela exécute le workflow pour générer et joindre un PDF.
</Tip>
### Étape 2 : Ajouter une fonction logique
1. Ajoute une action **Code** (fonction logique)
2. Créez une nouvelle fonction avec le code ci-dessous
3. Configurez les paramètres d'entrée
#### Paramètres d'entrée
| Paramètre | Valeur |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Si vous l'attachez à un objet différent (Personne, Opportunité, etc.), renommez le paramètre en conséquence (par exemple, `personId`, `opportunityId`) et mettez à jour la fonction logique.
</Note>
#### Code de la fonction logique
```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;
};
```
### Étape 3 : Personnaliser selon votre cas d'utilisation
#### Pour joindre à un objet différent
Remplacez `targetCompanyId` par le champ approprié :
| Objet | Nom du champ |
| ------------------ | -------------------------- |
| Entreprise | `targetCompanyId` |
| Personne | `targetPersonId` |
| Opportunité | `targetOpportunityId` |
| Objet personnalisé | `targetYourCustomObjectId` |
Mettez à jour à la fois le paramètre de la fonction et l'objet `variables.data` dans la mutation de pièce jointe.
#### Pour utiliser une URL de PDF dynamique
Si vous utilisez un service de génération de PDF, vous pouvez :
1. Commencez par créer une action HTTP Request pour générer le PDF
2. Transmettez l'URL du PDF retournée à la fonction logique en tant que paramètre
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Étape 4 : Tester et activer
1. Enregistrez le flux de travail
2. Accédez à un enregistrement d'Entreprise
3. Cliquez sur le menu **⋮** et sélectionnez votre flux de travail
4. Vérifiez la section **Pièces jointes** de l'enregistrement pour confirmer que le PDF a été joint
5. Activez le flux de travail
## Combiner avec des services de génération de PDF
Pour créer des devis ou des factures dynamiques :
### Exemple : Générer un devis → Joindre le PDF
| Étape | Action | Objectif |
| ----- | ------------------------------- | ----------------------------------------------------------------------- |
| 1 | Déclencheur manuel (Entreprise) | L'utilisateur lance l'action depuis un enregistrement |
| 2 | Rechercher un enregistrement | Obtenir les détails de l'Opportunité ou des lignes d'articles |
| 3 | Requête HTTP | Appeler l'API de génération de PDF avec les données de l'enregistrement |
| 4 | Fonction logique | Télécharger et joindre le PDF généré |
### Services populaires de génération de PDF
* **Carbone** - Génération de documents basée sur des modèles
* **PDFMonkey** - Création de PDF dynamiques à partir de modèles
* **DocuSeal** - Plateforme d'automatisation de documents
* **Documint** - Génération de documents axée sur l'API
Chaque service fournit une API qui retourne une URL PDF, que vous pouvez ensuite passer à la fonction logique.
## Résolution des problèmes
| Problème | Solution |
| ----------------------------------------- | ----------------------------------------------------------------------------- |
| "Échec du téléchargement du PDF" | Vérifiez que l'URL du PDF est accessible et renvoie un PDF valide |
| "Échec du téléversement" | Vérifiez que votre clé API est valide et dispose des autorisations d'écriture |
| "Échec de la création de la pièce jointe" | Assurez-vous que le nom du champ d'ID d'objet correspond à votre objet cible |
## Articles connexes
* [Déclencheurs de workflows](/l/fr/user-guide/workflows/capabilities/workflow-triggers)
* [Fonctions logiques](/l/fr/user-guide/workflows/capabilities/workflow-actions#code)
* [Générer un devis ou une facture depuis Twenty](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Générer un devis ou une facture depuis Twenty
description: Créer automatiquement des factures dans des outils externes lorsque des affaires se clôturent.
---
Envoyez automatiquement les données de l'affaire vers votre système de facturation (Stripe, QuickBooks, Xero, etc.) lorsqu'une opportunité est gagnée.
## Structure du flux de travail
1. **Déclencheur** : L'enregistrement est mis à jour (Opportunité)
2. **Filtre** : Étape = Closed Won
3. **Rechercher un enregistrement** : Obtenir les détails de l'Entreprise
4. **Code** (facultatif) : Mettre en forme la charge utile
5. **Requête HTTP** : Envoyer au système de facturation
## Étape 1 : Configurer le déclencheur
1. Créer un nouveau workflow
2. Sélectionnez le déclencheur **L'enregistrement est mis à jour**
3. Choisissez **Opportunité** comme objet
## Étape 2 : Filtrer pour Closed Won
Ajoutez une action **Filtre** pour ne continuer que lorsque l'affaire est gagnée :
| Paramètre | Valeur |
| ------------- | --------------------------------------- |
| **Champ** | Étape |
| **Condition** | Égal à |
| **Valeur** | `CLOSED_WON` (ou le nom de votre étape) |
<Tip>
Le déclencheur se déclenche lors de toute mise à jour de l'Opportunité. Le Filtre garantit que le flux de travail ne continue que lorsque l'étape passe à Closed Won.
</Tip>
## Étape 3 : Obtenir les détails de l'Entreprise
L'enregistrement de l'Opportunité peut ne pas inclure tous les champs de l'Entreprise dont vous avez besoin pour la facture. Ajoutez une action **Rechercher un enregistrement** :
| Paramètre | Valeur |
| ---------------------- | ---------------------------------------- |
| **Objet** | Entreprise |
| **Correspondance par** | ID égal à `{{trigger.object.companyId}}` |
Cela récupère l'enregistrement complet de l'Entreprise avec l'adresse de facturation, l'ID fiscal, etc.
## Étape 4 : Formater la charge utile (facultatif)
Si votre système de facturation attend un format spécifique, ajoutez une action **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
}
}
};
};
```
## Étape 5 : Envoyer au système de facturation
Ajoutez une action **Requête HTTP** :
| Paramètre | Valeur |
| ------------ | ---------------------------------------------------------------- |
| **Méthode** | POST |
| **URL** | Votre point de terminaison API de facturation |
| **En-têtes** | `Authorization: Bearer YOUR_API_KEY` |
| **Corps** | `{{code.invoice}}` ou faites correspondre les champs directement |
### Exemple : Facture 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
```
### Exemple : Facture QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Récapitulatif complet du flux de travail
| Étape | Action | Objectif |
| ----- | --------------------------------------- | ------------------------------------------------------------ |
| 1 | Déclencheur : Enregistrement mis à jour | Se déclenche à chaque modification d'une Opportunité |
| 2 | Filtre | Ne poursuivre que si Étape = Closed Won |
| 3 | Rechercher un enregistrement | Obtenir tous les détails de l'Entreprise pour la facturation |
| 4 | Code | Mettre en forme les données pour l'API de facturation |
| 5 | Requête HTTP | Créer la facture dans le système externe |
## Conseils
* **Stockez les ID externes** : Enregistrez l'ID de la facture renvoyé par l'API dans l'Opportunité à l'aide d'une action **Mettre à jour l'enregistrement**
* **Gestion des erreurs** : Ajoutez une branche pour envoyer une notification si la requête HTTP échoue
* **Testez d'abord** : Utilisez le mode bac à sable/test de votre système de facturation avant la mise en production
## Articles connexes
* [Générer un PDF à partir de Twenty](/l/fr/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — joindre les PDF générés aux enregistrements
* [Déclencheurs de flux de travail](/l/fr/user-guide/workflows/capabilities/workflow-triggers)
* [Actions de flux de travail](/l/fr/user-guide/workflows/capabilities/workflow-actions)
* [Automatisations Closed Won](/l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Configurer un déclencheur de webhook
description: Recevez des données de services externes pour déclencher des flux de travail.
---
Les déclencheurs de webhook permettent à des services externes de démarrer vos flux de travail en envoyant des données vers une URL unique. Utilisez-les pour connecter des formulaires, des applications tierces et des intégrations personnalisées.
## Quand utiliser les webhooks
| Cas d'utilisation | Exemple |
| ------------------------------- | -------------------------------------------------------------- |
| **Formulaires web** | Les soumissions de formulaires de contact créent des prospects |
| **Applications tierces** | Paiement Stripe → création d'un enregistrement client |
| **Intégrations personnalisées** | Votre application → automatisation dans Twenty |
| **Outils sans code** | Connexions Zapier, Make, n8n |
## Configuration pas à pas
### Étape 1 : Créer le flux de travail
1. Allez dans **Paramètres → Flux de travail**
2. Cliquez sur **+ Nouveau flux de travail**
3. Nommez-le (par exemple, "Soumission de formulaire de site web")
### Étape 2 : Configurer le déclencheur de webhook
1. Cliquez sur le bloc de déclenchement
2. Sélectionnez **Webhook**
3. Vous recevrez une URL de webhook unique, par exemple :
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Copiez cette URL — vous en aurez besoin pour votre service externe
### Étape 3 : Définir la structure de données attendue
Pour les requêtes **POST**, définissez la structure du corps attendue :
1. Cliquez sur **Définir le corps attendu**
2. Saisissez un JSON d'exemple correspondant à ce que votre service enverra :
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Cliquez sur **Enregistrer** — cela crée des variables que vous pouvez utiliser dans les étapes suivantes
### Étape 4 : Ajouter des actions
Ajoutez maintenant des actions qui utilisent les données du webhook :
**Exemple : Créer un enregistrement de personne**
1. Ajoutez l'action **Créer un enregistrement**
2. Sélectionnez l'objet **People**
3. Associez les champs :
| Champ | Valeur |
| -------------- | ------------------------------------------------------------- |
| Prénom | `{{trigger.body.firstName}}` |
| Nom de famille | `{{trigger.body.lastName}}` |
| E-mail | `{{trigger.body.email}}` |
| Entreprise | Rechercher ou créer en fonction de `{{trigger.body.company}}` |
### Étape 5 : Tester le webhook
Avant l'activation, testez votre webhook :
**Avec 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"}'
```
**Avec Postman ou équivalent** :
1. Créez une requête POST vers votre URL de webhook
2. Définissez l'en-tête Content-Type sur `application/json`
3. Ajoutez votre corps JSON de test
4. Envoyez et vérifiez les exécutions du flux de travail
### Étape 6 : Activer
Une fois testé, cliquez sur **Activer** pour rendre le flux de travail actif.
## Gestion de différentes structures de données
### Données imbriquées
Si votre webhook envoie des données imbriquées :
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Faites référence avec : `{{trigger.body.contact.email}}`
### Tableaux
Si les données comprennent des tableaux :
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
La manière de gérer les tableaux dépend de votre cas d'utilisation :
**Nombre d'éléments inconnu → Utilisez Iterator**
Si vous devez traiter chaque élément du tableau (par exemple, créer un enregistrement pour chacun), ajoutez une action **Code** pour analyser le tableau, puis utilisez **Iterator** :
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Utilisez ensuite Iterator pour itérer sur : `{{code.items}}`
**Champs connus/spécifiques → Extraire dans des champs nommés**
Si le tableau contient des champs spécifiques auxquels vous souhaitez accéder individuellement (par exemple, des réponses de formulaire où la position 0 est toujours "prénom", la position 1 est toujours "nom"), ajoutez une action **Code** pour les extraire :
```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
}
};
};
```
Vous pouvez maintenant sélectionner `product.name` et `product.qty` individuellement dans les étapes suivantes.
<Tip>
Pour plus de détails sur la gestion des tableaux, voir [Gérer les tableaux dans les actions Code](/l/fr/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Connexes
* [Déclencheurs de flux de travail](/l/fr/user-guide/workflows/capabilities/workflow-triggers)
* [Actions de flux de travail](/l/fr/user-guide/workflows/capabilities/workflow-actions)