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,167 @@
---
title: 通过工作流发送电子邮件
description: 使用工作流操作自动发送个性化电子邮件。
---
当您的 CRM 中发生特定事件时自动发送电子邮件——欢迎新联系人、跟进机会,或通知团队成员。
## 先决条件
在您可以通过工作流发送电子邮件之前:
1. 在 **Settings → Accounts** 下连接电子邮件帐户
2. 确保该帐户已启用发送权限
## 基础电子邮件工作流
### 示例:新联系人的欢迎电子邮件
**目标**:当 CRM 中添加新联系人时,发送一封欢迎电子邮件。
**设置**:
1. **创建工作流**:转到 **Settings → Workflows** 并点击 **+ New Workflow**
2. **添加触发器**:选择 **Record is Created** → **People**
3. **添加 Send Email 操作**:
* 点击 **+** 以添加一个操作
* 选择 **Send Email**
* 配置电子邮件:
| 字段 | 值 |
| ------- | -------------------------------------- |
| **收件人** | `{{trigger.object.email}}` |
| **主题** | `欢迎来到 {{Your Company Name}}` |
| **正文** | `Hi {{trigger.object.firstName}}, ...` |
4. **测试并激活**:使用示例记录进行测试,然后激活
## 在电子邮件中使用变量
使用 `{{variable}}` 语法引用前面步骤中的数据:
```text
Hi {{trigger.object.firstName}},
Thank you for connecting with us!
Your company, {{trigger.object.company.name}}, is now in our system.
Best regards,
The Team
```
### 触发器中的可用变量
| 触发类型 | 常用变量 |
| ------------- | -------------------------------------- |
| **记录已创建/已更新** | `{{trigger.object.fieldName}}` |
| **手动** | `{{trigger.selectedRecord.fieldName}}` |
| **Webhook** | `{{trigger.body.fieldName}}` |
## 高级:条件邮件
### 示例:基于线索来源发送不同邮件
**目标**:根据线索来源发送不同的欢迎邮件。
**设置**:
1. **触发器**:Record is Created(People)
2. **添加 Filter 操作**:
* 条件:`{{trigger.object.source}}` 等于 "Website"
* 如果为真 → 继续发送网站渠道的欢迎邮件
3. **其他来源的分支**:
* 为不同来源创建并行分支
* 每个分支都有其自己的 Send Email 操作
## 向多个收件人发送电子邮件
### 示例:交易达成时通知团队
**目标**:当机会赢单时,向销售代表及其经理发送电子邮件。
**设置**:
1. **触发器**:记录已更新(Opportunities,Stage = "Closed Won")
2. **搜索记录**:查找机会所有者的经理
3. **发送邮件 #1**:发送给机会所有者
* 收件人:`{{trigger.object.owner.email}}`
* 主题:`Congratulations on closing {{trigger.object.name}}!`
4. **发送邮件 #2**:发送给经理
* 收件人:`{{searchRecords.manager.email}}`
* 主题:`Deal Won: {{trigger.object.name}}`
## 定时跟进邮件
### 示例:会议后 3 天进行跟进
**目标**:在会议被记录后 3 天发送一封跟进邮件。
**设置**:
1. **触发器**:记录创建(Activities,Type = "Meeting")
2. **延迟操作**:等待 3 天
3. **发送邮件**:
* 收件人:会议参与者
* 主题:就我们的交流进行跟进
* 正文:参考触发器中的会议详细信息
## 电子邮件附件
您可以将文件附加到由工作流发送的电子邮件中。 该附件是在配置工作流时上传的静态文件。
### 如何添加附件
1. 在 **Send Email** 操作中,点击 **Attachments** 按钮
2. 点击以从您的计算机上传文件
3. 该文件将附加到此操作发送的所有电子邮件中
### 常见使用案例
| 用例 | 示例文件 |
| -------- | ----------- |
| **欢迎邮件** | 公司宣传册 (PDF) |
| **入门序列** | 入门指南 |
| **销售跟进** | 产品目录或价目表 |
| **活动确认** | 活动详情或议程 |
<Note>
附件是静态的——将向所有收件人发送相同的文件。 对于动态文档(如个性化报价),请使用 [逻辑函数](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) 生成并附加文件。
</Note>
## 最佳实践
### 电子邮件内容
* 保持主题行简洁且相关
* 使用收件人姓名进行个性化
* 包含明确的行动号召
* 在激活之前测试邮件
### 送达率
* 不要在短时间内发送过多电子邮件
* 使用专业的电子邮件签名
* 避免使用垃圾邮件触发词
* 确保营销邮件提供退订选项
### 故障排除
* 确认电子邮件帐户已连接且处于活动状态
* 检查收件人的电子邮件地址是否有效
* 查看工作流运行中的错误消息
* 先使用您自己的电子邮件地址进行测试
## 相关内容
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,88 @@
---
title: 在工作流中使用分支
description: 了解分支如何工作,以及如何控制执行哪条路径。
---
## 分支的工作方式
在工作流编辑器中,您可以从单个节点延伸出多条路径(分支)。 这使您能够构建具有不同结果的复杂自动化流程。
**重要**:当工作流运行时,默认情况下**所有分支都会并行执行**。 没有内置的 "if/else" 逻辑来在分支间做出选择——每条路径都会同时运行。
## 控制执行哪个分支
若要根据特定条件仅执行一个分支,**在每个分支的开头添加一个 Filter 节点**。
### 示例设置
1. 从单个节点创建包含多个分支的工作流
2. 在每个分支的第一步添加一个**Filter** 节点
3. 在每个 Filter 上设置条件,以确定该分支何时继续
4. 只有满足其 Filter 条件的分支会继续
### Filter 的工作方式
* 如果 Filter 条件被**满足**:该分支继续执行
* 如果 Filter 条件**未满足**:该分支会在 Filter 节点停止
这实际上创建了条件逻辑,使只有与您的数据相匹配的合适分支会运行。
## 示例:按交易金额路由
**场景**:当交易达成时,根据交易金额发送不同的通知。
1. **触发器**:Opportunity 已更新(Stage = Closed Won)
2. **分支 1**:Filter 条件:Amount > $10,000 → 向 #big-deals 发送 Slack 消息
3. **分支 2**:Filter 条件:Amount ≤ $10,000 → 向销售经理发送电子邮件
两个分支都会启动,但只有与交易金额匹配的分支会通过其 Filter 继续执行。
## 创建分支
<Note>
要从现有步骤创建新分支,请点击该步骤上的 **+** 按钮并添加您的操作。 您可以多次点击 **+** 来添加多个分支。
</Note>
1. 在工作流编辑器中,选择您想要从其分支出去的步骤
2. 点击 **+** 以添加一个操作
3. 这将创建一个分支
4. 在同一步骤上再次点击 **+** 以创建更多分支
5. 每个分支都可以拥有自己的一系列操作
## 合并分支
并行分支完成其工作后,您可以将它们合并回单一路径:
1. 完成您的分支内操作
2. 在所有分支之后添加一个应运行的新步骤
3. 将每个分支末端的连接拖拽到这个新步骤
4. 合并后的步骤会在所有连接的分支完成后才执行
### 示例:先处理后通知
```
Trigger
│
├── Branch A: Update Customer Record
│
└── Branch B: Create Support Ticket
↘ ↙
Merged Step: Send Confirmation Email
```
仅在客户更新和工单创建都完成后,才会发送确认电子邮件。
## 最佳实践
* 当您需要条件执行时,请始终在分支开始处使用**Filter 节点**
* 使分支条件**互斥**,以避免重复操作
* 使用不同数据测试您的工作流,以确保运行正确的分支
* **重命名分支步骤**,使每条路径的作用清晰明了
* **合并分支**,当您在并行处理后需要执行最终操作时
## 相关内容
* [工作流常见问题](/l/zh/user-guide/workflows/how-tos/need-more-help/workflows-faq) — 关于并行执行的解答
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions) — 分支可用的操作

View File

@@ -0,0 +1,183 @@
---
title: 使用迭代器
description: 遍历记录数组,对每个项执行操作。
---
迭代器可让你遍历记录数组,并对其中每一项执行操作。 对于需要处理由搜索记录返回或通过 Webhook 接收的多条记录的工作流,它至关重要。
## 何时使用迭代器
| 场景 | 示例 |
| ----------------- | --------------- |
| **处理搜索结果** | 向每位找到的人员发送电子邮件。 |
| **处理 Webhook 数组** | 按顺序为每个项目创建记录 |
| **批量更新** | 使用计算得出的值更新多条记录 |
| **通知** | 就某个事件通知多位人员 |
## 了解迭代器
迭代器需要一个**数组**作为输入。 然后:
1. 从数组中取出第一个元素
2. 使用该元素运行迭代器内的所有操作
3. 移动到下一个元素
4. 重复,直到所有元素处理完毕
### 选择要循环的数组
大多数数组来源(Search Records、Webhook 数组字段、Bulk manual trigger)都可以直接在变量选择器中选择。
当 **Code** 或 **Logic Function** 步骤返回一个*顶级数组*时,其输出会在变量选择器中显示为带索引的条目(`0`、`1`、`2`,…)。 若要对整个数组进行循环,请为该步骤选择 **Whole list** 选项 — Iterator 会自动从列表中推断每个项目的结构。
## 基本设置
### 示例:向搜索结果中的所有人发送电子邮件
**目标**:查找特定公司的所有联系人,并向每个人发送个性化电子邮件。
### 步骤 1:搜索记录
1. 添加 **搜索记录** 操作
2. 对象:**People**
3. 筛选条件:公司等于 "Acme Inc"
4. 这将返回一个人员数组
### 步骤 2:检查是否有结果
1. 添加 **过滤** 操作
2. 条件:`{{searchRecords.length}}` 大于 0
3. 这可防止在结果为空时出现迭代器错误
### 步骤 3:添加迭代器
1. 添加 **Iterator** 操作
2. 数组输入:选择 `{{searchRecords}}`
3. 这会创建一个循环
### 步骤 4:在迭代器内添加操作
在迭代器之后放置的操作会针对每个项运行:
1. 添加 **Send Email** 操作(在迭代器内)
2. 收件人:`{{iterator.currentItem.email}}`
3. 主题:你好,`{{iterator.currentItem.firstName}}`!
4. 正文:使用当前项字段的个性化消息
### 结果
如果 Search Records 返回 5 个人,迭代器将:
* 向第 1 个人发送电子邮件
* 向第 2 个人发送电子邮件
* ... 对全部 5 人继续执行
## 访问当前项数据
在迭代器内,使用 `{{iterator.currentItem}}` 访问当前记录:
| 变量 | 描述 |
| --------------------------------------- | ----------------- |
| `{{iterator.currentItem}}` | 整个当前记录对象 |
| `{{iterator.currentItem.id}}` | 记录 ID |
| `{{iterator.currentItem.email}}` | 电子邮件字段 |
| `{{iterator.currentItem.company.name}}` | 关联的公司名称 |
| `{{iterator.index}}` | 在数组中的当前位置(从 0 开始) |
在变量选择器中,你可以深入当前项目的特定字段,或者选择 **Use the whole item** 来引用整个当前项目(`{{iterator.currentItem}}`)。 在你希望将完整记录直接传入下游步骤,而不是逐个字段重新构建时,选择整个项目会非常方便。
## 常见模式
### 更新多条记录
**目标**:将所有逾期任务标记为 "Late"
```
1. Search Records (Tasks, Due Date < Today, Status ≠ Completed)
2. Filter (length > 0)
3. Iterator (searchRecords)
└── Update Record
- Object: Tasks
- Record: {{iterator.currentItem.id}}
- Status: Late
```
### 从数组创建记录
**目标**:Webhook 接收到包含多个项目的订单,为每个项目创建一条记录。
```
1. Webhook Trigger (receives items array)
2. Filter (items.length > 0)
3. Iterator (trigger.body.items)
└── Create Record
- Object: Order Items
- Name: {{iterator.currentItem.name}}
- Quantity: {{iterator.currentItem.qty}}
- Related Order: {{trigger.body.orderId}}
```
### 在循环内进行条件处理
**目标**:仅向电子邮件地址有效的联系人发送邮件
```
1. Search Records (People)
2. Iterator (searchRecords)
└── Filter (currentItem.email is not empty)
└── Send Email
- To: {{iterator.currentItem.email}}
```
## 故障排除
### "迭代器需要一个数组"
**原因**:你传递的是单个记录而不是数组。
**修复方法**:请确保传递的是 Search Records 的结果或数组字段,而不是单个记录。
```
✅ Correct: {{searchRecords}}
❌ Wrong: {{searchRecords[0]}}
```
### 迭代器未运行
**原因**:数组为空。
**修复方法**:在迭代器之前添加筛选条件以检查数组长度:
```
Filter: {{searchRecords.length}} > 0
```
### 操作运行次数过多
**原因**:Search Records 返回的记录数超出预期。
**修复方法**:
* 在 Search Records 中添加更具体的筛选条件
* 在 Search Records 中设置限制(最多 200)
* 在迭代器内部添加筛选条件用于附加条件
## 性能注意事项
* **额度使用**:每次迭代都会为其操作消耗额度
* **时间**:较大的数组处理时间更长
* **限制**:对于非常大的操作,考虑分批处理
* **速率限制**:大量迭代的外部 API 调用可能触发速率限制
## 最佳实践
1. **在迭代器之前始终检查数组长度**,以避免错误
2. **当并非所有项都需要处理时,在循环中添加筛选条件**
3. **重命名你的迭代器步骤**,以描述它正在遍历的内容
4. **先使用小数组进行测试**,再处理大型数据集
5. **监控工作流运行**,以确保迭代按预期完成
## 相关内容
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [如何使用分支](/l/zh/user-guide/workflows/capabilities/use-branches-in-workflows)
* [工作流常见问题](/l/zh/user-guide/workflows/how-tos/need-more-help/workflows-faq)

View File

@@ -0,0 +1,332 @@
---
title: 工作流动作
description: 了解 Twenty 工作流中可用的操作。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## 关于操作
动作定义触发后将发生的事件。 你可以将多个操作串联在一起,以构建复杂的自动化流程。
<Tip>
* 使用变量选择器(点击`(x+)`图标)浏览来自先前步骤的可用数据
* 将鼠标悬停在任意输入字段上即可查看变量来自哪个步骤——当相同字段(例如,ID)存在于多个先前步骤时,这很有帮助
* 为每个操作指定一个具有描述性的名称,以便于维护
</Tip>
## 记录操作
<img src="/images/user-guide/workflows/workflow_actions.png" style={{width:'100%'}} />
### 创建记录
在选定的对象中添加新记录。
**配置**:
* 选择目标对象
* 填写必填和可选字段
* 使用来自先前步骤的数据,或手动输入值来填充字段
**输出**:新创建的记录数据可在后续步骤中使用。
### 更新记录
修改选定对象中的现有记录。
<img src="/images/user-guide/workflows/update_record.png" style={{width:'100%'}} />
**配置**:
* 选择目标对象
* 选择要更新的特定记录.
* 你可以通过下拉菜单选择一个固定记录,该菜单会显示所有可用记录。
* 或者通过使用`(x+)`指定在先前步骤中找到的记录,让记录被动态选择。 此阶段无法根据不同条件搜索该记录。 如果你尚未确定该记录,请在此`Update Record`步骤之前添加一个`Search Record`步骤。
* 选择要修改的字段并输入新值
**输出**:更新后的记录数据可用于后续步骤。
### 删除记录
从选定的对象中删除记录。
**配置**:
* 选择目标对象
* 选择要删除的特定记录
**输出**:已删除的记录数据可继续用于后续步骤。
### 搜索记录
在选定对象中使用过滤条件查找记录。
**配置**:
* 选择要搜索的对象
* 设置过滤条件以缩小结果
* 配置排序和限制
**输出**:返回可在后续步骤中使用的匹配记录。
<Note>
**限制**:Search Records 最多返回 **200 条记录**。 如果需要处理更多,请添加特定筛选器以减少结果,或使用计划的工作流分批处理。
</Note>
**最佳实践**:在 Search Records 之后使用[分支](/l/zh/user-guide/workflows/capabilities/workflow-branches)来处理"找到"与"未找到"场景。
### Upsert 记录
根据匹配条件创建新记录或更新现有记录。 当你不确定记录是否已存在时,这非常有用。
<img src="/images/user-guide/workflows/upsert_double_identifier.png" style={{width:'100%'}} />
**配置**:
* 选择目标对象
* 请注意哪些字段可用于匹配:People 使用 email,Companies 使用 domain,任何对象使用 ID,或任何标记为 "Unique" 的字段。 你需要在下方至少填充其中一个。
* 填写字段值。 别忘了至少填充一个唯一标识符。
<Note>
**仅添加一个唯一标识符时,匹配通常效果更好。** 例如,下面的截图将基于其域名匹配公司。 不一定需要 ID。
</Note>
<img src="/images/user-guide/workflows/upsert_simple_identifier.png" style={{width:'100%'}} />
* 使用来自先前步骤的数据填充字段
**工作原理**:
1. 搜索与您的条件匹配的记录
2. 如果找到 → 更新现有记录
3. 如果未找到 → 创建新记录
**输出**:已创建或已更新的记录数据可用于后续步骤。
## 工作流操作
### 迭代器
**循环遍历**前一步返回的记录数组,允许你对每条记录分别执行操作。
**配置**:
* 从前一步中选择记录数组(例如,来自“搜索记录”的结果、来自具有批量可用性的手动触发、来自代码节点)。 当 Code 或逻辑函数步骤返回顶级数组时,选择其 **Whole list** 选项以遍历整个输出。
* 定义要对循环中每个记录执行的动作.
<Note>
- 你可以在迭代器中添加多个操作。
- 在迭代器中使用分支时,确保每个分支的最后一步连接回迭代器以闭合循环。
</Note>
* 访问 `Current Item` 字段:要使用当前正在处理的记录中的字段,点击 **Iterator** 步骤,然后选择 **Current item**。 将显示该记录可用字段的列表,并可选择用于后续操作。 你也可以选择 **Use the whole item**,将当前整个条目传递到下游步骤。
<VimeoEmbed videoId="1146577247" title="视频演示" />
### 过滤
根据指定条件过滤记录,仅允许满足条件的记录通过。
**配置**:
* 选择要过滤的记录
* 定义过滤条件和标准
* 配置哪些记录应通过到后续步骤
<Note>
1. **输出**:筛选器节点不返回数据——它们充当闸门。 如果满足条件,工作流将继续。 否则,工作流会在该分支停止。
2. `IS` 运算符可用于数值字段。 其作用等同于 `EQUAL`。
</Note>
### 延迟
将工作流执行暂停指定时长或直到特定日期/时间。
**延迟类型**:
| 类型 | 描述 |
| -------- | ----------------- |
| **持续时间** | 等待特定时长(天、小时、分钟、秒) |
| **计划日期** | 等待直到特定日期和时间 |
**持续时间的配置**:
* 设置天、小时、分钟和/或秒
* 组合多个单位(例如 2 天 4 小时)
**计划日期的配置**:
* 选择日期和时间
* 可以引用前一步的日期字段(例如,在会议后 3 天跟进)
**用法**:
* 在发送跟进邮件前等待 24 小时
* 暂停直到商机的关闭日期
* 将操作安排在工作时间内
<Note>
计划日期不能早于当前时间。 如果使用了前一步的日期字段且该日期已过去,工作流将失败。
</Note>
**限制与点数**:
* **无最长期限限制**——你可以设置分钟、天、周或更长的延迟
* **消耗 1 个点数**——无论持续时间如何,延迟节点执行时会消耗。
* **等待期间不消耗点数**——5 分钟的延迟与 5 天的延迟成本相同
## 通信操作
### 发送邮件
从工作流发送电子邮件。 这非常适合使用模板的群发邮件。 邮件看起来就像你从邮箱发送的邮件。
它不适合新闻简报(需要更丰富的格式)或自动化邮件序列。
**先决条件**:在“设置” → “账户”中添加电子邮件账户
**配置**:
* 选择发件邮箱账户
<Note>
你只能从与自己 Twenty 账户同步的邮箱发送邮件。 从其他团队成员的邮箱发送(例如账户所有者的邮箱)已在规划中。
</Note>
对于以下所有步骤,你可以引用前面步骤中的变量以实现个性化。
* 输入收件人电子邮件地址.
<Note>
目前只能指定一个收件人。
</Note>
* 设置主题行。
* 撰写消息内容. 你可以设置链接格式、创建编号列表、项目符号列表,并添加附件。
<Note>
目前无法添加 HTML 签名。
</Note>
### 表单
在工作流执行期间提示表单以收集用户输入。 随后可在后续步骤中使用这些响应,根据输入创建记录、发送电子邮件或执行其他任何操作。
<Warning>
**表单当前仅设计用于手动触发。** 对于具有其他触发器(记录创建、更新等)的工作流,表单仅可通过工作流运行界面访问,这不是预期的用户体验。 将于 2026 年发布通知中心,以适当支持自动化工作流中的表单。
</Warning>
**配置**:
* 配置要求用户填写的字段。 针对每个字段,选择
* 一种类型:文本、数字、日期、指定记录、选择字段。 所有对象中的字段都可供选择。
* 一个标签
* 在 `Placeholder` 下设置默认值(可选)
* 编辑表单标题
**输出**:表单响应可在后续步骤中使用。
**示例**:"Quick Lead" 工作流在所有工作区中默认可用,可在任意位置通过命令菜单 `Cmd+K` 调用。
**如何填写表单**:
* 从命令菜单 `Cmd+K` 触发你的手动工作流
* 在侧边面板中填写显示的表单并点击 `Submit`。
<Note>
这些字段无法设为必填。
</Note>
<VimeoEmbed videoId="1147277989" title="视频演示" />
## 集成操作
### 代码
在工作流中运行自定义 JavaScript。
在幕后,每个 Code 操作都由其自己的 **逻辑函数** 支持——一个在 Twenty 平台上运行的服务器端 TypeScript 函数。 当你添加 Code 操作时,Twenty 会为该步骤创建一个专用的逻辑函数,并内联显示其编辑器,因此你编写的代码与工作流共存。
**配置**:
* 访问先前步骤中的变量. 你可以动态编辑变量名称。
<VimeoEmbed videoId="1147281795" title="视频演示" />
* 在编辑器中编写 JavaScript 代码
* 返回变量以供后续步骤使用
* 直接在步骤中测试代码
<Note>
如果需要在代码中使用外部 API 密钥,必须将其直接输入到函数主体中。 你无法在其他位置配置 API 密钥并在逻辑函数中引用它们。
</Note>
<Tip>
**处理数组?** 来自外部系统或前序步骤的数组可能以字符串形式传入。 解决方案请参见 [如何在 Code 操作中处理数组](/l/zh/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)。
</Tip>
<Tip>
点击代码编辑器右上角的方形图标以全屏显示——这很有用,因为默认编辑器宽度有限。
</Tip>
#### 在多个工作流中复用逻辑函数
内联 Code 操作用于一次性逻辑非常合适,但其中的代码仅属于该单个步骤。 当你希望在多个工作流之间共享相同逻辑,或在应用中将其维护为有版本控制的源码时,应定义一个**可复用逻辑函数**,而不是在每个 Code 操作中复制粘贴代码。
逻辑函数在应用中定义一次(使用 SDK 的 `defineLogicFunction`),并通过添加 `workflowActionTriggerSettings` 暴露给工作流构建器。 安装应用后,该函数会作为独立操作出现在工作流构建器中,与内置操作并列。 选择它会渲染你声明的输入字段(带有变量选择器,与其他操作相同),并运行你的共享代码 —— 无需内联 JavaScript。
| | Code 操作 | 可复用逻辑函数 |
| ---------- | ---------------- | ------------------- |
| **代码所在位置** | 内联编辑器,绑定到单个工作流步骤 | 在应用中定义,在源码管理中进行版本控制 |
| **复用** | 按步骤复制粘贴 | 可作为操作添加到任意工作流中 |
| **输入** | 在编辑器中接入的变量 | 根据你声明的输入模式渲染出的字段 |
| **最适合** | 快速、特定于工作流的逻辑 | 在多个工作流中复用的共享逻辑 |
二者运行在同一逻辑函数运行时上,因此你在内联完成原型的 Code 操作,之后可以以最小改动升级为可复用逻辑函数。
<Tip>
有关为开发者提供的定义逻辑函数并将其暴露给工作流构建器的参考文档,请参阅 [Logic Functions](/l/zh/developers/extend/apps/logic/logic-functions) —— 尤其是 **Exposing a function as an AI tool or workflow action** 部分以及 `workflowActionTriggerSettings`。
</Tip>
### HTTP请求
作为工作流的一部分,向外部API发送请求。
<img src="/images/user-guide/workflows/http_action.png" style={{width:'100%'}} />
**配置**:
* 输入 API 端点 URL. 可以使用前面步骤中的参数。
* 选择 HTTP 方法(GET、POST、PUT、PATCH、DELETE)
* 添加必需的标头和值
* 提供示例响应以供结构预览
## AI 操作
### AI 代理 - 即将推出
在你的工作流中运行 AI 代理以执行智能任务。
**配置**:
* **代理**:选择一个现有 AI 代理或使用默认代理
* **提示**:为 AI 代理编写指令
* 在提示中引用前面步骤中的变量
**AI 代理能做什么**:
* 分析并总结数据
* 对记录进行分类或归类
* 生成文本内容
* 基于数据做出决策
* 使用工具与你的 CRM 数据交互
**输出**:AI 代理的响应可用于后续步骤。 如果代理具有结构化的输出模式,响应将遵循该格式。
<Note>
AI 代理操作会根据所用 AI 模型消耗工作流点数。 详见 [工作流点数](/l/zh/user-guide/workflows/capabilities/workflow-credits)。
</Note>
<Note>
AI 代理遵循基于角色的权限。 你可以在 **设置 → 成员 → 角色** 下为代理分配特定角色,以控制其可访问的数据。 详见 [权限](/l/zh/user-guide/permissions-access/capabilities/permissions)。
</Note>

View File

@@ -0,0 +1,66 @@
---
title: 工作流分支
description: 在你的工作流中创建并行路径和条件逻辑。
---
分支可以将你的工作流拆分为多个路径,这些路径可根据你的数据同时运行或按条件运行。
<img src="/images/user-guide/workflows/branches.png" style={{width:'100%'}} />
## 分支的工作原理
当你从单个节点创建多个连接时,每条路径都会成为一个分支。 默认情况下,**所有分支都会并行执行**——它们不会相互等待。
## 创建分支
### 添加新分支
1. **在主画布上右键单击**(在工作流中,不要在现有节点上)
2. 点击**添加节点**
3. 为你的新分支选择节点类型
4. 将箭头从上一步的底部拖到此新操作的顶部
5. 重复以上操作,从同一节点添加更多分支。
<Note>
每个分支都是独立的。 添加分支不会影响该节点的其他现有路径。
</Note>
### 可视化布局
在工作流编辑器中,分支显示为并行路径。 你可以拖动节点来重新排列可视化布局,而不会影响执行。
## 条件分支
由于默认情况下所有分支都会运行,请使用**筛选**节点来控制哪些路径实际执行:
| 分支 | 筛选条件 | 操作 |
| -- | --------- | ------ |
| A | 阶段 = "赢单" | 发送祝贺邮件 |
| B | 阶段 = "丢单" | 创建跟进任务 |
| C | 阶段 = "谈判" | 通知经理 |
1. 从你的触发器或操作创建分支
2. 在每个分支的第一步添加一个**筛选**节点
3. 为每个筛选节点配置互斥条件
4. 在每个筛选之后添加你的操作
只有满足筛选条件的分支才会继续执行。
## 合并分支
**分支不会自动合并。** 每个分支都会独立运行直至结束。 对此你有充分的灵活性:
* **选项 1:保持分支独立**
每个分支独立处理其后续操作。 当分支不需要汇合时,这是最简单的方法。
* **选项 2:手动合并分支**
在创建工作流时,你可以手动将多个分支连接到同一个下游操作。 只需将每个分支末端的箭头拖到同一个节点。
<Note>
虽然你可以使用[延迟](/l/zh/user-guide/workflows/capabilities/workflow-actions#delay)节点来暂停执行,但目前无法配置为等待"直到另一个分支结束"。
</Note>
## 相关
* [如何在工作流中使用分支](/l/zh/user-guide/workflows/capabilities/use-branches-in-workflows) - 分步指南
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions) - 可用操作(包括筛选)

View File

@@ -0,0 +1,75 @@
---
title: 工作流程积分
description: 了解工作流程积分的消耗与管理。
---
在 Twenty 中,工作流程积分为您的自动化提供支持。 了解其如何运作,有助于优化成本并有效管理您的自动化预算。
## 积分分配
工作流程积分基于您的计费周期分配,而非您的计划:
| 计费周期 | 积分 |
| -------- | -------- |
| **月度订阅** | 每月 5 积分 |
| **年度订阅** | 每年 50 积分 |
<Note>
每月 5 积分对于标准自动化而言已相当充足。 大多数团队在典型的工作流程使用中不会超出此限制。 额外积分主要用于高级 Code 操作和由 AI 驱动的工作流程。
</Note>
## 积分消耗工作原理
工作流程执行时消耗积分,而不是创建时。 每个工作流程操作根据其复杂性消耗积分:
### 按操作类型消耗积分
* **基本内部操作**:非常低的积分消耗
* 搜索记录
* 创建记录
* 更新记录
* 删除记录
* 表单操作
* **复杂操作**:更高的积分消耗
* 代码操作(JavaScript 执行)
* 向外部服务发送HTTP请求
* **AI 功能**:更高的积分消耗
* AI 代理操作会根据所用 AI 模型消耗积分
* 更复杂的提示词和更长的输出会消耗更多积分
* **延迟操作**:积分消耗极少
* Delay 节点在执行时会消耗**1 积分**
* 在等待期间**不会消耗积分**
* 5 分钟的延迟与 5 天的延迟消耗的积分相同
### 实时扣减
工作流程执行时,积分实时扣减。 这意味着:
* 草稿工作流程不消耗积分
* 仅激活并运行中的工作流程使用您的积分额度
* 失败的工作流程仍为已完成的步骤消耗积分
## 管理积分
### 检查积分使用情况
1. 前往**设置 → 结算**
2. 查看您当前的积分消耗和剩余余额
3. 监控使用模式以优化工作流程
### 购买额外的积分
如果您需要超出计划分配的更多积分:
1. 前往**设置 → 结算**
2. 点击选项以购买额外的积分。 提供不同大小的套餐。
3. 积分将添加到您当前的余额
## 最佳实践
* **批处理**:有效利用批量操作和迭代器操作,以减少单个操作的调用次数
* **手动触发优化**:对于手动触发,选择 `Bulk` 可用性以在单个工作流程运行中处理多个记录
* 优化代码操作以提高效率

View File

@@ -0,0 +1,93 @@
---
title: 工作流运行
description: 监控并管理工作流执行。
---
## 关于运行
**运行** 是一条工作流执行记录。 每当工作流被触发——无论是由记录事件、计划、手动操作还是 Webhook——都会创建一个新的运行。
## 查看运行
### 在工作流编辑器中
1. 打开你要监控的工作流
2. 单击右侧的**运行**面板
3. 查看最近的运行及其状态列表
### 在工作流运行视图中
1. 前往侧边栏中的**工作流运行**
2. 查看所有工作流的运行
3. 按状态、工作流或日期进行筛选
## 运行状态
| 状态 | 描述 |
| -------- | ------------------------------ |
| **正在运行** | 工作流当前正在执行 |
| **已完成** | 工作流已成功完成 |
| **失败** | 工作流遇到错误并已停止 |
| **等待中** | 工作流处于暂停状态(例如,等待 Delay 操作或表单提交) |
## 运行详情
单击任意运行以查看:
* **状态**:运行的当前状态
* **开始时间**:运行开始的时间
* **持续时间**:运行耗时
* **触发数据**:启动工作流的输入
* **步骤输出**:各步骤返回的数据
* **错误消息**:如果运行失败,出了什么问题
## 逐步执行
每次运行都会显示工作流中的执行过程:
1. 查看哪些步骤已成功完成
2. 确定失败发生的位置
3. 查看步骤之间传递的数据
4. 通过检查步骤的输入和输出来调试问题
## 错误处理
当运行失败时:
1. 打开失败的运行
2. 找到导致失败的步骤
3. 查看错误消息以了解详细信息
常见问题:
* 缺少必填字段
* 数据格式无效
* 外部 API 错误
* 权限问题
## 重新运行工作流
如果某次运行失败,你可以:
* 修复根本问题并等待下一次触发
* 对于手动工作流,使用相同或更新的数据再次触发
* 检查工作流逻辑以防止将来发生失败
## 性能提示
### 管理运行历史
* 运行会被保留以供历史参考
* 非常久远的运行可能会被自动归档
* 如果需要保留记录,请导出运行数据
### 监控最佳实践
* 在启用新的工作流后定期检查运行
* 审查失败的运行以识别模式
## 相关
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [工作流故障排除](/l/zh/user-guide/workflows/how-tos/need-more-help/workflow-troubleshooting)

View File

@@ -0,0 +1,137 @@
---
title: 工作流程触发器
description: 了解可启动你的工作流的不同触发器。
---
## 关于触发器
工作流程总是从一个单一触发器开始,定义自动化运行的条件。
<img src="/images/user-guide/workflows/workflow_triggers.png" style={{width:'100%'}} />
<Tip>
**支持高级对象!** 除了标准 CRM 对象(人员、公司、商机)之外,你还可以基于以下对象触发工作流并执行操作:
* 工作区成员
* 日历事件
* 消息(电子邮件)
* 任务、备注以及许多其他系统对象
这将带来强大的自动化能力,例如在创建日历事件时通知团队成员,或自动处理收到的电子邮件。
</Tip>
## 记录已创建
当在选定的对象(人员、公司、机会或任何自定义对象)中创建新记录时,启动工作流程。
**配置**:选择要监控新记录的对象类型。
<Note>
* 此触发器对于CSV、邮箱和日历同步或 API 创建的记录非常棒。
* **不建议用于手动创建的记录**:使用此触发器,记录一创建就会启动工作流。 因为二十个用户界面在运行时自动保存(没有编辑模式,然后进行验证以保存记录), 在用户输入所有字段之前,将触发工作流。
若要在手动创建的记录上触发此工作流,建议改用触发器 `Record is created or updated`。
</Note>
## 记录已更新
当对现有记录进行更改时,启动工作流程。
**配置**:
* 选择对象类型
* 可以选择指定要监控更改的字段
## 记录已更新或创建
当在选定的对象中创建或更新记录时,启动工作流程。
**重要性**:此触发器尤其有用,因为通过不同方法创建的记录行为不同:
* **API/CSV导入**:记录立即以填充所有字段的方式创建
* **手动创建**:首先创建记录,然后在随后的更新中添加字段
**配置**:
* 选择要监控的对象类型
* 可以选择指定要监控更改的字段
* 工作流程将在初始创建和后续更新时触发
## 记录已删除
当从对象中移除记录时,启动工作流程。
**配置**:选择要监控删除的对象类型。
## 手动触发器
当由用户操作触发时启动工作流程。 此触发器可以通过 `Cmd+K` 菜单访问,或在选择记录后通过顶部导航栏中显示的自定义按钮进行访问。
<img src="/images/user-guide/workflows/add_to_newsletter_button.png" style={{width:'100%'}} />
**可用性配置**:
选择工作流程如何处理记录选择:
* **全局**:无需记录即可触发此工作流程。 工作流是从命令菜单`Cmd+K`(来自任何对象)触发的,不使用记录作为输入。
因为**单点** 和 **批量** 可用性,所选记录将传递到您的工作流。 这是为给定对象配置的,在触发工作流之前可以选择几项记录。
* **单一** :工作流将从开始到结束,因为有记录被选中。
<Note>
**软限制:100 次/分钟**。 超过该限制后,工作流将保持在 "Not Started" 状态,并会逐步处理—由后台作业处理,或在另一个工作流进入队列时处理。 这意味着你可以通过一次触发选择超过 100 条记录;只是执行会更慢。
</Note>
* **批量**:工作流程将运行一次,提供完整的记录列表作为输入。 这意味着工作流需要包含一个[迭代器操作](/l/zh/user-guide/workflows/capabilities/workflow-actions#iterator)。
<Note>
这更为高级,最适合希望优化工作流运行次数的人。
</Note>
<img src="/images/user-guide/workflows/manual_trigger.png" style={{width:'100%'}} />
**附加配置**:
* 选择目标对象(用于单个和批量可用性)
* 为工作流程触发器选择一个命令图标
* 配置导航栏位置(已固定或未固定)
**访问方法**:
* 使用 `Cmd+K` 菜单查找并启动手动工作流
* 导航栏顶部的自定义按钮(如果已配置)
## 基于时间的触发器:按计划
在您定义的重复基础上启动工作流程。
**配置**:
* 选择时间单位(分钟、小时、天)
* 输入值或使用自定义 Cron 表达式进行高级调度
<Note>
**时区**:计划的工作流在 **UTC** 下运行。 为每日计划设置时间时,请将本地时间换算为 UTC。
</Note>
## 外部触发器:Webhook
当接收到外部服务的 GET 或 POST 请求时启动工作流程。
<img src="/images/user-guide/workflows/webhook_trigger.png" style={{width:'100%'}} />
**配置**:
* 该工作流会提供唯一的 webhook URL—复制它并将其添加到你的外部系统,作为调用的端点。
* 对于 POST 请求,定义预期的正文结构,以便 Twenty 知道将接收哪些数据。 在此添加你将接收的字段,这些字段将在下方的工作流中使用。
* 配置认证(即将推出)。
## 选择合适的触发器
| 用例 | 推荐的触发器 |
| --------- | ------------ |
| 新线索需要处理 | 记录已创建 |
| 数据变更需要同步 | 记录已更新 |
| 导入/手动数据处理 | 记录已更新或创建 |
| 删除后的清理 | 记录已删除 |
| 用户发起的操作 | 手动启动 |
| 定期报告 | 计划 |
| 外部集成 | Webhook 或按计划 |

View File

@@ -0,0 +1,84 @@
---
title: 工作流版本
description: 管理工作流的版本和草稿。
---
## 关于版本
每次您激活一个工作流时,都会创建一个新版本。 这使您能够随时间跟踪更改,并在需要时恢复到先前的配置。
## 版本状态
| 状态 | 描述 |
| ------- | --------------- |
| **草稿** | 正在编辑,尚未发布 |
| **活跃** | 运行中的版本,响应触发器 |
| **已停用** | 之前为活跃状态,但已被手动停止 |
| **已归档** | 为历史记录保留的过去版本 |
## 处理草稿
当您编辑活跃的工作流时,您的更改将保存为**草稿**。 在您进行更新时,活跃版本会继续运行。
完成编辑后,您可以:
* **激活**:将草稿发布为新的活跃版本(先前的版本会被归档)
* **丢弃**:删除草稿并保留当前的活跃版本
## 版本历史
### 查看历史版本
1. 打开该工作流
2. 点击**版本**选项卡
3. 查看包含时间戳的所有先前版本
### 还原版本
1. 找到您要还原的版本
2. 点击**用作草稿**
3. 该版本会被复制为一个新的草稿
4. 进行任何必要的更新
5. 准备就绪后激活
## 最佳实践
### 版本管理
* 仅在准备好用于生产环境时再激活
* 在各版本之间仅保留有意义的更改
* 记录工作流名称或描述中的重大更改
* 在激活前先在草稿模式下进行测试
### 还原更改
* 如果新版本引发问题,请还原到上一版本
* 使用版本历史来跟踪发生了哪些更改
* 在激活之前务必测试已还原的版本
## 常见工作流
### 快速编辑
1. 对活跃的工作流进行小幅更改
2. 在草稿模式下测试
3. 激活新版本
### 重大修订
1. 以先前版本为起点
2. 在草稿中进行重大更改
3. 全面测试所有场景
4. 确认无误后再激活
### 回滚
1. 找出现有版本的问题
2. 在历史记录中找到最后一个可用的版本
3. 点击**用作草稿**
4. 激活以恢复原有行为
## 相关内容
* [工作流入门](/l/zh/user-guide/workflows/overview)
* [工作流运行](/l/zh/user-guide/workflows/capabilities/workflow-runs)

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>

View File

@@ -0,0 +1,81 @@
---
title: 工作流
description: 了解如何在 Twenty 中构建自动化流程。
---
## 为何工作流很重要
Twenty的设计宗旨是为用户提供最大的灵活性。 工作流可以让您自行构建自动化功能,打造最能支持您独特业务用例的CRM,而不是强迫您将业务流程迁就于僵化的预设功能。
工作流是Twenty应用内构建这些自动化功能的特色。 它们为您提供了创建企业所需模块的基础,无论何时都能满足需求。
## 我可以用工作流做什么?
我们建议将自动化用于两个主要目的:
1. **内部自动化以便日常团队工作**:减少手动输入和重复任务,这些任务会减缓团队的速度。
2. **引入和导出Twenty的数据**:通过API调用和Webhooks将Twenty与您的数据库和其他工具连接。
## 构建您的第一个工作流
### 步骤 1:创建新工作流
1. 前往位于其他对象下方的 **工作流**
2. 点击 **+ 新建记录**
3. 为您的工作流命名
### 步骤 2:添加触发器
每个工作流都从一个触发器开始。 从以下选项中选择:
* **记录事件**:当记录被创建、更新或删除时
* **计划**:在特定时间运行(每天、每周等)
* **手动**:由用户操作触发
* **Webhook**:由 webhook 触发
<img src="/images/user-guide/workflows/workflow_triggers.png" style={{width:'100%'}} />
### 步骤 3:添加操作
在触发器之后,添加一个或多个操作:
* **创建记录**:向任何对象添加新记录
* **更新记录**:修改现有记录数据
* **删除记录**:从对象中移除记录
* **搜索记录**:查找符合条件的记录
* **插入或更新记录**:基于匹配条件创建或更新
* **迭代器**:遍历记录数组
* **过滤器**:控制哪些记录继续执行
* **延迟**:在继续之前等待(持续时间或计划日期)
* **发送电子邮件**:通过已连接的账户发送电子邮件
* **代码**:运行自定义 JavaScript
* **HTTP 请求**:调用外部 API
* **表单**:在执行时于 Twenty UI 内获取用户输入
* **AI 代理**(即将推出):运行智能 AI 任务
<img src="/images/user-guide/workflows/workflow_actions.png" style={{width:'100%'}} />
### 步骤 4:测试并启用
1. 使用 **Test** 按钮,以示例数据运行您的工作流
2. 检查结果以确保其按预期工作
3. 准备就绪后,将工作流切换为 **Active**
## 工作流最佳实践
* **编辑步骤名称**:重命名您的工作流步骤,以明确描述每个步骤的作用。 这有助于维护,并使工作交接给同事更轻松
* **利用前一步的数据**:您可以使用由工作流程中任何前一步返回的记录字段
* **从简单开始**:从基础工作流开始,并随着对系统的熟悉,随着时间的推移增加复杂性
* **在构建之前计划**:在开始构建之前,绘制出您的工作流逻辑,以避免半途而废
## 下一步
* [工作流触发器](/l/zh/user-guide/workflows/capabilities/workflow-triggers)
* [工作流操作](/l/zh/user-guide/workflows/capabilities/workflow-actions)
* [CRM 自动化](/l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations)
## 需要帮助?
<Tip>
需要帮助来实现工作流自动化吗? [查找认证的20个伙伴](https://twenty.com/partners/list?categories=SOLUTIONING\&ref=docs-workflows) 来设计和构建您的端到端自动化。
</Tip>