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.
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.
Almost everyone overcomplicates this. The entire integration is two arrows:
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.
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.
localhost
will not work. For local dev I tunnel with ngrok and swap the URL for the real one before going live.200
. For mid-call functions this response 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.
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.
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:
Caller: "Do you have anything Tuesday afternoon?"
-> Retell fires custom function check_availability -> n8n webhook
-> n8n queries Google Calendar / GHL calendar
-> n8n responds { "slots": ["2pm", "3:30pm"] }
-> Agent: "I have 2pm or 3:30 on Tuesday, which works?"
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.
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:
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. If you have ever templated an email in code, it is the same idea pointed at a phone call.
Here is how the two directions combine in a real missed-call callback:
Missed call in GHL -> GHL webhook -> n8n
-> n8n HTTP Request -> Retell create-phone-call (lead name + context)
-> Retell dials the lead, agent talks
-> mid-call: agent calls book_appointment -> n8n webhook -> GHL calendar -> responds
-> call_ended -> Retell webhook -> n8n
-> n8n writes transcript + outcome to the GHL contact, sends SMS confirmation
Every arrow in that diagram is one of the two webhook directions above. Nothing exotic. Just discipline about where each piece of logic lives.
call_ended
.call_id
so a duplicate event does not create a duplicate contact. Treat webhooks as at-least-once, like you would any queue.Once the two directions are solid, everything else is just adding nodes: swap the CRM, fire a Slack alert when a caller sounds angry, branch on the post-call analysis to tag hot leads. The voice agent stays dumb and simple, and the intelligence lives in n8n where you can see it, test it, and change it without ever touching the prompt. That separation is the whole trick, and it is why I have never had to rebuild a client's system just because they wanted to try a different voice platform.
I write up the rest of this stack, platform choice, the testing pipeline, and what these agents cost to run, over on my blog. If you are wiring your first one together, start with the two arrows and resist the urge to be clever inside the prompt.