snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,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)