An AI Voice Agent Is Just Two Webhooks: Wiring Retell to n8n in Production

An AI Voice Agent Is Just Two Webhooks: Wiring Retell to n8n in Production

AI 语音代理其实就是两个 Webhook:在生产环境中连接 Retell 与 n8n

A voice agent that answers the phone is the easy 20 percent. The interesting 80 percent is everything the call is supposed to trigger: check the calendar, write the CRM contact, send the confirmation text, tag the hot lead. New developers building their first voice agent almost always try to cram that logic into the voice prompt, and it works in the demo and falls apart in production. 一个能接听电话的语音代理只占整个工作的 20%,而那有趣的 80% 是通话所触发的一切后续动作:查询日历、录入 CRM 联系人、发送确认短信、标记潜在客户。刚开始构建语音代理的开发者几乎总是试图将这些逻辑塞进语音提示词(Prompt)中,这在演示时或许有效,但在生产环境中却会彻底崩溃。

I build AI voice agents for US clients, and the question I get most from other devs is some version of “okay, the agent talks, but how does it actually do anything?” In my stack the answer is always the same: Retell handles the conversation, n8n handles everything the conversation triggers. This is the exact integration I ship, including the parts that break once real calls start hitting it. 我为美国客户构建 AI 语音代理,其他开发者问我最多的问题通常是:“好吧,代理能说话了,但它到底怎么执行任务呢?”在我的技术栈中,答案始终如一:Retell 负责对话,n8n 负责对话触发的一切。这就是我交付的完整集成方案,包括那些在真实通话开始后容易出错的部分。

The mental model: two webhooks pointing in opposite directions

思维模型:两个指向相反方向的 Webhook

Almost everyone overcomplicates this. The entire integration is two arrows: 几乎每个人都把这个问题复杂化了。整个集成其实就是两个箭头:

  • Retell to n8n: the agent tells n8n what happened (call lifecycle events) and asks n8n questions mid-call (custom functions).
  • Retell 到 n8n: 代理告诉 n8n 发生了什么(通话生命周期事件),并在通话中向 n8n 提问(自定义函数)。
  • n8n to Retell: n8n tells Retell to do something (place an outbound call, update an agent, fetch a transcript).
  • n8n 到 Retell: n8n 指示 Retell 执行操作(拨打外呼电话、更新代理配置、获取通话记录)。

Get those two directions right and everything else is composition. The reason I keep the logic in n8n and not in the prompt is the same reason you do not hardcode business rules into your view layer: if you bake booking logic into the voice prompt, you have married the platform. Keep it in n8n and the voice layer becomes swappable. Switching or A/B testing platforms later is a change to one webhook, not a rewrite. 搞定这两个方向,剩下的就只是组合问题了。我将逻辑保留在 n8n 而非提示词中的原因,和你不在视图层(View Layer)硬编码业务规则的原因是一样的:如果你将预约逻辑嵌入语音提示词,你就被该平台绑定了。将其保留在 n8n 中,语音层就可以随时替换。以后切换平台或进行 A/B 测试时,只需修改一个 Webhook,而无需重写代码。

Direction 1: Retell to n8n (the inbound webhook)

方向 1:Retell 到 n8n(入站 Webhook)

This is where most of the work lives. Retell can call your webhook on lifecycle events and on custom functions you define inside the agent. Set up the n8n side first. Add a Webhook node. Method POST, and copy the production URL it generates. If you self-host, the URL has to be publicly reachable, because Retell’s servers hit it directly, so localhost will not work. For local dev I tunnel with ngrok and swap the URL for the real one before going live. 这是大部分工作所在的地方。Retell 可以在生命周期事件和你在代理内部定义的自定义函数上调用你的 Webhook。首先设置 n8n 端:添加一个 Webhook 节点,方法设为 POST,并复制生成的生产环境 URL。如果你是自托管的,该 URL 必须是公网可访问的,因为 Retell 的服务器会直接请求它,所以 localhost 是行不通的。在本地开发时,我使用 ngrok 进行隧道映射,并在上线前将其替换为真实 URL。

Add a Respond to Webhook node so Retell gets a clean 200. For mid-call functions this response is the answer the agent speaks back, so it matters more than it looks. Point Retell at it. In the agent settings, set the webhook URL to the n8n production URL. Retell now POSTs a JSON body on each event. The payload carries an event field (call_started, call_ended, call_analyzed) and the full call object with the transcript, metadata, and any dynamic variables you passed in. 添加一个“响应 Webhook”(Respond to Webhook)节点,以便 Retell 收到 200 状态码。对于通话中的函数调用,此响应就是代理要回复的内容,因此它比看起来更重要。在代理设置中,将 Webhook URL 指向 n8n 的生产环境 URL。现在,Retell 会在每个事件发生时 POST 一个 JSON 主体。载荷包含一个事件字段(如 call_started, call_ended, call_analyzed)以及完整的通话对象,包括通话记录、元数据和你传入的任何动态变量。

A habit that pays off: put a Switch node right after the webhook that branches on {{ $json.event }}. Each event wants different handling. call_ended is usually where I write the contact and transcript to the CRM. call_analyzed is where I read Retell’s post-call analysis (sentiment, whether a booking happened, custom extraction fields) and route on it. 一个值得养成的习惯:在 Webhook 之后立即放置一个 Switch 节点,根据 {{ $json.event }} 进行分支处理。每个事件都需要不同的处理方式。call_ended 通常是我将联系人和通话记录写入 CRM 的地方;call_analyzed 则是读取 Retell 通话后分析(情绪、是否预约成功、自定义提取字段)并进行后续路由的地方。

The mid-call custom function

通话中的自定义函数

This is the powerful part. Inside the agent you define a custom function, say check_availability or book_appointment. When the caller asks about a Tuesday slot, the agent calls that function, which hits your n8n webhook mid-conversation, waits, and speaks the result back. 这是最强大的部分。在代理内部,你可以定义一个自定义函数,比如 check_availability(检查空闲时间)或 book_appointment(预约)。当来电者询问周二的时间段时,代理会调用该函数,触发你的 n8n Webhook,等待并播报结果。

The constraint that will bite you here is latency. The caller is sitting in silence while n8n runs. Keep mid-call workflows lean: one lookup, a fast response, no chained API calls that eat four seconds. If a step is genuinely slow, have the agent say “give me one moment” and design around it. Anything non-blocking, like sending the confirmation SMS or writing the CRM note, does not belong in the mid-call path. Move it to the call_ended branch. 这里会遇到的限制是延迟。当 n8n 运行时,来电者会处于沉默状态。保持通话中工作流的精简:一次查询、快速响应,不要进行耗时四秒的链式 API 调用。如果某个步骤确实很慢,让代理说“请稍等片刻”并围绕此进行设计。任何非阻塞的操作,如发送确认短信或编写 CRM 备注,都不应放在通话中路径,请将其移至 call_ended 分支。

Direction 2: n8n to Retell (the outbound call)

方向 2:n8n 到 Retell(外呼电话)

The reverse direction uses Retell’s REST API. The most common use is placing an outbound call, which is the backbone of any missed-call callback system. In n8n, an HTTP Request node: POST to Retell’s create-phone-call endpoint. Authorization header with your Retell API key. Store it in n8n credentials, never inline in the node. This is the single most common mistake I see in shared workflows, an API key sitting in plaintext in an exported JSON. 反向操作使用 Retell 的 REST API。最常见的用途是拨打外呼电话,这是任何漏接电话回拨系统的核心。在 n8n 中使用 HTTP Request 节点:POST 到 Retell 的 create-phone-call 端点。授权头使用你的 Retell API 密钥。请将其存储在 n8n 的凭据(Credentials)中,永远不要直接写在节点里。这是我在共享工作流中看到的最常见的错误:API 密钥以明文形式出现在导出的 JSON 文件中。

Body: the from_number (your registered Twilio number), the to_number (the lead), the agent_id, and a retell_llm_dynamic_variables object carrying anything the agent should know before it dials: the caller’s name, what they enquired about, the business name. Those dynamic variables are how you personalize a call without editing the agent. The prompt references {{customer_name}}, n8n fills it at call time. One agent, infinitely reusable across contacts. 主体包含:from_number(你注册的 Twilio 号码)、to_number(潜在客户)、agent_id,以及一个 retell_llm_dynamic_variables 对象,携带代理拨号前需要知道的一切信息:来电者姓名、咨询内容、公司名称。这些动态变量是你无需修改代理即可实现个性化通话的方式。提示词引用 {{customer_name}},n8n 在拨号时填充它。一个代理,即可在不同联系人之间无限复用。

The full loop, end to end

完整的闭环

Here is how the two directions combine in a real missed-call callback: 以下是这两个方向在真实的漏接电话回拨场景中如何结合:

  1. Missed call in GHL -> GHL webhook -> n8n

  2. n8n HTTP Request -> Retell create-phone-call (lead name + context)

  3. Retell dials the lead, agent talks

  4. Mid-call: agent calls book_appointment -> n8n webhook -> GHL calendar -> responds

  5. call_ended -> Retell webhook -> n8n -> n8n writes transcript + outcome to the GHL contact, sends SMS confirmation

  6. GHL 漏接电话 -> GHL Webhook -> n8n

  7. n8n HTTP Request -> Retell create-phone-call(客户姓名 + 上下文)

  8. Retell 拨打客户电话,代理开始对话

  9. 通话中:代理调用 book_appointment -> n8n Webhook -> GHL 日历 -> 返回结果

  10. call_ended -> Retell Webhook -> n8n -> n8n 将通话记录和结果写入 GHL 联系人,发送确认短信

Every arrow in that diagram is one of the two webhook directions above. Nothing exotic. Just discipline about where each piece of logic lives. 该流程图中的每一个箭头都是上述两个 Webhook 方向之一。没什么花哨的,关键在于严格管理每一段逻辑所处的位置。

Production gotchas

生产环境的注意事项

  • A2P 10DLC: If your flow sends SMS through Twilio, unregistered traffic gets silently filtered. Register the brand and campaign before you test, or you will lose a day debugging texts that never arrive and were never going to.
  • A2P 10DLC: 如果你的工作流通过 Twilio 发送短信,未注册的流量会被静默过滤。在测试前注册品牌和活动,否则你会浪费一天时间去调试那些永远不会到达的短信。
  • Idempotency on call_ended: Retell can, in rare cases, deliver an event more than once. Guard CRM writes with the call_id so a duplicate event does not create a duplicate contact.
  • call_ended 的幂等性: 在极少数情况下,Retell 可能会多次发送同一个事件。请使用 call_id 对 CRM 写入操作进行防护,以确保重复的事件不会创建重复的联系人。