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: 将产品数据导入 Twenty
description: 按计划将数据仓库中的产品目录数据同步到您的 CRM。
---
使用此模式,使 Twenty 与您的数据仓库(例如 Snowflake、BigQuery、PostgreSQL)中的产品数据保持同步。
## 工作流结构
1. **触发器**:按计划
2. **Code**:查询您的数据仓库
3. **Code**(可选):将数据格式化为数组
4. **Iterator**:遍历每个产品
5. **Upsert Record**:在 Twenty 中创建或更新
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## 步骤 1:为触发器设定计划
将工作流设置为按满足数据新鲜度需求的频率运行:
* 每 5 分钟一次,用于近实时同步
* 每小时一次,适用于不太关键的数据
* 每天一次,适用于批量更新
## 步骤 2:查询您的数据仓库
添加一个 **Code** 操作以获取最新数据:
```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>
通过 `updated_at >= last X minutes` 进行筛选,仅检索最近更改的记录。 这样可以保持同步的高效性。
</Tip>
## 步骤 3:格式化数据(可选)
如果数据仓库返回的数据格式需要转换,请再添加一个 **Code** 操作。 常见的转换包括类型转换、字段重命名和数据清理。
### 示例:包含布尔值和状态字段的用户数据
```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),
})),
};
};
```
### 示例:产品数据的类型转换
```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
}))
};
};
```
### 示例:日期与货币格式化
```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, " ")
}))
};
};
```
### 常见转换
| 源格式 | 目标格式 | 代码 |
| -------------------- | ---------------- | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \\|\\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | ISO 日期 | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \\|\\| ""` |
## 步骤 4:遍历产品
添加 **Iterator** 操作:
* 输入:`{{code.products}}`
这将遍历数组中的每个产品。
## 步骤 5:插入或更新每条记录
在迭代器内,添加一个 **Upsert Record** 操作:
| 设置 | 值 |
| --------- | ------------------------- |
| **对象** | 您自定义的 Product 对象 |
| **匹配方式** | 外部 ID 或 SKU(唯一标识符) |
| **Name** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Price** | `{{iterator.item.price}}` |
<Tip>
使用 **Upsert**(更新或创建),而不是为“创建”和“更新”分别构建分支。 这样构建更快,也更易于调试。
</Tip>
## 示例用例
| 来源 | 数据 |
| ---------- | ----------- |
| **ERP 系统** | 产品目录、定价、库存 |
| **电商平台** | 订单、客户、产品更新 |
| **数据仓库** | 汇总指标、丰富后的数据 |
| **库存系统** | 库存水平、补货警报 |
## 相关内容
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [在 Code 操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: 将 Typeform 提交导入 Twenty
description: 处理 Typeform 的 webhook 负载,以从表单提交创建潜在客户。
---
关于标准 webhook 设置,请参见 [设置 Webhook 触发器](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)。 本文介绍针对 Typeform 自定义负载结构所需的特定处理。
### 步骤 1:创建 Webhook 工作流
1. 进入 **设置 → 工作流**
2. 点击 **+ 新建工作流**
3. 选择 **Webhook** 作为触发器
4. 复制 webhook URL
### 步骤 2:配置 Typeform
1. 在 Typeform 中,打开您的表单
2. 前往 **Connect → Webhooks**
3. 粘贴您的 Twenty webhook URL
4. 保存
### 步骤 3:了解 Typeform 负载
Typeform 会发送嵌套的 JSON 结构。 以下是一个简化示例:
```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" }
}
]
}
}
```
关键注意事项:
* 表单数据嵌套在 `form_response` 下
* **答案以数组形式返回**,而不是按命名字段返回
* 每个答案都包含字段类型和标题以供参考
### 步骤 4:从 answers 数组中提取字段
由于 `answers` 是一个数组,在后续步骤中,您只能选择整个数组,而不能选择单独的字段。 添加一个 **Code** 操作来提取您需要的字段:
```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
}
};
};
```
现在在后续步骤中,您可以在变量选择器中选择 `contact.firstName`、`contact.email` 等。
<Tip>
关于在 Code 操作中处理数组的更多详情,请参见[在 Code 操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)。
</Tip>
### 步骤 5:创建记录
添加 **创建记录** 操作:
| 字段 | 值 |
| -------- | ----------------------------------- |
| **对象** | 人员 |
| **名字** | `{{code.contact.firstName}}` |
| **姓氏** | `{{code.contact.lastName}}` |
| **电子邮件** | `{{code.contact.email}}` |
| **公司** | 基于 `{{code.contact.company}}` 搜索或创建 |
### 步骤 6:测试并启用
1. 在 Typeform 中提交测试响应
2. 检查工作流运行以验证已捕获数据
3. 激活工作流
## 相关内容
* [设置 Webhook 触发器](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [在 Code 操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: 在 Twenty 中生成 PDF
description: 创建一个工作流,用于生成 PDF(例如报价单)并将其附加到记录。
---
自动生成或获取 PDF,并将其附加到 Twenty 中的记录。 这通常用于创建与公司、商机或其他对象关联的报价单、发票或报告。
## 概览
此工作流使用 **手动触发器**,因此用户可以按需为任何选定的记录生成 PDF。 **逻辑函数** 句柄:
1. 从 URL(来自 PDF 生成服务)下载 PDF
2. 将文件上传到 Twenty
3. 创建与该记录关联的附件
## 先决条件
设置工作流之前:
1. **创建 API 密钥**:前往 **设置 → APIs** 并创建一个新的 API 密钥。 逻辑函数需要这个令牌。
2. **设置 PDF 生成服务**(可选):如果您想动态生成 PDF(例如报价单),可使用 Carbone、PDFMonkey 或 DocuSeal 等服务来创建 PDF 并获取下载 URL。
## 分步设置
### 步骤 1:配置触发器
1. 转到 **工作流** 并创建一个新工作流
2. 选择 **手动触发器**
3. 选择要将 PDF 附加到的对象(例如,**公司** 或 **商机**)
<Tip>
手动触发, 用户可以使用右上角显示的按钮来运行此工作流——这将运行工作流来生成并附加一个 PDF
</Tip>
### 步骤 2: 添加逻辑函数
1. 添加 **代码** 动作(逻辑函数)
2. 使用下面的代码创建一个新函数
3. 配置输入参数
#### 输入参数
| 参数 | 值 |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
如果附加到一个不同的对象(人员、机会等),则相应重命名参数(如:`personId`、`opportunityId`),并更新逻辑函数。
</Note>
#### 逻辑函数代码
```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;
};
```
### 步骤 3:根据您的用例自定义
#### 要附加到其他对象
将 `targetCompanyId` 替换为相应的字段:
| 对象 | 字段名称 |
| ----- | -------------------------- |
| 公司 | `targetCompanyId` |
| 人员 | `targetPersonId` |
| 机会 | `targetOpportunityId` |
| 自定义对象 | `targetYourCustomObjectId` |
同时更新函数参数以及附件的 mutation 中的 `variables.data` 对象。
#### 要使用动态 PDF URL
如果使用 PDF 生成服务,您可以:
1. 首先创建一个 HTTP 请求操作以生成 PDF
2. 将返回的 PDF URL 传递到逻辑函数作为参数
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### 步骤 4:测试并启用
1. 保存工作流
2. 导航到一条公司记录
3. 单击 **⋮** 菜单并选择您的工作流
4. 在记录上的 **附件** 部分检查并确认 PDF 已被附加
5. 激活工作流
## 与 PDF 生成服务结合使用
用于创建动态报价单或发票:
### 示例:生成报价单 → 附加 PDF
| 步骤 | 操作 | 目的 |
| -- | --------- | ------------------- |
| 1 | 手动触发器(公司) | 用户在记录上发起 |
| 2 | 搜索记录 | 获取商机或行项目详情 |
| 3 | HTTP请求 | 使用记录数据调用 PDF 生成 API |
| 4 | 逻辑函数 | 下载并附加生成的 PDF |
### 常用的 PDF 生成服务
* **Carbone** - 基于模板的文档生成
* **PDFMonkey** - 基于模板的动态 PDF 创建
* **DocuSeal** - 文档自动化平台
* **Documint** - API 优先的文档生成
每个服务提供一个 API 返回一个 PDF URL,然后你可以传递到逻辑函数。
## 故障排除
| 问题 | 解决方案 |
| ---------- | -------------------------- |
| "无法下载 PDF" | 检查 PDF URL 是否可访问并返回有效的 PDF |
| "上传失败" | 验证您的 API 密钥有效且具有写入权限 |
| "附件创建失败" | 确保对象 ID 字段名称与您的目标对象匹配 |
## 相关内容
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [逻辑函数](/l/zh/user-guide/workflows/capabilities/workflow-actions#code)
* [从 Twenty 生成报价单或发票](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: 从 Twenty 生成报价单或发票
description: 在交易达成时,自动在外部工具中创建发票。
---
自动将交易数据发送到您的开票系统(Stripe、QuickBooks、Xero 等) 当机会赢单时。
## 工作流结构
1. **触发器**:记录已更新(机会对象)
2. **筛选器**:阶段 = 赢单
3. **搜索记录**:获取公司详细信息
4. **代码**(可选):格式化负载
5. **HTTP 请求**:发送到开票系统
## 步骤 1:设置触发器
1. 创建新的工作流
2. 选择 **记录已更新** 触发器
3. 选择 **机会** 作为对象
## 步骤 2:筛选赢单
添加一个 **筛选** 操作,仅在机会赢单时才继续:
| 设置 | 值 |
| ------ | --------------------- |
| **字段** | 阶段 |
| **条件** | 等于 |
| **值** | `CLOSED_WON`(或您的阶段名称) |
<Tip>
该触发器会在任何机会更新时触发。 筛选器确保仅当阶段变更为赢单时,工作流才会继续。
</Tip>
## 步骤 3:获取公司详细信息
机会记录可能不包含开票所需的所有公司字段。 添加一个 **搜索记录** 操作:
| 设置 | 值 |
| -------- | ------------------------------------ |
| **对象** | 公司 |
| **匹配方式** | ID 等于 `{{trigger.object.companyId}}` |
这将检索包含账单地址、税号等在内的完整公司记录。
## 步骤 4:格式化负载(可选)
如果您的开票系统需要特定格式,请添加一个 **代码** 操作:
```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
}
}
};
};
```
## 步骤 5:发送到开票系统
添加一个 **HTTP 请求** 操作:
| 设置 | 值 |
| ------- | ------------------------------------ |
| **方法** | POST |
| **URL** | 您的开票 API 端点 |
| **请求头** | `Authorization: Bearer YOUR_API_KEY` |
| **请求体** | `{{code.invoice}}` 或直接映射字段 |
### 示例: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
```
### 示例:QuickBooks 发票
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## 完整的工作流摘要
| 步骤 | 操作 | 目的 |
| -- | --------- | --------------- |
| 1 | 触发器:记录已更新 | 当任何机会发生更改时触发 |
| 2 | 过滤 | 仅当阶段 = 赢单时才继续 |
| 3 | 搜索记录 | 获取用于开票的完整公司详细信息 |
| 4 | 代码 | 为开票 API 格式化数据 |
| 5 | HTTP请求 | 在外部系统中创建发票 |
## 提示
* **存储外部 ID**:使用 **更新记录** 操作将 API 返回的发票 ID 保存回机会
* **错误处理**:添加一个分支,在 HTTP 请求失败时发送通知
* **先测试**:在上线之前,使用开票系统的沙盒/测试模式
## 相关内容
* [从 Twenty 生成 PDF](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — 将生成的 PDF 附加到记录
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [赢单自动化](/l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: 设置 Webhook 触发器
description: 从外部服务接收数据以触发工作流。
---
Webhook 触发器允许外部服务通过向唯一的 URL 发送数据来启动您的工作流。 使用它们连接表单、第三方应用和自定义集成。
## 何时使用 Webhook
| 用例 | 示例 |
| --------- | ------------------ |
| **网页表单** | 联系表单提交会创建线索 |
| **第三方应用** | Stripe 支付 → 创建客户记录 |
| **自定义集成** | 您的应用 → Twenty 自动化 |
| **无代码工具** | Zapier、Make、n8n 连接 |
## 分步设置
### 步骤 1:创建工作流
1. 进入 **设置 → 工作流**
2. 点击 **+ 新建工作流**
3. 为其命名(例如,“网站表单提交”)
### 步骤 2:配置 Webhook 触发器
1. 单击触发器块
2. 选择 **Webhook**
3. 您将收到一个唯一的 Webhook URL,例如:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. 复制此 URL——您需要在外部服务中使用它
### 步骤 3:定义预期的数据结构
对于 **POST** 请求,定义预期的请求体结构:
1. 点击 **定义预期请求体**
2. 输入与您的服务将发送内容匹配的示例 JSON:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. 点击 **保存**——这会创建可在后续步骤中使用的变量
### 步骤 4:添加操作
现在添加使用 Webhook 数据的操作:
**示例:创建一条 Person 记录**
1. 添加 **创建记录** 操作
2. 选择 **People** 对象
3. 映射字段:
| 字段 | 值 |
| ---- | ----------------------------------- |
| 名字 | `{{trigger.body.firstName}}` |
| 姓氏 | `{{trigger.body.lastName}}` |
| 电子邮件 | `{{trigger.body.email}}` |
| 公司 | 基于 `{{trigger.body.company}}` 搜索或创建 |
### 步骤 5:测试 Webhook
在激活之前,测试您的 Webhook:
**使用 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"}'
```
**使用 Postman 或类似工具**:
1. 向您的 Webhook URL 发起一个 POST 请求
2. 将 Content-Type 头设置为 `application/json`
3. 添加您的测试 JSON 请求体
4. 发送并检查工作流运行情况
### 步骤 6:启用
测试完成后,点击 **启用** 使该工作流生效。
## 处理不同的数据结构
### 嵌套数据
如果您的 Webhook 发送的是嵌套数据:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
通过以下方式引用:`{{trigger.body.contact.email}}`
### 数组
如果数据包含数组:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
如何处理数组取决于您的使用场景:
**项目数量未知 → 使用 Iterator**
如果需要处理数组中的每个项目(例如为每个项目创建一条记录),请添加一个 **Code** 操作来解析数组,然后使用 **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
然后使用 Iterator 进行循环:`{{code.items}}`
**已知/特定字段 → 提取到具名字段**
如果数组包含您想单独访问的特定字段(例如表单答案,其中位置 0 始终为“名”,位置 1 始终为“姓”),请添加一个 **Code** 操作来提取它们:
```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
}
};
};
```
现在您可以在后续步骤中分别选择 `product.name` 和 `product.qty`。
<Tip>
关于处理数组的更多详情,请参见[在代码操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)。
</Tip>
## 相关内容
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)