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: Produktdaten in Twenty importieren
description: Synchronisieren Sie Produktkatalogdaten nach Zeitplan aus einem Data Warehouse in Ihr CRM.
---
Verwenden Sie dieses Muster, um Twenty mit Produktdaten aus Ihrem Data Warehouse (z. B. Snowflake, BigQuery, PostgreSQL) synchron zu halten.
## Workflow-Struktur
1. **Auslöser**: Nach Zeitplan
2. **Code**: Ihr Data Warehouse abfragen
3. **Code** (optional): Daten als Array formatieren
4. **Iterator**: Jedes Produkt durchlaufen
5. **Upsert Record**: In Twenty erstellen oder aktualisieren
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Schritt 1: Auslöser planen
Stellen Sie den Workflow so ein, dass er mit einer Häufigkeit läuft, die Ihren Anforderungen an die Datenaktualität entspricht:
* Alle 5 Minuten für eine nahezu Echtzeit-Synchronisierung
* Stündlich für weniger kritische Daten
* Täglich für Batch-Updates
## Schritt 2: Ihr Data Warehouse abfragen
Fügen Sie eine **Code**-Aktion hinzu, um aktuelle Daten abzurufen:
```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>
Filtern Sie nach `updated_at >= last X minutes`, um nur kürzlich geänderte Datensätze abzurufen. So bleibt die Synchronisierung effizient.
</Tip>
## Schritt 3: Daten formatieren (optional)
Wenn Ihr Data Warehouse Daten in einem Format zurückgibt, das transformiert werden muss, fügen Sie eine weitere **Code**-Aktion hinzu. Häufige Transformationen umfassen Typkonvertierungen, Feldumbenennungen und Datenbereinigung.
### Beispiel: Benutzerdaten mit booleschen und Statusfeldern
```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),
})),
};
};
```
### Beispiel: Produktdaten mit Typkonvertierungen
```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
}))
};
};
```
### Beispiel: Datums- und Währungsformatierung
```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, " ")
}))
};
};
```
### Häufige Transformationen
| Quellformat | Zielformat | Code |
| -------------------- | ---------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | ISO-Datum | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Schritt 4: Durch die Produkte iterieren
Fügen Sie eine **Iterator**-Aktion hinzu:
* Eingabe: `{{code.products}}`
Dies durchläuft jedes Produkt im Array.
## Schritt 5: Jeden Datensatz upserten
Fügen Sie innerhalb des Iterators eine **Upsert Record**-Aktion hinzu:
| Einstellung | Wert |
| --------------------- | -------------------------------------------- |
| **Objekt** | Ihr benutzerdefiniertes Produktobjekt |
| **Abgleichen anhand** | Externe ID oder SKU (eindeutiger Bezeichner) |
| **Name** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Preis** | `{{iterator.item.price}}` |
<Tip>
Verwenden Sie **Upsert** (aktualisieren oder erstellen), anstatt separate Verzweigungen für Erstellen vs. Aktualisieren zu bauen. Das ist schneller aufzubauen und leichter zu debuggen.
</Tip>
## Beispielanwendungsfälle
| Quelle | Daten |
| ----------------------------- | --------------------------------------------- |
| **ERP-System** | Produktkatalog, Preise, Bestand |
| **E-Commerce-Plattform** | Bestellungen, Kunden, Produktaktualisierungen |
| **Data Warehouse** | Aggregierte Metriken, angereicherte Daten |
| **Bestandsverwaltungssystem** | Bestandsmengen, Nachbestellwarnungen |
## Verwandt
* [Workflow-Trigger](/l/de/user-guide/workflows/capabilities/workflow-triggers)
* [Workflow-Aktionen](/l/de/user-guide/workflows/capabilities/workflow-actions)
* [Arrays in Code-Aktionen verarbeiten](/l/de/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Typeform-Formulareinsendungen in Twenty übernehmen
description: Verarbeiten Sie die Webhook-Payload von Typeform, um aus Formulareinsendungen Leads zu erstellen.
---
Für die Standard-Webhook-Einrichtung siehe [Webhook-Trigger einrichten](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Dieser Artikel behandelt die spezifische Verarbeitung, die für die benutzerdefinierte Payload-Struktur von Typeform erforderlich ist.
### Schritt 1: Webhook-Workflow erstellen
1. Gehen Sie zu **Einstellungen → Workflows**
2. Klicken Sie auf **+ Neuer Workflow**
3. Wählen Sie **Webhook** als Trigger
4. Kopieren Sie die Webhook-URL
### Schritt 2: Typeform konfigurieren
1. Öffnen Sie in Typeform Ihr Formular
2. Gehen Sie zu **Connect → Webhooks**
3. Fügen Sie Ihre Twenty-Webhook-URL ein
4. Speichern
### Schritt 3: Die Typeform-Payload verstehen
Typeform sendet eine verschachtelte JSON-Struktur. Hier ist ein vereinfachtes Beispiel:
```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" }
}
]
}
}
```
Wichtige Hinweise:
* Formulardaten sind unter `form_response` verschachtelt
* **Antworten werden als Array zurückgegeben**, nicht als benannte Felder
* Jede Antwort enthält den Feldtyp und den Titel als Referenz
### Schritt 4: Felder aus dem `answers`-Array extrahieren
Da `answers` ein Array ist, können Sie in den folgenden Schritten nur das gesamte Array auswählen — nicht einzelne Felder. Fügen Sie eine **Code**-Aktion hinzu, um die benötigten Felder zu extrahieren:
```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
}
};
};
```
In den folgenden Schritten können Sie nun `contact.firstName`, `contact.email` usw. aus der Variablenauswahl auswählen.
<Tip>
Weitere Details zum Umgang mit Arrays in Code-Aktionen finden Sie unter [Arrays in Code-Aktionen verarbeiten](/l/de/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Schritt 5: Datensatz erstellen
Fügen Sie die Aktion **Datensatz erstellen** hinzu:
| Feld | Wert |
| --------------- | ------------------------------------------------------------------ |
| **Objekt** | Personen |
| **Vorname** | `{{code.contact.firstName}}` |
| **Nachname** | `{{code.contact.lastName}}` |
| **E-Mail** | `{{code.contact.email}}` |
| **Unternehmen** | Suchen oder erstellen Sie basierend auf `{{code.contact.company}}` |
### Schritt 6: Testen und aktivieren
1. Senden Sie in Typeform eine Testantwort ab
2. Überprüfen Sie die Workflow-Ausführung, um zu bestätigen, dass die Daten erfasst wurden
3. Aktivieren Sie den Workflow
## Verwandt
* [Webhook-Trigger einrichten](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Arrays in Code-Aktionen verarbeiten](/l/de/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Ein PDF aus Twenty generieren
description: Erstellen Sie einen Workflow, um ein PDF (z. B. ein Angebot) zu generieren und an einen Datensatz anzuhängen.
---
Automatisch ein PDF generieren oder abrufen und an einen Datensatz in Twenty anhängen. Dies wird häufig verwendet, um Angebote, Rechnungen oder Berichte zu erstellen, die mit Companies, Opportunities oder anderen Objekten verknüpft sind.
## Übersicht
Dieser Workflow verwendet einen **Manuellen Auslöser**, damit Benutzer bei Bedarf für jeden ausgewählten Datensatz ein PDF generieren können. Eine **Logikfunktion** übernimmt:
1. Das Herunterladen des PDFs von einer URL (von einem PDF-Generierungsdienst)
2. Das Hochladen der Datei in Twenty
3. Das Erstellen eines Anhangs, der mit dem Datensatz verknüpft ist
## Voraussetzungen
Bevor Sie den Workflow einrichten:
1. **API-Schlüssel erstellen**: Gehen Sie zu **Einstellungen → APIs** und erstellen Sie einen neuen API-Schlüssel. Sie benötigen dieses Token für die Logic Funktion.
2. **Richten Sie einen PDF-Generierungsdienst ein** (optional): Wenn Sie PDFs dynamisch generieren möchten (z. B. Angebote), verwenden Sie einen Dienst wie Carbone, PDFMonkey oder DocuSeal, um das PDF zu erstellen und eine Download-URL zu erhalten.
## Schritt-für-Schritt-Einrichtung
### Schritt 1: Trigger konfigurieren
1. Gehen Sie zu **Workflows** und erstellen Sie einen neuen Workflow
2. Wählen Sie **Manueller Auslöser**
3. Wählen Sie das Objekt aus, dem Sie PDFs anhängen möchten (z. B. **Company** oder **Opportunity**)
<Tip>
Mit einem manuellen Trigger Benutzer können diesen Workflow mit einem Button ausführen, der rechts oben erscheint, sobald ein Datensatz ausgewählt ist — dies führt den Workflow aus, um eine PDF zu generieren und anzuhängen.
</Tip>
### Schritt 2: Logikfunktion hinzufügen
1. Füge eine **Code** Aktion (Logikfunktion) hinzu
2. Erstellen Sie eine neue Funktion mit dem folgenden Code
3. Konfigurieren Sie die Eingabeparameter
#### Eingabeparameter
| Parameter | Wert |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Wenn Sie an ein anderes Objekt anhängen (Person, Opportunity usw.), benennen Sie den Parameter entsprechend um (z. B. `personId`, `opportunityId`) und aktualisieren Sie die Logikfunktion.
</Note>
#### Code der Logikfunktion
```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;
};
```
### Schritt 3: An Ihren Anwendungsfall anpassen
#### An ein anderes Objekt anhängen
Ersetzen Sie `targetCompanyId` durch das entsprechende Feld:
| Objekt | Feldname |
| -------------------------- | -------------------------- |
| Unternehmen | `targetCompanyId` |
| Person | `targetPersonId` |
| Opportunity | `targetOpportunityId` |
| Benutzerdefiniertes Objekt | `targetYourCustomObjectId` |
Aktualisieren Sie sowohl den Funktionsparameter als auch das Objekt `variables.data` in der Attachment-Mutation.
#### So verwenden Sie eine dynamische PDF-URL
Wenn Sie einen PDF-Generierungsdienst verwenden, können Sie:
1. Führen Sie zuerst eine HTTP-Request-Aktion aus, um das PDF zu generieren
2. Übergeben Sie die zurückgegebene PDF-URL als Parameter an die Logikfunktion
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Schritt 4: Testen und aktivieren
1. Speichern Sie den Workflow
2. Navigieren Sie zu einem Company-Datensatz
3. Klicken Sie auf das **⋮**-Menü und wählen Sie Ihren Workflow aus
4. Überprüfen Sie im Datensatz den Bereich **Attachments**, um zu verifizieren, dass das PDF angehängt wurde
5. Aktivieren Sie den Workflow
## Kombination mit PDF-Generierungsdiensten
Zum Erstellen dynamischer Angebote oder Rechnungen:
### Beispiel: Angebot generieren → PDF anhängen
| Schritt | Aktion | Zweck |
| ------- | ---------------------------- | ------------------------------------------------ |
| 1 | Manueller Auslöser (Company) | Benutzer initiiert am Datensatz |
| 2 | Datensatz suchen | Opportunity- oder Positionsdetails abrufen |
| 3 | HTTP-Anfrage | PDF-Generierungs-API mit Datensatzdaten aufrufen |
| 4 | Logikfunktion | Das generierte PDF herunterladen und anhängen |
### Beliebte PDF-Generierungsdienste
* **Carbone** – Vorlagenbasierte Dokumentenerstellung
* **PDFMonkey** – Dynamische PDF-Erstellung aus Vorlagen
* **DocuSeal** – Plattform für Dokumentenautomatisierung
* **Documint** – API-first-Dokumentenerstellung
Jeder Dienst stellt eine API zur Verfügung, die eine PDF-URL zurückgibt, die Sie dann an die Logic Funktion übergeben können.
## Fehlerbehebung
| Problem | Lösung |
| ----------------------------------------- | -------------------------------------------------------------------------------------- |
| "PDF konnte nicht heruntergeladen werden" | Prüfen Sie, ob die PDF-URL erreichbar ist und ein gültiges PDF zurückgibt |
| "Upload fehlgeschlagen" | Stellen Sie sicher, dass Ihr API-Schlüssel gültig ist und Schreibberechtigungen hat |
| "Anhangserstellung fehlgeschlagen" | Stellen Sie sicher, dass der Feldname der Objekt-ID mit Ihrem Zielobjekt übereinstimmt |
## Verwandt
* [Workflow-Trigger](/l/de/user-guide/workflows/capabilities/workflow-triggers)
* [Logikfunktionen](/l/de/user-guide/workflows/capabilities/workflow-actions#code)
* [Ein Angebot oder eine Rechnung aus Twenty generieren](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Ein Angebot oder eine Rechnung aus Twenty erstellen.
description: Rechnungen in externen Tools automatisch erstellen, wenn Deals abgeschlossen werden.
---
Deal-Daten automatisch an Ihr Rechnungsstellungssystem senden (Stripe, QuickBooks, Xero usw.) wenn eine Opportunity gewonnen wird.
## Workflow-Struktur
1. **Auslöser**: Datensatz wird aktualisiert (Opportunity)
2. **Filter**: Phase = Closed Won
3. **Datensatz suchen**: Unternehmensdetails abrufen
4. **Code** (optional): Nutzlast formatieren
5. **HTTP-Anfrage**: An Rechnungsstellungssystem senden
## Schritt 1: Auslöser einrichten
1. Erstellen Sie einen neuen Workflow
2. Wählen Sie den Auslöser **Datensatz wird aktualisiert** aus
3. Wählen Sie **Opportunity** als Objekt
## Schritt 2: Nach Closed Won filtern
Fügen Sie eine **Filter**-Aktion hinzu, damit nur fortgefahren wird, wenn der Deal gewonnen wurde:
| Einstellung | Wert |
| ------------- | ---------------------------------------- |
| **Feld** | Phase |
| **Bedingung** | Gleich |
| **Wert** | `CLOSED_WON` (oder der Name Ihrer Phase) |
<Tip>
Der Auslöser wird bei jeder Opportunity-Aktualisierung ausgelöst. Der Filter stellt sicher, dass der Workflow nur fortgesetzt wird, wenn die Phase auf Closed Won geändert wird.
</Tip>
## Schritt 3: Unternehmensdetails abrufen
Der Opportunity-Datensatz enthält möglicherweise nicht alle Unternehmensfelder, die Sie für die Rechnung benötigen. Fügen Sie eine **Datensatz suchen**-Aktion hinzu:
| Einstellung | Wert |
| --------------------- | -------------------------------------------- |
| **Objekt** | Unternehmen |
| **Abgleichen anhand** | ID entspricht `{{trigger.object.companyId}}` |
Dadurch wird der vollständige Unternehmensdatensatz mit Rechnungsadresse, Steuernummer usw. abgerufen.
## Schritt 4: Nutzlast formatieren (optional)
Wenn Ihr Rechnungsstellungssystem ein bestimmtes Format erwartet, fügen Sie eine **Code**-Aktion hinzu:
```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
}
}
};
};
```
## Schritt 5: An Rechnungsstellungssystem senden
Fügen Sie eine **HTTP-Anfrage**-Aktion hinzu:
| Einstellung | Wert |
| ----------- | ---------------------------------------------- |
| **Methode** | POST |
| **URL** | Ihr API-Endpunkt für die Rechnungsstellung |
| **Header** | `Authorization: Bearer YOUR_API_KEY` |
| **Inhalt** | `{{code.invoice}}` oder Felder direkt zuordnen |
### Beispiel: Stripe-Rechnung
```
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
```
### Beispiel: QuickBooks-Rechnung
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Vollständige Workflow-Zusammenfassung
| Schritt | Aktion | Zweck |
| ------- | -------------------------------- | ------------------------------------------------------------------ |
| 1 | Auslöser: Datensatz aktualisiert | Wird ausgelöst, wenn sich eine beliebige Opportunity ändert |
| 2 | Filter | Nur fortfahren, wenn Phase = Closed Won |
| 3 | Datensatz suchen | Vollständige Unternehmensdetails für die Rechnungsstellung abrufen |
| 4 | Code | Daten für die Rechnungsstellungs-API formatieren |
| 5 | HTTP-Anfrage | Rechnung im externen System erstellen |
## Tipps
* **Externe IDs speichern**: Speichern Sie die von der API zurückgegebene Rechnungs-ID mithilfe einer **Datensatz aktualisieren**-Aktion in der Opportunity
* **Fehlerbehandlung**: Fügen Sie einen Zweig hinzu, um eine Benachrichtigung zu senden, wenn die HTTP-Anfrage fehlschlägt
* **Zuerst testen**: Verwenden Sie den Sandbox-/Testmodus Ihres Rechnungsstellungssystems, bevor Sie live gehen
## Verwandt
* [Ein PDF aus Twenty generieren](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — generierte PDFs an Datensätze anhängen
* [Workflow-Trigger](/l/de/user-guide/workflows/capabilities/workflow-triggers)
* [Workflow-Aktionen](/l/de/user-guide/workflows/capabilities/workflow-actions)
* [Automatisierungen für Closed Won](/l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Webhook-Trigger einrichten
description: Empfangen Sie Daten von externen Diensten, um Workflows auszulösen.
---
Webhook-Trigger ermöglichen es externen Diensten, Ihre Workflows zu starten, indem sie Daten an eine eindeutige URL senden. Verwenden Sie sie, um Formulare, Drittanbieter-Apps und benutzerdefinierte Integrationen zu verbinden.
## Wann Sie Webhooks verwenden sollten
| Anwendungsfall | Beispiel |
| ------------------------------------ | ------------------------------------------------- |
| **Webformulare** | Einsendungen von Kontaktformularen erzeugen Leads |
| **Drittanbieter-Apps** | Stripe-Zahlung → Kundendatensatz erstellen |
| **Benutzerdefinierte Integrationen** | Ihre App → Twenty-Automatisierung |
| **No-Code-Tools** | Zapier-, Make- und n8n-Verbindungen |
## Schritt-für-Schritt-Einrichtung
### Schritt 1: Workflow erstellen
1. Gehen Sie zu **Einstellungen → Workflows**
2. Klicken Sie auf **+ Neuer Workflow**
3. Benennen Sie ihn (z. B. „Website-Formularübermittlung“)
### Schritt 2: Webhook-Trigger konfigurieren
1. Klicken Sie auf den Trigger-Block
2. Wählen Sie **Webhook** aus
3. Sie erhalten eine eindeutige Webhook-URL wie:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Kopieren Sie diese URL — Sie benötigen sie für Ihren externen Dienst
### Schritt 3: Erwartete Datenstruktur definieren
Bei **POST**-Anfragen die erwartete Body-Struktur definieren:
1. Klicken Sie auf **Erwarteten Body definieren**
2. Geben Sie ein Beispiel-JSON ein, das dem entspricht, was Ihr Dienst sendet:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Klicken Sie auf **Speichern** — dadurch werden Variablen erstellt, die Sie in den nächsten Schritten verwenden können.
### Schritt 4: Aktionen hinzufügen
Fügen Sie nun Aktionen hinzu, die die Webhook-Daten verwenden:
**Beispiel: Einen Personendatensatz erstellen**
1. Fügen Sie die Aktion **Datensatz erstellen** hinzu
2. Wählen Sie das Objekt **Personen** aus
3. Felder zuordnen:
| Feld | Wert |
| ----------- | ------------------------------------------------------------------ |
| Vorname | `{{trigger.body.firstName}}` |
| Nachname | `{{trigger.body.lastName}}` |
| E-Mail | `{{trigger.body.email}}` |
| Unternehmen | Suchen oder erstellen Sie basierend auf `{{trigger.body.company}}` |
### Schritt 5: Den Webhook testen
Testen Sie Ihren Webhook, bevor Sie ihn aktivieren:
**Mit 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"}'
```
**Mit Postman oder ähnlichen Tools**:
1. Erstellen Sie eine POST-Anfrage an Ihre Webhook-URL
2. Setzen Sie den Content-Type-Header auf `application/json`
3. Fügen Sie Ihren Test-JSON-Body hinzu
4. Senden und die Workflow-Ausführungen prüfen
### Schritt 6: Aktivieren
Nach dem Testen klicken Sie auf **Aktivieren**, um den Workflow live zu schalten.
## Umgang mit unterschiedlichen Datenstrukturen
### Verschachtelte Daten
Wenn Ihr Webhook verschachtelte Daten sendet:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Referenzieren mit: `{{trigger.body.contact.email}}`
### Arrays
Wenn die Daten Arrays enthalten:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
Wie Sie Arrays handhaben, hängt von Ihrem Anwendungsfall ab:
**Unbekannte Anzahl von Elementen → Iterator verwenden**
Wenn Sie jedes Element im Array verarbeiten müssen (z. B. für jedes einen Datensatz erstellen), fügen Sie eine **Code**-Aktion hinzu, um das Array zu parsen, und verwenden Sie anschließend **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Verwenden Sie dann Iterator, um zu iterieren: `{{code.items}}`
**Bekannte/spezifische Felder → In benannte Felder extrahieren**
Wenn das Array bestimmte Felder enthält, auf die Sie einzeln zugreifen möchten (z. B. Formularantworten, bei denen Position 0 immer „Vorname“ und Position 1 immer „Nachname“ ist), fügen Sie eine **Code**-Aktion hinzu, um sie zu extrahieren:
```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
}
};
};
```
Jetzt können Sie `product.name` und `product.qty` in den folgenden Schritten jeweils einzeln auswählen.
<Tip>
Weitere Details zum Umgang mit Arrays finden Sie unter [Arrays in Code-Aktionen verarbeiten](/l/de/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Verwandt
* [Workflow-Trigger](/l/de/user-guide/workflows/capabilities/workflow-triggers)
* [Workflow-Aktionen](/l/de/user-guide/workflows/capabilities/workflow-actions)