Require human approval before your agent sends email
Require human approval before your agent sends email
在你的 AI Agent 发送邮件前,请务必加入人工审核环节
Most “AI email agent” demos end with a triumphant send. The model writes a reply, the code POSTs it, and a real message lands in a real stranger’s inbox. That’s a great demo and a terrible production default. 大多数“AI 邮件代理”的演示都以成功发送告终。模型写好回复,代码执行 POST 请求,一封真实的邮件便落入了陌生人的收件箱。这作为演示很棒,但作为生产环境的默认设置却非常糟糕。
The moment your agent can send mail with nobody watching, you’ve handed an LLM a corporate email address and the standing authority to use it. One hallucinated price, one confidently wrong refund promise, one apology to the wrong customer, and you’re explaining to legal why a bot signed an email as your company. 一旦你的 Agent 可以在无人监管的情况下发送邮件,你就等于把公司的邮箱地址和使用权限交给了大语言模型(LLM)。只要出现一个幻觉产生的价格、一个自信但错误的退款承诺,或者向错误的客户发送了道歉信,你就得去向法务部门解释为什么一个机器人会以公司的名义签署邮件。
There’s a boring, durable fix that predates AI by decades: don’t send — draft. Stage the message, put a human in front of it, and only send once someone with a name and a pulse approves. Email systems have had a “Drafts” folder forever for exactly this reason. 有一个比 AI 早出现几十年的、枯燥但有效的解决方案:不要直接发送,而是先存为草稿。将邮件暂存,由人工进行审核,只有当一个有血有肉的人批准后才发送。电子邮件系统之所以一直存在“草稿箱”文件夹,正是为了这个目的。
The Nylas Drafts API turns that folder into something better — an approval queue your agent writes into and your reviewers drain. This post builds that queue. The agent creates a draft, a human reviews the pending drafts, and an approved draft gets sent byte-for-byte unchanged. No re-rendering, no “the agent regenerates it on approval” race where the thing you approved isn’t the thing that ships. Nylas Drafts API 将草稿箱变成了一个更强大的工具——一个由你的 Agent 写入、由审核员处理的审批队列。本文将教你如何构建这个队列。Agent 创建草稿,人工审核待处理草稿,批准后的草稿将原封不动地发送出去。没有重新渲染,也不会出现“Agent 在批准后重新生成内容”导致的竞态条件,确保你批准的内容就是最终发出的内容。
I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for, and I’ll pair every one with the raw curl so you can wire it into a backend in whatever language you like. This is deliberately not about escalating inbound threads to a human (that’s a different problem, where the trigger is a message arriving). Here the trigger is the agent wanting to send, and the gate sits on the outbound path. 我负责 Nylas CLI 的开发,因此下文中的终端命令是我平时最常用的。我还会为每个命令提供原始的 curl 请求,以便你可以将其集成到任何你喜欢的后端语言中。本文特意不讨论将收到的邮件升级给人工处理(那是另一个问题,触发条件是收到消息),这里的触发条件是 Agent 想要发送邮件,而关卡设置在出站路径上。
Why a draft is the right approval primitive
为什么草稿是实现审批的最佳原语
You could build approval a dozen ways. You could buffer the agent’s output in a queue table and call send later. You could stash a JSON blob in Redis. Both work, and both quietly reinvent something the email stack already gives you. 你可以通过多种方式构建审批流程。你可以将 Agent 的输出缓冲到队列表中,稍后再调用发送;也可以将 JSON 数据存入 Redis。这两种方法都可行,但它们实际上是在重复造轮子,而电子邮件系统本身已经提供了现成的功能。
A draft is a real, persisted email object, on the mailbox, with a stable id. That buys you three things a homegrown buffer doesn’t: 草稿是一个真实的、持久化的邮件对象,存储在邮箱中,并拥有一个稳定的 ID。这为你提供了自建缓冲区所不具备的三个优势:
-
It survives your process. Restart the worker, redeploy, fail over — the pending message is still sitting on the mailbox, not lost in a memory queue.
-
它不受进程影响。 重启工作进程、重新部署或发生故障时,待处理的邮件依然保存在邮箱中,不会在内存队列中丢失。
-
A human can see it where humans already look. The draft lives in the Drafts folder of the agent’s mailbox. A reviewer can open it in a normal mail client (Agent Accounts support IMAP/SMTP) instead of squinting at a JSON preview in your admin panel.
-
人工可以在他们习惯的地方查看。 草稿存放在 Agent 邮箱的“草稿箱”中。审核员可以在普通的邮件客户端(Agent 账户支持 IMAP/SMTP)中打开它,而不必在你的管理后台盯着 JSON 预览看。
-
Approval becomes a single, atomic action. Sending an approved draft is one API call against the draft’s id. You don’t reassemble recipients, subject, body, and attachments from a buffer and hope you got it right — the object you approved is the object that sends.
-
审批成为一个单一的原子操作。 发送已批准的草稿只需针对草稿 ID 调用一次 API。你不需要从缓冲区重新组装收件人、主题、正文和附件并祈祷不出错——你批准的对象就是最终发送的对象。
That last point is the whole game. The contract you want is “what the human saw is what went out.” A draft gives you that for free, because the send action references the stored draft rather than re-supplying the content. 最后一点是核心所在。你想要的契约是“人工看到什么,就发出什么”。草稿功能免费为你提供了这一点,因为发送操作引用的是已存储的草稿,而不是重新提供内容。
The grant is the only abstraction you need
Grant 是你唯一需要的抽象概念
Everything below runs against a grant — a grant_id that represents one mailbox. If you’ve used Nylas with a connected Gmail or Microsoft account, an Agent Account is the same thing: a grant. Same /v3/grants/{grant_id}/* endpoints, same auth header, same payloads. There’s nothing new to learn on the data plane. 下文中的所有操作都基于一个 grant(授权)——即代表一个邮箱的 grant_id。如果你曾使用 Nylas 连接过 Gmail 或 Microsoft 账户,那么 Agent 账户本质上是一样的:它就是一个 grant。使用相同的 /v3/grants/{grant_id}/* 端点、相同的认证头和相同的负载。在数据层面无需学习任何新东西。
The difference is provisioning — an Agent Account is a Nylas-hosted mailbox you create on demand, with no OAuth dance and no human to click “Allow.” That matters here because the mailbox is the agent. When the agent drafts from support@yourapp.nylas.email, the draft sits in that account’s own Drafts folder, and the eventual send comes from that address. The reviewer is approving mail “from the bot,” which is exactly the identity you want under human control. 区别在于配置——Agent 账户是你按需创建的、由 Nylas 托管的邮箱,无需繁琐的 OAuth 流程,也无需人工点击“允许”。这一点很重要,因为邮箱本身就是 Agent。当 Agent 从 support@yourapp.nylas.email 创建草稿时,草稿会存放在该账户的草稿箱中,最终发送也通过该地址发出。审核员批准的是“来自机器人”的邮件,这正是你希望在人工控制下的身份。
One honest caveat up front, because it shapes the design: Agent Accounts don’t support custom metadata. You can’t tag a draft with {“approval_status”: “pending”} and filter on it server-side. So the approval decision itself — who approved, when, the state machine — lives in your database. Nylas stores the message; your app stores the verdict. I’ll show where that seam falls. 提前说明一个注意事项,因为它会影响设计:Agent 账户不支持自定义元数据。你不能给草稿打上 {“approval_status”: “pending”} 标签并在服务端进行过滤。因此,审批决定本身——谁批准的、何时批准、状态机逻辑——都必须存储在你的数据库中。Nylas 存储邮件,你的应用存储审批结果。我稍后会展示如何处理这一衔接。
Before you begin
开始之前
You need: 你需要:
- A Nylas API key from the dashboard, exported as NYLAS_API_KEY.
- 从仪表板获取 Nylas API 密钥,并导出为 NYLAS_API_KEY。
- A registered sending domain, or a free *.nylas.email trial subdomain. New domains warm up over roughly four weeks, so provision early.
- 一个已注册的发送域名,或免费的 *.nylas.email 测试子域名。新域名通常需要约四周的预热期,请尽早配置。
- The Nylas CLI if you want the terminal path. Run nylas init once to store your API key.
- 如果你想使用终端操作,请安装 Nylas CLI。运行一次 nylas init 来存储你的 API 密钥。
Examples use the US data region host https://api.us.nylas.com. Swap in api.eu.nylas.com if your app lives in the EU. 示例使用美国数据中心主机 https://api.us.nylas.com。如果你的应用位于欧盟,请替换为 api.eu.nylas.com。
Provision the agent’s mailbox
配置 Agent 邮箱
The mailbox the agent drafts from is an Agent Account. Create one with POST /v3/connect/custom, using the built-in nylas provider and an address on your domain: Agent 创建草稿所使用的邮箱是一个 Agent 账户。使用 POST /v3/connect/custom 创建一个,使用内置的 nylas 提供商和你域名下的地址:
curl --request POST \
--url 'https://api.us.nylas.com/v3/connect/custom' \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"provider": "nylas",
"name": "Support Bot",
"settings": {
"email": "support@yourapp.nylas.email"
}
}'
The response includes the grant_id you’ll use for everything else. The API also auto-creates a default workspace and policy for the account, so there’s no extra setup to send mail. 响应中包含你后续所有操作所需的 grant_id。API 还会自动为该账户创建默认的工作区和策略,因此无需额外设置即可发送邮件。
The CLI collapses all of that into one command: CLI 将上述所有步骤简化为一个命令:
nylas agent account create support@yourapp.nylas.email --name "Support Bot"
No —workspace flag exists on create, and there’s no refresh token to manage — that’s the OAuth-free part. If you later want to attach a stricter custom policy (say, to cap inbound size or block recipients), you do that against the auto-created workspace with nylas workspace update
export NYLAS_GRANT_ID="<grant-id-from-output>"
Step 1 — the agent drafts instead of sends
第一步:Agent 创建草稿而非直接发送
Here’s the core inversion. Your agent’s “send” tool does not call the send-message endpoint. It calls create draft. The message gets composed, persisted, and parked — but nothing leaves the mailbox. Create a draft with POST /v3/grants/{…} 这是核心的逻辑反转。你的 Agent 的“发送”工具不再调用 send-message 端点,而是调用 create draft。邮件会被撰写、持久化并暂存——但不会离开邮箱。使用 POST /v3/grants/{…} 创建草稿。