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.
The mental model: two webhooks pointing in opposite directions
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).
- n8n to Retell: n8n tells Retell to do something (place an outbound call, update an agent, fetch a transcript).
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.
Direction 1: Retell to n8n (the inbound 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
localhostwill not work. For local dev I tunnel with ngrok and swap the URL for the real one before going live. - 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.
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?"
Enter fullscreen mode Exit fullscreen mode
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.
Direction 2: n8n to Retell (the outbound call)
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.
- Body: the
from_number(your registered Twilio number), theto_number(the lead), theagent_id, and aretell_llm_dynamic_variablesobject 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.
The full loop, end to end
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
Enter fullscreen mode Exit fullscreen mode
Every arrow in that diagram is one of the two webhook directions above. Nothing exotic. Just discipline about where each piece of logic lives.
Production gotchas I have actually hit
- A2P 10DLC first. 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.
-
Idempotency on
call_ended. Retell can, in rare cases, deliver an event more than once. Guard CRM writes with thecall_idso a duplicate event does not create a duplicate contact. Treat webhooks as at-least-once, like you would any queue. - Secure the webhook. Verify Retell's signature, or at minimum put a shared secret in a header, so random POSTs cannot trigger your workflow. An open n8n webhook that places outbound calls is a genuine liability, not a hypothetical one.
- Separate test and production webhooks. n8n gives you a test URL and a production URL and they are not the same endpoint. I have watched people wire the test URL into a live agent and wonder why nothing fires after they close the editor.
- Log the raw payload. Early in a build I write every incoming Retell payload to a Sheet or a database node. When something misbehaves in week two, the raw event history is the fastest way to see what the agent actually sent versus what I assumed it sent.
Where this goes from here
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.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.