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 2s

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,82 @@
---
title: 代码动作中处理数组
description: 学习如何在工作流代码操作中正确处理数组输入。
---
当在代码操作中与数组一起工作时,您可能会遇到两个共同的挑战:
1. **作为字符串传递的数组** - 来自外部系统或前几步的数据作为字符串而不是实际数组
2. **不能选择单个项目** — 您只能选择整个数组,而不是其中的特定字段
两者都可以用代码节点解决。
## 从字符串解析数组
数组通常是作为字符串或 JSON 而不是本机数组在工作流步骤之间传递的。 这种情况发生在:
* 通过 HTTP 请求从外部 API 接收数据
* 正在处理 webhook payload
* 工作流步骤之间传输数据
**Solution**:在代码开始时添加此模式:
```javascript
export const main = async (params: {
users: any;
}): Promise<object> => {
const { users } = params;
// Handle input that may come as a string or an array
const usersFormatted = typeof users === "string" ? JSON.parse(users) : users;
// Now you can safely work with usersFormatted as an array
return {
users: usersFormatted.map((user) => ({
...user,
activityStatus: String(user.activityStatus).toUpperCase(),
})),
};
};
```
键行 `typeof users === "string" ? JSONparse(users) : users`检查输入是否是字符串, 如果需要解析它, 或者如果它已经是一个数组直接使用它。
## 从数组中提取个别字段
Web 钩子可能返回一个数组,如'答案: [...]', 但在随后的工作流步骤中,您只能选择 **整个数组** ——而不是其中的个别项目。
**Solution**: 添加代码节点来提取特定字段并将其作为结构化对象返回:
```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 specific fields from the array
const firstname = answersFormatted[0]?.text || "";
const name = answersFormatted[1]?.text || "";
return {
answer: {
firstname,
name
}
};
};
```
代码节点返回结构化对象而不是数组。 在随后的步骤中,您现在可以从变量选择器中选择单个字段,例如`answer.firstname` 和 `answer.name` 。
<Tip>
**想要遍历数组而不是提取字段?** 当一个 Code 或 Logic Function 步骤返回顶层数组时,你可以将其直接传入 [Iterator](/l/zh/user-guide/workflows/capabilities/use-iterator):在 Iterator 的输入中选择该步骤的 **Whole list** 选项,然后在循环内部使用 `{{iterator.currentItem}}` 引用每个元素。 在那种情况下,你不需要将数组重构为对象。
</Tip>
<Tip>
点击代码编辑器右上角的方形图标在全屏显示它——因为默认编辑器宽度是有限的,所以它是有用的。
</Tip>

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)

View File

@@ -0,0 +1,121 @@
---
title: 入站邮件自动回复
description: 构建一个使用 AI 对入站邮件进行分拣,并自动以线程形式发送回复的工作流。
---
在数秒内回复入站邮件—而不是数小时。 此工作流使用 AI 智能体筛除噪音(新闻通讯、垃圾邮件、自动回复),并为真实邮件起草个性化回复。 然后,AI 会在原始会话中以线程形式发送该回复。
## 电子邮件线程如何运作
每封电子邮件都包含一个隐藏的 `Message-ID` 头部—由发件人的邮件服务器分配的唯一指纹。 当您回复一封电子邮件时,您的邮件客户端会设置一个 `In-Reply-To` 标头以引用该指纹。 这就是 Gmail、Outlook 以及其他所有客户端将邮件分组为会话的方式。
在 Twenty 中,该指纹作为 `headerMessageId` 存储在 Message 对象中。 您的工作流会获取它,并将其传递到 Send Email 操作的 In-Reply-To 字段。
## 构建工作流
### 步骤 1:创建新工作流
前往 **Settings -> Workflows** 并单击 **+ New Workflow**。
### 步骤 2:在收到邮件时触发
选择 **When a Record is Created**,然后选择 **Messages**。
每当有邮件进入 Twenty 时,这一步就会触发。
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
### 步骤 3:查找发件人
添加 **搜索记录** 操作.
发件人的地址不在消息本身上——而是在关联的 Message Participant 记录中。
| 字段 | 值 |
| ------- | ------------------------------ |
| **对象** | 消息参与者 |
| **筛选器** | Message **为** `{{trigger.id}}` |
| **筛选器** | Role **为** From |
| **限制** | 1 |
这会在 `handle` 中提供发件人的邮箱地址,在 `displayName` 中提供其姓名。
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
### 步骤 4:使用 AI 分流并起草回复
添加一个 **AI Agent** 操作。 此单一步骤完成两件事:决定该邮件是否需要回复;如果需要,则撰写一封回复。
使用类似如下的提示词:
```
You are an email triage assistant for a sales team. Read the following
inbound email and decide if it deserves a reply.
Subject: {{trigger.subject}}
Body: {{trigger.text}}
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
If this email is spam, a newsletter, an automated notification, or
otherwise does not need a human reply, respond with exactly: SKIP
Otherwise, write a short, professional reply (3-4 sentences max) that:
- Acknowledges their specific message
- Lets them know someone from the team will follow up shortly
- Is warm but not overly casual
Respond with only the reply text, no subject line or greeting prefix.
```
AI Agent 会将其响应输出到 `response` 字段,后续步骤可以引用该字段。
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
### 步骤 5:根据 AI 的决策进行分支
添加一个 **If/Else** 操作,用于检查 AI 决定是回复还是跳过。
| 字段 | 值 |
| ------- | ---------------------------------- |
| **条件** | AI Agent `response` **不包含** `SKIP` |
| **若为真** | 继续执行 Send Email |
| **否则** | 不执行任何操作(工作流结束) |
垃圾邮件、新闻简报以及自动生成的邮件会被丢弃。 其余内容将进入下一步。
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### 步骤 6:发送会话内回复
在 "若为真" 分支上添加一个 **Send Email** 操作。 单击 **Advanced options**,然后选择 **Add In-Reply-To**。
| 字段 | 值 |
| --------------- | -------------------------------------- |
| **收件人** | `{{Find Sender.first.handle}}` |
| **主题** | `Re: {{trigger.subject}}` |
| **正文** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
正是 In-Reply-To 字段使其成为一次回复,而不是一段新的会话。 收件人在 Gmail、Outlook 或任何其他客户端中,会看到它被归入原始邮件下的同一会话。
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
<Tip>
**In-Reply-To** 需要来自触发器的 `message.headerMessageId`——它是该邮件的唯一指纹,而不是收件人地址。 如果将其留空,邮件仍会发送,只是作为一封独立的消息。
</Tip>
<Warning>
Gmail 使用主题行将邮件归入同一会话。 要让 Gmail 在原始会话中显示回复,主题行**必须**以 `Re:` 开头(包括冒号和空格)。 否则,即使正确设置了 In-Reply-To 标头,回复也会显示为一个单独的会话。
</Warning>
### 步骤 7:测试并启用
点击 **Test**,然后检查您的邮件客户端。 回复应当嵌套显示在原始邮件下方。
确认无误后启用。
## 进阶构建思路
* **仅回复 VIP** — 添加一个分支,用于检查发件人的域名,或他们是否在 Twenty 中作为联系人存在
* **按意图路由** — 使用不同的 AI Agent 提示词,将销售咨询与支持请求区分处理
* **回复前先丰富信息** — 添加一个 Search Records 步骤,将发件人的公司或交易历史拉入 AI 提示词中,以获得更个性化的回复

View File

@@ -0,0 +1,179 @@
---
title: 赢单自动化
description: 当机会成交后,自动化处理赢单后的活动。
---
当交易成交时,需要执行多项操作:更新公司状态、通知团队成员、创建客户入门任务。 使用一个工作流将这一切自动化。
## 问题
当一个机会移动到 "Closed Won":
* 公司类型需要从 "Prospect" 变更为 "Customer"
* 需要创建客户入门任务
* 需要通知客户成功团队
* 销售代表需要收到确认
手动执行这些操作既耗时又容易出错。
## 解决方案
创建一个工作流,自动处理所有赢单后的活动。
## 完整的工作流设置
### 步骤 1:创建工作流
1. 进入 **设置 → 工作流**
2. 点击 **+ 新建工作流**
3. 将其命名为 "Deal Won - Post-Win Automation"
### 步骤 2:配置触发器
1. 选择 **记录已更新**
2. 选择 **机会**
3. 在 "Fields to monitor" 下,选择 **Stage**
### 步骤 3:添加阶段过滤器
1. 添加 **过滤** 操作
2. 条件:`{{trigger.object.stage}}` 等于 "Closed Won"
### 步骤 4:更新公司类型
1. 添加 **更新记录** 操作
2. 配置:
| 字段 | 值 |
| ---------- | ------------------------------- |
| **对象** | 公司 |
| **记录** | `{{trigger.object.company.id}}` |
| **类型** | 客户 |
| **首次交易日期** | `{{trigger.object.closedAt}}` |
| **账户所有者** | `{{trigger.object.owner.id}}` |
### 步骤 5:创建客户入门任务
1. 添加 **创建记录** 操作
2. 配置:
| 字段 | 值 |
| -------- | -------------------------------------------------------------------------- |
| **对象** | 任务 |
| **标题** | `Onboarding: {{trigger.object.name}}` |
| **受托人** | 客户成功团队成员 |
| **到期日** | 3 天后 |
| **优先级** | 高 |
| **关联公司** | `{{trigger.object.company.id}}` |
| **关联机会** | `{{trigger.object.id}}` |
| **描述** | `为 {{trigger.object.company.name}} 的新客户入门。 交易金额:{{trigger.object.amount}}` |
### 步骤 6:通知客户成功团队
1. 添加 **Send Email** 操作
2. 配置:
| 字段 | 值 |
| ------- | -------------------------------------------------- |
| **收件人** | customer-success@yourcompany.com |
| **主题** | `🎉 New Customer: {{trigger.object.company.name}}` |
| **正文** | 请参见下方示例 |
**电子邮件正文示例**:
```
Hi CS Team,
We have a new customer!
Company: {{trigger.object.company.name}}
Deal: {{trigger.object.name}}
Value: {{trigger.object.amount}}
Sales Rep: {{trigger.object.owner.name}}
Close Date: {{trigger.object.closedAt}}
An onboarding task has been created automatically.
Let's give them a great start!
```
### 步骤 7:向销售代表发送确认
1. 再添加一个 **Send Email** 操作
2. 配置:
| 字段 | 值 |
| ------- | ---------------------------------------- |
| **收件人** | `{{trigger.object.owner.email}}` |
| **主题** | `✅ Deal Closed: {{trigger.object.name}}` |
| **正文** | 恭喜! 您的交易已处理。 客户成功团队已被通知,入门流程已开始。 |
### 步骤 8:测试并启用
1. 通过将测试机会移动到 "Closed Won" 进行测试
2. 验证:
* 公司类型已更改为 "Customer"
* 已创建客户入门任务
* 客户成功团队已收到电子邮件
* 销售代表已收到确认
3. 准备就绪后启用
## 处理 Closed Lost
为失单创建一个类似的工作流:
### 触发器
* 记录已更新(Opportunities,Stage = "Closed Lost")
### 操作
1. **创建记录**:用于“失单分析”的任务
2. **更新记录**:将失单原因添加到公司记录中
3. **Send Email**:通知经理失单情况
## 高级:多步骤客户入门
对于复杂的入门流程,创建多个任务:
```javascript
export const main = async (params) => {
const tasks = [
{ title: "Welcome call", daysFromNow: 1, assignee: "CS" },
{ title: "Send onboarding materials", daysFromNow: 2, assignee: "CS" },
{ title: "Technical setup", daysFromNow: 5, assignee: "Support" },
{ title: "30-day check-in", daysFromNow: 30, assignee: "CS" }
];
return { tasks };
};
```
使用 **Iterator** 根据数组创建每个任务。
## 自定义思路
### 使其他工具保持最新状态
* 使用 **HTTP 请求** 在计费系统中创建客户
### 条件操作
使用 **过滤** 操作以:
* 针对企业与中小企业(SMB)采用不同的入门流程
* 根据地区指定不同的负责人
* 对小额交易跳过通知
### 包含交易详情
使用 **Code** 操作来格式化:
* 交易摘要文档
* 面向客户成功团队的交接备注
* 自定义入门检查清单
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [从工作流发送电子邮件](/l/zh/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,136 @@
---
title: 检测停滞商机
description: 当商机未更新时自动通知经理。
---
通过在商机变得停滞时提醒经理,保持销售管道健康。 此工作流会检查在指定天数内未更新的商机。
## 问题
长期未更新的商机会导致:
* 交易变冷
* 不可靠的预测
* 收入流失
## 解决方案
创建一个定时工作流,用于查找停滞商机并向其经理发送电子邮件。
## 分步设置
### 步骤 1:创建工作流
1. 进入 **设置 → 工作流**
2. 点击 **+ 新建工作流**
3. 命名为 "停滞商机警报"
### 步骤 2:配置触发器
1. 选择 **On a Schedule**
2. 设置为每日运行(例如,每天上午 8 点)
### 步骤 3:搜索停滞商机
1. 添加 **搜索记录** 操作
2. 配置:
| 字段 | 值 |
| ------ | ----------------------------------- |
| **对象** | 机会 |
| **过滤** | 更新时间早于(今天 - 7 天) |
| **过滤** | 阶段不为 "Closed Won" 且不为 "Closed Lost" |
| **限制** | 100 |
### 步骤 4:检查是否有结果
1. 添加 **过滤** 操作
2. 条件:`{{searchRecords.length}}` 大于 0
3. 如果没有停滞商机,工作流在此停止
### 步骤 5:格式化警报(代码操作)
添加一个 **Code** 操作来格式化邮件:
```javascript
export const main = async (params) => {
const opportunities = params.opportunities;
// Group opportunities by owner
const byOwner = {};
opportunities.forEach(opp => {
const ownerEmail = opp.owner?.email || 'unassigned';
if (!byOwner[ownerEmail]) {
byOwner[ownerEmail] = [];
}
byOwner[ownerEmail].push({
name: opp.name,
amount: opp.amount,
lastUpdated: opp.updatedAt,
stage: opp.stage
});
});
// Format summary for manager
let summary = "Stale Opportunities Report\n\n";
Object.entries(byOwner).forEach(([owner, opps]) => {
summary += `${owner}: ${opps.length} stale opportunities\n`;
opps.forEach(opp => {
summary += ` - ${opp.name} (${opp.stage})\n`;
});
summary += "\n";
});
return {
summary,
totalCount: opportunities.length
};
};
```
### 步骤 6:发送警报邮件
添加 **Send Email** 操作:
| 字段 | 值 |
| ------- | ---------------------------------- |
| **收件人** | sales-manager@yourcompany.com |
| **主题** | `🚨 {{code.totalCount}} 个停滞商机需要关注` |
| **正文** | `{{code.summary}}` |
### 步骤 7:测试并启用
1. 点击 **Test** 以运行工作流
2. 检查邮件是否包含正确的数据
3. 准备就绪后启用
## 自定义选项
### 更改停滞阈值
修改“搜索记录”的过滤条件,将 7 天更改为你偏好的周期:
* 高节奏销售:3 天
* 企业级交易:14 天
* 长销售周期:30 天
### 单独提醒销售代表
使用 **Iterator** 向每位销售代表发送关于其自身停滞交易的个性化邮件,而不是只发一封经理邮件。
### 添加升级机制
创建多个工作流,并逐步提高严重性:
1. 第 7 天:发送电子邮件给销售代表
2. 第 14 天:发送电子邮件给销售代表和经理
3. 第 21 天:为经理创建任务以便介入
### 集成 Slack
使用 **HTTP Request** 向 Slack 的 webhook 发送消息,替代或补充电子邮件。
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [从工作流发送电子邮件](/l/zh/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,69 @@
---
title: 显示收到的电子邮件数量
description: 创建一个工作流,自动统计并显示从每个联系人收到的电子邮件数量。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/workflow-number-of-emails-received.png" style={{width:'100%'}} />
## 概览
此工作流在每次收到新电子邮件时触发,并将该发件人发送的电子邮件总数更新到人员记录上的自定义字段。
## 先决条件
在设置此工作流之前,请在 **人员** 对象上创建一个自定义字段:
1. 进入 **设置 → 数据模型 → 人员**
2. 添加一个新的 **数字** 字段
3. 将其命名为 "从此人收到的电子邮件数量"
## 分步设置
<VimeoEmbed videoId="1148700336" title="视频演示" />
### 步骤 1:配置触发器
1. 转到 **工作流** 并创建一个新工作流
2. 选择 **记录已创建** 作为触发器
3. 选择 **消息参与者**(位于高级对象下)
<Note>
消息参与者是消息 ID 与人员 ID 的组合,每条消息对应一个唯一记录。 这比直接在消息上跟踪更容易,因为我们可以访问 `handle` 字段,其中包含发件人(或收件人)的电子邮件地址。
</Note>
### 步骤 2:按角色筛选
1. 添加 **过滤** 操作
2. 设置条件:**角色** 等于 **FROM**
这可确保你只统计此人发送的消息,而不是发送给他们的消息。
### 步骤 3:搜索 handle 相同的所有消息参与者
1. 添加 **搜索记录** 操作
2. 选择 **消息参与者** 作为对象
3. 添加筛选条件:**Handle** 等于触发器中的 handle(发件人的电子邮件地址),并且 **Role** 等于 **FROM**
4. 将 **Limit** 从 1 提高到 **200**(最大值)
这样可以找到来自该电子邮件地址的所有消息,以获取总计数。
<Note>
“搜索记录”操作最多只能返回 200 条记录。 但是,由于你只使用 `totalCount` 值(而不是单条记录),此步骤将返回该人发送的电子邮件总数。
</Note>
### 步骤 4:使用创建或更新记录操作更新人员记录
1. 添加 **创建或更新记录** 操作
2. 选择 **人员** 作为对象
3. 通过将其电子邮件与消息参与者中的 `handle` 匹配来找到该人员。
4. 将自定义的 "收到的电子邮件数量" 字段设置为 `{{searchRecords.totalCount}}`
“搜索记录”操作中的 `totalCount` 值表示从该人员收到的电子邮件总数。
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [创建自定义字段](/l/zh/user-guide/data-model/how-tos/customize-your-data-model)
* [搜索记录操作](/l/zh/user-guide/workflows/capabilities/workflow-actions#search-records)

View File

@@ -0,0 +1,170 @@
---
title: 显示关联记录数据
description: 使用工作流显示关联记录中的数据(例如,在机会中显示公司的信息)。
---
将关联记录的数据直接显示在你的记录上——例如,将公司的员工数量显示在其机会上。 在原生支持嵌套字段之前,此工作流变通方案很有用。
## 常见使用案例
| 来源 | 目标 | 要复制的字段 |
| -- | -- | ----------------- |
| 公司 | 机会 | 行业、公司规模、ARR |
| 人员 | 机会 | 电子邮件、电话、职位 |
| 机会 | 公司 | 最后一次成交金额、最后一次赢单日期 |
## 基础字段复制
### 示例:将联系人电子邮件复制到机会
**目标**:在机会上设置联系人时,将其电子邮件复制到该机会以便于访问。
### 先决条件
在构建工作流之前,先在 **设置 → 数据模型 → 机会** 中创建目标字段:
* 联系人邮箱(类型:Email)
* 联系人电话(类型:Phone)
### 设置
1. **触发器**:记录已更新(机会,联系人字段)
2. **筛选条件**:检查联系人不为空
3. **搜索记录**:查找关联的人员
* 对象:People
* 筛选条件:ID 等于 `{{trigger.object.pointOfContact.id}}`
4. **更新记录**:
* 对象:机会
* 记录:`{{trigger.object.id}}`
* 联系人邮箱:`{{searchRecords[0].email}}`
* 联系人电话:`{{searchRecords[0].phone}}`
## 复制多个字段
### 示例:将公司信息同步到所有相关机会
**目标**:当公司详细信息发生变化时,更新所有相关的机会。
### 设置
1. **触发器**:记录已更新(公司)
* 字段:行业、公司规模、年度收入
2. **搜索记录**:查找该公司的所有机会
* 对象:机会
* 筛选条件:公司 ID 等于 `{{trigger.object.id}}`
3. **Iterator**:遍历每个机会
4. **更新记录**(在迭代器内):
* 对象:机会
* 记录:`{{iterator.currentItem.id}}`
* 公司行业:`{{trigger.object.industry}}`
* 公司规模:`{{trigger.object.companySize}}`
* 公司 ARR:`{{trigger.object.annualRevenue}}`
## 在创建记录时复制
### 示例:用公司数据预填机会
**目标**:当创建与某公司关联的机会时,自动复制关键的公司信息。
### 先决条件
在 **设置 → 数据模型 → 机会** 中创建目标字段:
* 公司行业(类型:Text)
* 公司规模(类型:Number)
### 设置
1. **触发器**:记录已创建(机会)
* 筛选条件:公司不为空
2. **搜索记录**:获取关联公司的详细信息
* 对象:公司
* 筛选条件:ID 等于 `{{trigger.object.company.id}}`
3. **更新记录**:
* 对象:机会
* 记录:`{{trigger.object.id}}`
* 公司行业:`{{searchRecords[0].industry}}`
* 公司规模:`{{searchRecords[0].employees}}`
<Note>
**任务和备注限制**:任务和备注上的关系被硬编码为多对多,目前尚不可用于工作流触发器或操作。 要访问这些关系,请改用 [API](/l/zh/developers/extend/api)。
</Note>
## 双向同步
### 示例:保持主要联系人同步
**目标**:当公司的主要联系人发生变化时,更新该联系人。 当某人被设为主要联系人时,更新公司。
### 工作流 1:公司 → 人员
1. **触发器**:记录已更新(公司,主要联系人字段)
2. **更新记录**:将该人员的“是否为主要联系人”设为 true
3. **搜索记录**:查找之前的主要联系人
4. **更新记录**:将先前联系人的“是否为主要联系人”设为 false
### 工作流 2:人员 → 公司
1. **触发器**:记录已更新(人员,是否为主要联系人 = true)
2. **更新记录**:将公司的主要联系人设置为此人
<Note>
在进行双向同步时要小心,避免无限循环。 在更新前使用筛选条件检查值是否实际发生变化。
</Note>
## 使用代码进行复杂映射
### 示例:在复制过程中转换数据
**目标**:将人员的电话号码复制到机会并进行格式化。
```javascript
export const main = async (params) => {
const { phone } = params;
if (!phone) return { formattedPhone: null };
// Remove non-numeric characters
const digits = phone.replace(/\D/g, '');
// Format as (XXX) XXX-XXXX
const formatted = digits.length === 10
? `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`
: phone;
return { formattedPhone: formatted };
};
```
## 最佳实践
### 避免循环
* 不要创建相互无限触发的工作流
* 使用特定的字段条件
* 添加检查以确定值是否真的发生变化
### 处理缺失数据
* 在复制之前始终检查源记录是否存在
* 为可选字段提供默认值
* 当源字段为空时使用筛选条件跳过
### 性能
* 在复制到大量记录时进行批量更新
* 对于批量同步操作,使用计划的工作流
* 对于多条记录的更新,考虑使用 Iterator
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)

View File

@@ -0,0 +1,202 @@
---
title: 公式字段
description: 在原生支持可用之前,使用工作流创建公式字段。
---
Twenty 尚未支持原生公式字段(预计 2026 年推出),但你可以通过工作流实现相同的效果。 此变通方案可让你自动计算并填充字段值——从简单的拼接到复杂的业务逻辑。
## 常见使用案例
| 用例 | 公式示例 |
| ----------- | ----------- |
| **全名** | 名 + " " + 姓 |
| **预期金额** | 金额 × 概率 |
| **距离到期的天数** | 到期日期 - 今天 |
| **阶段停留天数** | 今天 - 阶段进入日期 |
| **线索评分** | 根据多个条件计分 |
<Tip>
有关在流水线阶段跟踪时间的完整示例,请参阅 [跟踪机会在每个阶段停留时长](/l/zh/user-guide/views-pipelines/how-tos/track-time-in-stage)。
</Tip>
## 基础公式:拼接
### 示例:自动填充全名
**目标**:将名字和姓氏自动合并为一个全名字段。
### 设置
1. **触发器**:记录被更新或创建(人员)
2. **过滤器**:检查名字或姓氏是否发生变化
3. **代码操作**:
```javascript
export const main = async (params) => {
const { firstName, lastName } = params;
const fullName = [firstName, lastName]
.filter(Boolean)
.join(' ');
return { fullName };
};
```
4. **更新记录**:将全名设置为 `{{code.fullName}}`
## 数值公式:预期金额
### 示例:计算预期收入
**目标**:用机会金额乘以概率以得到预期金额。
完整工作流请参阅 [如何在流水线中显示预期金额](/l/zh/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)。
### 设置
1. **触发器**:记录被更新(机会,金额或概率字段)
2. **代码操作**:
```javascript
export const main = async (params) => {
const { amount, probability } = params;
const expectedAmount = (amount || 0) * (probability || 0) / 100;
return { expectedAmount };
};
```
3. **更新记录**:将预期金额设置为 `{{code.expectedAmount}}`
## 日期公式:天数计算
### 示例:距离任务到期的天数
**目标**:计算距任务到期日期还剩多少天。
### 设置
1. **触发器**:记录被更新或创建(任务,到期日期字段)
2. **代码操作**:
```javascript
export const main = async (params) => {
const { dueDate } = params;
if (!dueDate) {
return { daysUntilDue: null };
}
const due = new Date(dueDate);
const today = new Date();
const diffTime = due - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return { daysUntilDue: diffDays };
};
```
3. **更新记录**:将距离到期的天数设置为 `{{code.daysUntilDue}}`
<Note>
负值表示任务已逾期。 你可以使用此字段按紧急程度筛选或排序任务。
</Note>
## 条件公式:线索评分
### 示例:根据条件计算线索评分
**目标**:根据公司规模、行业和互动情况对线索打分。
### 设置
1. **触发器**:记录被更新(人员或公司)
2. **代码操作**:
```javascript
export const main = async (params) => {
const { companySize, industry, hasEmail, hasPhone, source } = params;
let score = 0;
// Company size scoring
if (companySize === 'Enterprise') score += 30;
else if (companySize === 'Mid-Market') score += 20;
else if (companySize === 'SMB') score += 10;
// Industry scoring
const targetIndustries = ['Technology', 'Finance', 'Healthcare'];
if (targetIndustries.includes(industry)) score += 25;
// Contact info scoring
if (hasEmail) score += 10;
if (hasPhone) score += 15;
// Source scoring
if (source === 'Referral') score += 20;
else if (source === 'Website') score += 10;
return { leadScore: score };
};
```
3. **更新记录**:将线索评分设置为 `{{code.leadScore}}`
## 文本公式:域名提取
### 示例:从电子邮件中提取域名
**目标**:自动提取并存储电子邮件域名。
### 设置
1. **触发器**:记录被更新(人员,电子邮件字段)
2. **代码操作**:
```javascript
export const main = async (params) => {
const { email } = params;
if (!email) return { domain: null };
const domain = email.split('@')[1]?.toLowerCase();
return { domain };
};
```
3. **更新记录**:将域名字段设置为 `{{code.domain}}`
## 最佳实践
### 性能
* 仅在相关字段更改时触发
* 使用过滤器跳过不需要计算的记录
* 避免在大规模工作流中进行复杂计算
### 错误处理
* 在计算前检查 null/undefined 值
* 在数据缺失时使用默认值
* 计算失败时返回清晰的错误信息
### 测试
* 使用边界情况进行测试(空字段、零值)
* 在启用前手动验证计算结果
* 监控工作流运行情况以发现异常结果
## 相关内容
* [如何在流水线中显示预期金额](/l/zh/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)
* [如何跟踪阶段时长](/l/zh/user-guide/views-pipelines/how-tos/track-time-in-stage)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,127 @@
---
title: 通知团队成员审阅笔记
description: 当指派同事审阅笔记时,自动发送电子邮件通知。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<Note>
**目前尚不支持在笔记中直接标记团队成员。** 此功能计划在 2026 年下半年推出。 在此期间,此工作流提供一个实用的变通方案。
</Note>
## 挑战
你希望在笔记中标记一位同事,以便其收到审阅通知。 目前,Twenty 不支持在笔记或任务中使用 @ 提及。
## 变通方案
不用在笔记正文中标记,而是可以:
1. 在笔记对象上创建一个 **Reviewer** 关系字段
2. 在创建或编辑笔记时指定审阅人
3. 工作流程自动发送电子邮件给审核者,链接到笔记
<Warning>
**限制**:此方法每个字段只能指定一位审阅人。 如果需要通知多人,请创建额外的关系字段(例如,Reviewer 1、Reviewer 2)。 这在需要一次性标记许多协作者时扩展性不佳。
</Warning>
## 分步设置
### 步骤 1:创建 Reviewer 字段
<img src="/images/user-guide/workflows/reviewer-field-for-note.png" style={{width:'100%'}} />
1. 进入 **设置 → 数据模型 → 笔记**
2. 点击 **添加字段**
3. 配置:
| 设置 | 值 |
| -------- | -------------- |
| **Name** | Reviewer |
| **类型** | 关系 |
| **关联对象** | 工作区成员 |
| **关系类型** | 多条笔记 → 一个工作区成员 |
4. 保存该字段
### 步骤 2:创建工作流
<VimeoEmbed videoId="1151897168" title="视频演示" />
#### 创建工作流触发器
1. 转到主视图并创建一个新工作流
2. 选择 **记录已更新** 作为触发器
3. 选择 **笔记** 作为对象,并指定 Reviewer 字段。
4. 每当笔记的审阅人被更新时都会触发。
#### 添加筛选器以检查是否刚刚指定了审阅人
添加一个 **筛选** 操作,仅在指定了审阅人时才继续:
| 条件 | 值 |
| ------------ | --- |
| **Reviewer** | 不为空 |
#### 搜索该工作区成员
1. 添加 **搜索记录** 操作
2. 选择 **Workspace Member**(位于高级对象下)
3. 添加过滤器:
| 条件 | 值 |
| ------ | ------------------------ |
| **ID** | 等于 `{{note.reviewerId}}` |
这将检索完整的工作区成员记录,包括其电子邮件地址和姓名。
#### 发送通知邮件
1. 添加 **Send Email** 操作
2. 配置:
| 字段 | 值 |
| ------- | ---------------------- |
| **收件人** | 搜索记录 → 第一个工作区成员 → 用户邮箱 |
| **主题** | 有一条笔记需要你审阅 |
| **正文** | 请参见下方示例 |
**电子邮件正文示例:**
```text
Hi {{searchRecord.firstWorkspaceMember.name.firstName}},
You've been assigned to review a note.
View the note here:
https://yourSubDomain.twenty.com/object/note/{{recordIsUpdated.id}}
Best,
Twenty
```
<Note>
将 `yourSubDomain` 替换为你实际的 Twenty 工作区子域名。
</Note>
#### 测试并激活
1. 点击 **Test** 以手动运行工作流
2. 创建或编辑一条笔记并指定审阅人
3. 在审阅人的收件箱中查收通知邮件
4. 激活工作流
## 用法
设置完成后,你的团队可以通过以下方式使用此工作流:
1. 创建新笔记(或编辑现有笔记)
2. 在 **Reviewer** 字段中选择一位同事
3. 该同事会自动收到一封包含该笔记直达链接的电子邮件
## 相关内容
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [从工作流发送电子邮件](/l/zh/user-guide/workflows/capabilities/send-emails-from-workflows)
* [创建自定义字段](/l/zh/user-guide/data-model/how-tos/create-custom-fields)
* [创建关系字段](/l/zh/user-guide/data-model/how-tos/create-relation-fields)

View File

@@ -0,0 +1,106 @@
---
title: 发送到期任务的电子邮件提醒
description: 自动通知团队成员他们即将到期或已逾期的任务。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/email_tasks_due.png" style={{width:'100%'}} />
每天向每位团队成员发送电子邮件提醒,告知他们今天到期的任务。
## 概览
此工作流按计划运行,并:
1. 获取所有工作区成员
2. 遍历每位成员
3. 查找他们今天到期的任务
4. 格式化并发送个性化电子邮件
## 分步设置
<VimeoEmbed videoId="1147463514" title="视频演示" />
### 步骤 1:配置触发器
1. 转到 **Settings → Workflows** 并创建一个新工作流
2. 选择 **On a Schedule** 作为触发器
3. 使用如下 cron 表达式以在每天上午 8:00 运行:`0 8 * * *`
### 步骤 2:搜索所有工作区成员
1. 添加 **搜索记录** 操作
2. 选择 **Workspace Members**(位于高级对象下)
3. 无需添加过滤器 — 这将返回所有成员
### 步骤 3:添加迭代器
1. 添加 **Iterator** 操作
2. 将输入数组设置为上一步的工作区成员
3. 迭代器中的所有操作将针对每位成员运行一次
### 步骤 4:搜索今天到期的任务(在迭代器内)
1. 在迭代器内,添加一个 **搜索记录** 操作
2. 选择 **Tasks** 作为对象
3. 添加过滤器:
* **Assignee** = 当前工作区成员(来自迭代器)
* **Due Date** = 今天
### 步骤 5:将任务格式化为电子邮件正文(在迭代器内)
添加一个 **Code** 操作,将任务格式化为带有链接的可读列表:
```javascript
export const main = async (params: {
tasksDue?: Array<{ id: string; title: string }> | null | string;
}) => {
const tasksDue =
typeof params.tasksDue === "string"
? JSON.parse(params.tasksDue)
: params.tasksDue;
if (!Array.isArray(tasksDue) || tasksDue.length === 0) {
return {
formattedTasks: "No tasks due today."
};
}
const formattedTasks = tasksDue
.map(
t =>
`${t.title}\nhttps://yourSubDomain.twenty.com/object/task/${t.id}`
)
.join("\n\n");
return { formattedTasks };
};
```
<Note>
将 `yourSubDomain` 替换为你实际的 Twenty 工作区子域名。
</Note>
### 步骤 6:发送电子邮件(在迭代器内)
1. 添加一个 **Send Email** 操作(仍在迭代器内)
2. 配置:
| 字段 | 值 |
| ------- | ------------------------------------------------ |
| **收件人** | `{{iterator.currentItem.userEmail}}`(工作区成员的电子邮件) |
| **主题** | 您今天到期的任务 |
| **正文** | `{{code.formattedTasks}}` |
### 步骤 7:测试并启用
1. 点击 **Test** 以手动运行工作流
2. 检查收件箱中的电子邮件
3. 激活工作流
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [从工作流发送电子邮件](/l/zh/user-guide/workflows/capabilities/send-emails-from-workflows)
* [在 Code 操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,21 @@
---
title: 专业服务
description: 获取 Twenty 团队及认证合作伙伴的帮助,构建复杂的工作流程和自动化。
---
## 什么时候需要专业帮助?
针对以下事项,考虑使用专业服务:
* 复杂的多系统集成
* 高级的业务逻辑和自动化规则
* 大规模数据处理工作流程
* 自定义API开发
* 团队培训和工作流程优化
* 当您没有内部资源时
## 服务选项
### 实施合作伙伴
与认证合作伙伴合作进行高级定制。 请通过 [contact@twenty.com](mailto:contact@twenty.com) 与我们联系,以便与我们的[实施合作伙伴](https://twenty.com/partners)建立联系。

View File

@@ -0,0 +1,170 @@
---
title: 工作流故障排除
description: 常见工作流问题及其解决方法。
---
## 常见问题及解决方案
### 工作流未触发
**症状**:您的工作流未在您预期的时间运行。
**可能原因**:
1. **工作流未激活**:请确保工作流设置为 "Active" 而非 "Draft"
2. **未满足触发条件**:请确认触发器与您期望的事件匹配
3. **字段未被监控**:对于 "Record is Updated" 触发器,请确保正在监控特定字段
4. **权限**:检查您是否具有运行工作流的权限
**解决方案**:
* 在工作流列表中核实工作流状态
* 使用您期望触发它的特定操作进行测试
* 检查触发器配置
* 就权限问题联系您的管理员
### 工作流触发过早(字段为空)
**症状**:在 UI 中手动创建记录时,您的工作流会在您填完所有字段之前就被触发。 工作流在大多数字段值为空的情况下运行。
**原因**:Twenty 实时保存所有内容——不存在单独的 "edit" 与 "read" 模式。 当您创建一条记录时,它会立即被保存,在您填写其他字段之前就触发 "Record is created" 事件。
**当 "Record is created" 效果良好时**:
* 通过 API 调用创建的记录(字段在单个请求中填充)
* 通过导入创建的记录
* 由其他工作流自动创建记录
**解决方案**:对于在 UI 中手动创建的记录,请改用 **"Record is created or updated"** 作为触发器。 这样一来:
* 工作流会在用户完成填写并保存字段后再触发
* 您将获得完整的数据,而不是空值
<Tip>
如果您只希望每条记录的工作流仅运行一次,请添加一个 Filter 操作以检查某个字段,例如 `createdAt equals updatedAt`(首次保存),或使用自定义复选框字段来跟踪该工作流是否已运行。
</Tip>
### 操作失败
**症状**:工作流已运行,但部分操作失败。
**可能原因**:
1. **数据缺失**:必填字段为空
2. **引用无效**:来自前面步骤的变量不存在
3. **API 错误**:外部服务返回错误
4. **权限问题**:该操作需要您不具备的权限
**解决方案**:
* 在工作流运行详情中查看错误消息
* 确认所有必填字段都有值
* 单独测试 API 连接
* 检查角色权限
### HTTP 请求错误
**症状**:HTTP 请求操作失败或返回意外结果。
**常见错误代码**:
* **400**: 错误请求 - 检查请求正文格式
* **401**: 未授权 - 验证 API 密钥
* **403**: 禁止访问 - 检查 API 权限
* **404**: 未找到 - 核实端点 URL
* **429**: 请求过多 - 实施速率限制
* **500**: 服务器错误 - 外部服务问题
**解决方案**:
* 核实 API 端点 URL
* 检查身份验证请求头
* 先在 Twenty 之外测试该 API 调用
* 在 Code 操作中添加错误处理
### Code 操作错误
**症状**:JavaScript 代码执行失败。
**常见问题**:
1. **语法错误**:拼写错误或无效的 JavaScript
2. **未定义的变量**:引用了不存在的变量
3. **类型错误**:对错误的数据类型进行操作
4. **超时**:代码执行时间过长
**解决方案**:
* 使用内置代码编辑器的校验功能
* 先在 JavaScript 控制台中测试代码逻辑
* 添加 console.log 语句进行调试
* 简化复杂操作
### 电子邮件无法发送
**症状**:Send Email 操作未能投递电子邮件。
**可能原因**:
1. **未连接电子邮件账户**:检查 Settings → Accounts
2. **无效的电子邮件地址**:收件人电子邮件格式错误
3. **发送限制**:已达到电子邮件服务商的速率限制
4. **垃圾邮件过滤器**:电子邮件被拦截
**解决方案**:
* 确认电子邮件账户连接正常
* 校验收件人电子邮件地址
* 检查电子邮件服务商的限制
* 检查电子邮件内容是否包含垃圾邮件触发因素
## 调试工作流
### 使用工作流运行
1. 进入工作流编辑器
2. 打开 **Runs** 面板
3. 找到失败的运行
4. 点击查看逐步详情
5. 查看错误消息和输出数据
### 测试单个步骤
1. 对于 Code 操作,使用 **Test** 按钮
2. 对于 HTTP 请求,请单独测试该端点
3. 创建测试记录以触发工作流
4. 使用手动触发器进行可控测试
### 常见调试模式
**添加日志**:
使用 Code 操作记录中间值以进行调试。
**隔离步骤**:
独立测试每个步骤以定位失败。
**检查数据流**:
确认每个步骤都接收到预期的输入数据。
## 避免问题的最佳实践
### 激活前
* 在草稿模式下进行充分测试
* 验证所有 API 连接
* 仔细检查触发条件
* 记录预期行为
### 开发过程中
* 使用具有描述性的步骤名称
* 在 Code 操作中添加注释
* 使用真实数据进行测试
* 考虑边界情况
### 激活后
* 密切监控初始运行
* 设置失败告警
* 定期查看运行历史
* 尽可能保持工作流简洁

View File

@@ -0,0 +1,236 @@
---
title: 工作流常见问题
description: 关于 Twenty 中工作流的常见问题。
---
<AccordionGroup>
<Accordion title="为什么我无法启用工作流?">
这很可能是权限问题。 您需要工作流的访问权限才能创建并启用它们。
**解决方法**:联系您的工作区管理员,在**设置 → 成员 → 角色**下为您授予工作流访问权限。
如果您在侧边栏根本看不到工作流部分,这就说明是权限问题。
</Accordion>
<Accordion title="为什么我在顶部导航栏中看不到我的手动工作流按钮?">
手动工作流仅在正确配置后才会出现在导航栏中:
1. 工作流必须**已启用**(不是草稿模式)
2. 导航栏位置必须设置为**Pinned**
3. 对于 Single/Bulk 触发器,您必须位于正确的对象页面上
**检查方式**:打开工作流 → 点击触发器 → 确认 "Navbar placement" 设置为 "Pinned"。
无论导航栏设置如何,您始终可以通过**Cmd + K**(或**Ctrl + K**)访问手动工作流。
</Accordion>
<Accordion title="Single、Global 和 Bulk 手动触发器有什么区别?">
| 类型 | 需要的记录 | 工作流运行 |
| -- | ----- | ----- |
\| **Global** | 无 | 一次,无记录输入 |
\| **Single** | 选择一个或多个 | 每个选中的记录执行一次 |
\| **Bulk** | 选择一个或多个 | 执行一次,所有记录作为数组 |
* **Global**:当工作流不需要任何记录上下文时使用(例如,生成报告)
* **Single**:当您希望独立处理每个选定的记录时使用(例如,发送单独的电子邮件)
* **Bulk**:当您需要一起处理记录或优化额度使用时使用(需要 Iterator 操作)
详见 [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)。
</Accordion>
<Accordion title="我没有看到 If/Else 节点。 我应该怎么办?">
明确的 If/Else 节点尚不可用,但已在我们的路线图上。
**当前变通方法**:从您的步骤创建多个分支,每个分支以一个**Filter** 操作开始:
```
Step 1
│
├── Branch A: Filter (condition = true) → Actions...
│
└── Branch B: Filter (condition = false) → Actions...
```
只有通过 Filter 条件的分支会执行后续操作。
逐步指南请参见[如何使用分支](/l/zh/user-guide/workflows/capabilities/workflow-branches)。
</Accordion>
<Accordion title="工作流分支是并行运行的吗?">
**是的**,分支默认并行运行。
如果您只希望一个分支执行:
* 在每个分支的开头添加一个**Filter** 操作
* 设置相反的条件(例如,分支 A:status = "Open",分支 B:status ≠ "Open")
未通过其 Filter 条件的分支将停止执行,其他分支会继续。
</Accordion>
<Accordion title="我可以将分支合并在一起吗?">
**是**。 并行分支完成后,您可以添加一个步骤,让两个分支都连接到该步骤。
在工作流编辑器中:
1. 完成您的分支内操作
2. 在分支之后添加一个新步骤
3. 将每个分支末端的连接拖拽到这个新步骤
合并后的步骤将在所有已连接分支完成后执行。
</Accordion>
<Accordion title="Search Records 的结果限制是多少?">
**Search Records 最多返回 200 条记录。**
如果您需要处理更多:
* 添加更具体的筛选条件以减少结果
* 使用计划的工作流分批处理
* 考虑使用 API 进行批量操作
对大多数工作流而言,200 条记录已足够。 如果您经常触及此限制,请考虑重构您的自动化。
</Accordion>
<Accordion title="我可以在工作流邮件中抄送他人吗?">
\*\*还不可以。\*\*Send Email 操作的 CC 和 BCC 字段已在我们的路线图上。
**当前变通方法**:添加多个 Send Email 操作以发送给其他收件人,或者使用 HTTP Request 通过支持 CC 的外部电子邮件服务发送。
</Accordion>
<Accordion title="如何使用前面步骤的输出?">
每个操作都会产生可在后续步骤中使用的输出数据。
**引用前一步数据**:
* 在配置字段时使用变量选择器
* 或直接输入 `{{stepName.fieldName}}`
**示例**:
* 触发器数据:`{{trigger.object.email}}`
* 搜索结果:`{{searchRecords[0].name}}`
* 代码输出:`{{code.calculatedValue}}`
在操作配置中将鼠标悬停在任意字段上,即可查看前面步骤可用的变量。
</Accordion>
<Accordion title="为什么我的 Iterator 会失败?">
**Iterator 需要数组输入。** 常见问题:
1. **输入不是数组**:请确保您传入的是 Search Records 或其他返回数组的操作的结果
2. **数组为空**:在 Iterator 之前添加筛选条件,检查 `{{searchRecords.length}} > 0`
3. **选择了错误的变量**:请确保您选择的是数组本身,而不是单个记录
**正确设置**:
1. Search Records(返回数组)
2. Filter:length > 0
3. Iterator:选择 `{{searchRecords}}`
4. Iterator 内的操作使用 `{{iterator.currentItem.fieldName}}`
</Accordion>
<Accordion title="Code 操作的最大执行时间是多少?">
Code 操作(逻辑函数)的**默认超时时间为 5 分钟**(300 秒)。
可配置的最大超时时间为**15 分钟**(900 秒)。
如果您的代码超过此限制,该操作将因超时错误而失败。
**避免超时的提示**:
* 使用 Iterator 将大型操作拆分为更小的块
* 避免进行大量计算;通过 HTTP Request 使用外部服务进行密集型处理
* 优化您的代码以减少执行时间
* 如果您需要更长的处理时间,考虑使用计划的工作流按批次处理数据
</Accordion>
<Accordion title="如何查看工作流运行记录?">
工作流运行显示执行历史,并帮助您调试问题。
**访问运行记录**:
* 在工作流编辑器中 → 右侧的 **运行** 面板
* 或者前往侧边栏中的 **工作流运行**
**了解一次运行**:
* **状态**:运行中、已完成、失败、等待中
* **步骤**:查看哪些步骤已执行及其输出
* **错误**:点击失败的步骤查看错误消息
* **数据**:查看每个步骤的输入/输出数据
详见 [工作流运行](/l/zh/user-guide/workflows/capabilities/workflow-runs)。
</Accordion>
<Accordion title="为什么我看不到我的工作流运行?">
工作流运行可能由于速率限制而立即失败。 Twenty 对每个工作区每小时的运行次数实施 5,000 次的硬性限制。
如果超出此限制,工作流会被立即标记为失败,并且不会出现在您的运行列表中。
**常见的触发此限制的场景**:
* 使用 Single 手动触发器选择超过 5,000 条记录
* 整个工作区同时运行多个工作流
* 高频率的自动触发(例如,在繁忙对象上的 Record Updated)
**解决方案**:
* 使用**Bulk** 触发器而非 Single,以在一次运行中处理多条记录
* 将大型批量操作分散进行
* 使用筛选条件降低触发频率
* 将繁重的工作流安排在非高峰时段
</Accordion>
<Accordion title="工作流的速率限制是什么?">
Twenty 设有两个速率限制以确保系统稳定性:
| 限制 | 值 | 行为 |
| -- | - | -- |
\| **Soft limit** | 每分钟 100 次运行 | 运行会以 "Not Started" 状态排队,逐步处理 |
\| **Hard limit** | 每小时 5,000 次运行 | 运行会立即失败 |
**Soft limit(100/分钟)**:您的工作流不会失败——它们只会在队列中等待,并随着时间被处理。 您可以触发超过 100 条记录;执行速度会更慢。
**Hard limit(5,000/小时)**:这适用于您的整个工作区。 如果您所有工作流合计在一小时内超过 5,000 次运行,额外的运行将立即失败。
**保持在限制内的提示**:
* 对于大型批量,使用带有 Iterator 的 Bulk 触发器,而不是 Single 触发器
* 将相关的自动化合并为更少的工作流
* 使用计划的工作流在一段时间内分散负载
</Accordion>
<Accordion title="我可以重试失败的工作流运行吗?">
**不,目前没有自动重试功能。**
如果某次工作流运行失败,您需要:
1. 在**设置 → 工作流 → [Your Workflow] → 运行**中查看错误
2. 修复问题(数据、配置或外部服务)
3. 在受影响的记录上手动再次触发该工作流
**减少失败的提示**:
* 在操作前添加**Filter** 节点以验证数据
* 使用**Search Records** 检查相关记录是否存在
* 在进行批量操作之前,先用少量记录充分测试
自动重试功能已在我们的路线图上,计划在未来版本中提供。
</Accordion>
<Accordion title="在进行 CSV 导入前,我需要停用工作流吗?">
**是的,如果您的工作流由记录的创建或更新触发。**
当您通过 CSV 导入数据时,每个创建或更新的记录都可能触发工作流。 大规模导入(数千条记录)可能会:
* 触及每小时 5,000 次运行的限制
* 消耗大量工作流额度
* 发送意外的电子邮件或通知
* 创建重复的任务或记录
**进行批量导入之前**:
1. 进入**设置 → 工作流**
2. 识别由您要导入的对象触发的工作流
3. 暂时**停用**它们
4. 执行您的 CSV 导入
5. 完成后**重新启用**这些工作流
**替代方案**:如果您需要这些工作流在导入的数据上运行,请以较小的批次导入以维持在速率限制内。
</Accordion>
<Accordion title="如何整理我的工作流布局?">
如果您的工作流画布看起来很乱,节点散落各处,您可以自动整理:
1. 在工作流画布的任意位置点击右键
2. 点击**整理工作流**
这将自动将所有节点重新排列为整洁有序的布局。
</Accordion>
</AccordionGroup>