
Retell post-call analysis gets into your CRM cleanly when you do three things: define typed custom fields (booleans and selectors) instead of asking the summary to list details, listen to the call_analyzed webhook rather than call_ended, and upsert contacts on the caller's phone number while treating empty strings as missing. You can do that with Retell's native CRM sync or with a webhook into n8n. Native sync is fine for one CRM and simple mappings. n8n wins once you need routing, deduplication, or more than one destination.
We didn't want to write this from the docs alone. So on October 17, 2026 we pulled every call on our Retell workspace through the API: 389 calls from October 2025 to October 2026, across 18 agents, inbound and outbound, English and Spanish. Some of what came back matched the docs. Some of it didn't, and those gaps are exactly what breaks CRM syncs.
What does Retell post-call analysis actually return?
Retell renamed the feature to Post Call Extraction, but the API object is still call_analysis. After a call ends, an LLM reads the transcript and writes these fields:
call_summary: a short paragraph about the call. Its prompt is editable.call_successful: true or false against success criteria you can customize.user_sentiment: Positive, Negative, Neutral or Unknown. Retell's docs note this is inferred from the transcript text, not from tone of voice.in_voicemail: whether the agent reached voicemail.custom_analysis_data: your own fields, typed as boolean, selector, number or text.
The official Post Call Extraction overview also says custom fields aren't populated for calls that didn't connect. Our data confirmed that: of 389 calls, only the ones with a real conversation and a custom schema had anything in custom_analysis_data.
The built-in fields are for humans skimming a call log. The custom fields are what your CRM should run on.
Why you shouldn't parse details out of the call summary
Our first inbound screening agent did what a lot of teams do. We rewrote the call_summary prompt so it produced a line like "Caller name: X, Company: Y, Reason: Z, Callback number: N", and then pulled values out of that text in n8n.
Here's what 81 real calls on that agent returned for the caller's name:
- 22 calls had an actual name.
- 27 calls said "not provided".
- 26 calls said "unknown".
- 6 calls ignored the format entirely and wrote a normal sentence.
So a parser had to handle at least three spellings of "missing", plus capitalization changes, plus free-form prose on 7% of calls. One summary even listed the caller's company as the business being called. None of that is the model being bad. A summary is prose, and prose drifts.
If a value is going to drive a workflow, it needs its own typed field, not a label inside a paragraph.
What changed when we switched to typed custom fields
The next version of the same agent used eight custom fields: caller_name, caller_company, callback_number and reason as text, urgency and language as selectors, and is_spam and needs_callback as booleans. It produced 67 analyzed calls. The difference was clear:
- Booleans and selectors came back on 67 of 67 calls. Every value was one of the allowed choices.
is_spamwas true on 4 calls,needs_callbackon 11,urgencysplit 51 low, 15 medium and 1 high. - Text fields were often blank.
reasonwas filled on 59 calls,caller_nameon 27,callback_numberon 23 andcaller_companyon 20. - Blank meant an empty string, never null. Our field descriptions literally said "Null if they never gave a name." The model returned
""every time: 40 times for name, 44 for callback number, 47 for company. Zero nulls. - The callback number was mostly redundant. Of the 23 calls with a callback number, 21 just repeated the caller ID. Only 2 gave a genuinely different number.
That gives you a few practical rules. Normalize "" to null before you write anything, or you'll overwrite a good CRM value with a blank. Use the call's from_number as the contact key and treat an extracted callback number as an extra field. And route on booleans and selectors, because those are the fields that actually come back every time.
Retell's own docs back up the selector advice: put the explanation of each choice in the description and keep the choices list to the bare values. Our urgency description defines what high, medium and low mean, and the field never returned anything off-list.
Which built-in fields can you trust for automation?
call_successful. It was true on 101 of 389 calls. The 91 calls that never connected (outbound dials that didn't pick up, for example) still came back with a full call_analysis object: success false, sentiment Unknown and an empty summary. If your workflow fires "unsuccessful call" follow-ups, it will fire on every dial that never rang through unless you also check call_status. We also found a 5-second inbound call, summarized as having no meaningful interaction, marked as successful.
in_voicemail. On an inbound call-screening agent, 38 of 81 calls were flagged as voicemail. On an inbound line our agent is the one answering, so the flag is really describing robocalls and callers who read a message at it and hang up. It's useful on outbound. On inbound, don't filter on it. If voicemail handling on outbound is your problem, we covered that in our voicemail detection post.
user_sentiment. Across 389 calls: 246 Neutral, 105 Unknown, 33 Positive, 5 Negative. That's fine for a dashboard trend. It's far too flat to trigger anything.
call_summary. The median summary was 27 words. It's perfect as a CRM note and useless as a data source.
Should you use Retell's native CRM sync or a webhook into n8n?
Retell now has native CRM integrations for Salesforce, HubSpot, Dynamics 365, GoHighLevel and Zoho. Contacts sync every five minutes, matched on phone number, and you can map extraction fields onto contact fields with three update modes: Overwrite, Fill if empty, or Merge, which combines old and new values with an LLM at $0.005 per merge according to the docs.
Native sync is the right call when:
- You have one CRM and it's on that list.
- Your fields map one to one onto contact properties.
- You don't need conditional logic, like "don't create a contact if this was spam".
A webhook into n8n is the right call when:
- Different agents use different schemas. Retell configures mappings per workspace, not per agent. We had one agent whose fields didn't match the others at all. In n8n you route on
agent_idand map each schema on its own terms. - The call should do more than update a contact. A callback request might need a CRM task, a Slack ping and an offline conversion to your ad platform, which is the same pipeline we use for voice AI lead qualification.
- You need to clean data first. Empty-string normalization, spam filtering and deduplication all happen before the CRM ever sees the record.
For most agencies we work with, the answer ends up being n8n, mostly because of the second point.
How to send Retell post-call analysis into a CRM with n8n
This is the workflow shape we run in production.
1. Subscribe the agent to call_analyzed only
Set the agent's webhook_events to call_analyzed. Retell's webhook docs are explicit that call_ended excludes call_analysis, so reading summary or custom fields from it gets you nulls. Subscribing to all three default events just triples your executions.
2. Verify the signature and answer fast
Retell signs requests with an X-Retell-Signature header. Turn on Raw Body in the Webhook node and verify against the exact bytes, as we walk through in our n8n webhook authentication guide. Then respond 200 right away. Retell's timeout is 10 seconds with up to 3 retries, and a slow CRM call inside the request is the classic way to get the same call delivered twice.
3. Filter, normalize and dedupe
Drop anything where call_status isn't ended or custom_analysis_data is empty. Convert every "" to null. Then dedupe on call_id, because retries are real. Our post on duplicate n8n webhook executions covers the pattern.
4. Upsert the contact on the caller's phone number
Use from_number for inbound and to_number for outbound, both already in E.164. Write text fields with "fill if empty" logic so a blank extraction never erases a name you already have.
5. Branch on the booleans
is_spamtrue: tag the contact, skip the task, stop.needs_callbacktrue: create a CRM task with the summary andreason, and notify the owner.- Everything else: save the summary as a note on the contact timeline.
The whole decision tree runs on two booleans and one selector. That's the point of typed fields.
How should you design the custom fields?
A few habits that have held up for us:
- One question per field. "Is this spam?" and "Do they want a callback?" are two booleans, not one "outcome" text field.
- Selectors over text whenever the answer has a known set of values. Urgency, language, service type, appointment status.
- Keep field names identical across agents. If every agent writes
needs_callback, your n8n routing and any native mapping work without a per-agent translation table. - Test on real calls before you rerun history. Retell lets you edit extraction prompts and rerun analysis on past calls, but the API docs note reruns incur charges. Try a handful first.
Get a free automation audit
If your voice agents are taking calls but your CRM still needs someone to read every transcript, the problem is usually the extraction schema and the plumbing after it, not the agent. We'll review your Retell setup, the fields you're extracting and where the data lands, and show you what to fix. Get a Free Automation Audit, or see how we build voice AI agents end to end.
