Call destinations

Write what happened on every call into a spreadsheet or a table you already use — automatically, as each call ends.

Set it up on an agent under After the Call → Call log (the Post-call actions step while you're creating one), or for the whole workspace under Workplace → Post-call export. Add opens the editor in its own window; each destination you've set up is one row in the list, with its edit and remove buttons beside it.

You can configure this on a brand-new agent before saving it — the row is tagged Saves with agent, and the destination is created together with the agent.

Why not just give the agent a tool?

You can already give an agent a "add a row to my sheet" tool, and it isn't the same thing:

  • A tool fires only if the model decides to call it. A log has to be guaranteed.
  • A tool cannot record a call nobody answered — there was no conversation for it to act in. That's usually the row you most want.

A destination is neither of those. It runs after the call, from the call record itself, whatever happened.

Where you can write

DestinationNotes
Google SheetValues are matched to columns by header name, so inserting a column doesn't shift everything
Airtable tableValues land correctly in single-select and date fields, not just text
DatabasePostgreSQL, MySQL, SQL Server or MongoDB — see Connecting your database
CRM call log — HubSpot, Salesforce, Zoho CRMEach call becomes the CRM's own call activity — a HubSpot call, a Salesforce Task, a record in Zoho's Calls module — never a new lead (see below)
Custom APIYour own endpoint. Finished calls are POSTed as JSON, {"calls": [ … ]}, up to 100 per request, each entry holding the fields you mapped. Any non-2xx answer counts as a failed write

Every destination is shown whether you've connected it or not; one you haven't says Not connected, and choosing it puts a Connect button right there, so you don't have to go to Workplace → Integrations first. The four database engines sit behind one Database entry; choose it, then pick your engine.

A CRM gets a call log, never a new lead. Creating a lead for every call would fill your CRM with duplicates — a campaign that dials 5,000 existing leads would add 5,000 more — so the CRM destinations write the CRM's own record of a call instead. Map at least these fields: for HubSpot hs_timestamp (when the call happened — HubSpot rejects a call without it); for Salesforce Subject; for Zoho Subject, Call_Start_Time and Call_Duration (Zoho rejects a call without them).

Picking a sheet and its columns

For a Google Sheet, paste the sheet's link — or just its ID — into Spreadsheet ID. The Tab list is read from the spreadsheet, and the tab's header row becomes the list of columns to map:

  • Add all N columns (or Add the other N once some are mapped) adds a row for every header, with the value already chosen wherever a header names a field — Full Name picks your agent's full_name, Call Outcome picks Outcome. A header that matches nothing is left for you to choose rather than guessed.
  • A mapped column the sheet no longer has shows as (not found) instead of being dropped.
  • If the tabs or headers can't be read (the connected account has no access to that sheet yet, or the ID is wrong), you get text boxes instead, so you're never stuck.

Picking a table and columns

For a database, you don't type names. The table list is read off the connection, and choosing a table lists its columns with their types:

Table    call_log  ▼
outcome · text      status · text      metadata · jsonb

Two things this catches before a single call is written:

  • A column that is NOT NULL with no default and isn't mapped is flagged — the insert names only the columns you map, so leaving one out fails every write.
  • A table or column name that no longer exists shows as such rather than failing silently later.

If the role can't read the catalog, you get a text box and a note saying why — you're never stuck.

JSON and JSONB columns

A json/jsonb column can hold the real shape instead of flattened text:

  • One field — keeps its shape: analysis.topics stays a real array, not "pricing, demo".
  • Several fields as one object — tick what goes in, and the editor previews exactly what will be written:
{ "sentiment": "positive",
  "action_items": ["Send the pricing PDF", "Book the Friday demo"],
  "interested": "yes" }

Keys are the short names, so you query metadata->>'sentiment'. On SQL Server, which has no native JSON type, a bundle lands as JSON text.

What you can write

You map their column (yours, in the sheet or table) to a value from the call.

Field
call.outcomeanswered, no-answer, busy or failed — the carrier's own verdict, not a flattened "failed"
call.ended_at call.attempt call.to call.from
session.call_idThe call's own id. Open it at /calls/<id> for the recording and transcript, or fetch it from the API as GET /api/v1/calls/<id> — it's the same sessionId your webhooks send. Names the call this row was written from; on a retried target that's the most recent attempt. Blank when the number was never dialled successfully, because there's no call to point at.
record.dispositionHow your agent graded the lead against the rules in its own prompt — QUALIFIED, NURTURE, NOT_INTERESTED. Not the same as call.outcome, which only says whether the phone was picked up.
record.opted_out record.declined record.actions_doneWhether they asked not to be contacted; what they wouldn't give; which tools actually ran (written when the tool runs, never from what the agent said).
keypad.digitsEvery keypad digit the caller pressed, in order — 1,3. Blank if they pressed nothing. Campaign calls only.
keypad.choicesWhat each press meant, in the same order — Confirm,Talk to team. The option's name as your flow defined it when the call happened, so a later edit can't re-label a call already written to your sheet. An unmapped press leaves its slot empty.
analysis.summary analysis.sentiment analysis.dispositionfrom post-call analysis
analysis.action_items analysis.topicslists, written as comma-separated text
analysis.custom.<name>anything your agent extracts — analysis.custom.interested, analysis.custom.budget
var.<name>a context variable carried into the call
'some text'a literal, in single quotes

analysis.custom.* is usually the interesting half. Whatever you've told the agent to extract is mappable straight into your own columns.

The picker shows you what each field actually contains, taken from this agent's own recent calls — so you're choosing by value, not by guessing at a name:

Sentiment              analysis.sentiment
How the caller sounded overall.
e.g. positive

budget                 analysis.custom.budget
Stated budget, if the caller mentioned one.
⚠ Empty on all 12 recent calls — this column would stay blank

That last line is the useful one: a field your agent declares and never fills is otherwise only discoverable by mapping it and watching a blank column for a week.

A field with nothing in it is skipped, not blanked. If a call has no summary, that cell is left alone rather than overwritten with an empty string — we won't erase something you already had.

If you map an analysis field

Post-call analysis takes a moment to run, so the destination waits for it before writing. That means the row appears a minute or two after the call, not instantly — the editor tells you when a mapping does this.

If analysis can't complete, we write anyway after 30 minutes with those fields empty, and record why. A missing summary is better than a missing row.

Two things worth knowing: analysis must be enabled on the agent or those fields are always blank, and a call that falls under the agent's minimum-substance threshold is never analysed at all, so it waits the full 30 minutes before writing.

Which calls

Setting
Which callsEvery call · Except campaign calls · Only campaign calls
Outcomesany of Answered No answer Busy Failed (answered no-answer busy failed)
Call-backsWrite every call · Write once per chain

Use Except campaign calls when a campaign already writes back to its own source and you don't want the call logged twice.

Call-backs change your numbers. A chain is several calls for what the customer experienced as one request. Write every call (the default) writes each of them, so a report that counts rows counts that lead more than once; Write once per chain writes just the call that ended the thread — almost certainly what you want if the destination feeds a report that counts leads. The trade: that row only arrives once the chain ends, which can be days later.

More than one destination

An agent can have several, each with its own mapping — a summary row into your warehouse and a status flag into your CRM, say. Every one of them fires on every matching call, independently. A destination that maps analysis fields waits for analysis; one that doesn't writes as soon as the call ends. They don't wait for each other.

Workspace default vs per-agent

A destination set under Workplace → Post-call export applies to every agent, and new agents inherit it automatically. That's the place to set it once if you want every call in your workspace logged to the same table — rather than repeating it on each agent and having the next one silently miss it.

An agent that defines its own uses only its own — it replaces the default rather than adding to it, so you never get a duplicate row you didn't configure.

The two panels are the same editor, and tell you which one you're looking at:

HeadingRows
Workplace → Post-call exportWhere to send every callyours to edit
An agentCall logan inherited one carries a Workspace default tag and is read-only here — change it in Post-call export, or add the agent's own to override it. Once the agent has its own, the inherited row is dimmed and tagged Workspace · not used

When something goes wrong

The error appears on the destination itself, with the vendor's own message — "No such column 'Call_Status__c'", "the sheet has no header row". That matters because the failure is otherwise invisible: the agent keeps answering calls perfectly and the sheet simply stops filling.

Failed writes are retried. Nothing is lost by a temporary outage at Google or Airtable.

Removing a destination asks you to confirm first. Calls stop being written there; the rows already written stay where they are.

Each call is written once per destination, even across restarts or multiple servers — so you won't get duplicate rows.

Campaign write-back is the same machinery

If you fill a campaign from a source, its write-back is a destination too — one that updates the row the contact came from rather than appending a new one. It's configured in the campaign's own importer, so it doesn't appear in these lists.

The difference is only the verb:

  • Campaign — we read the row, so we know its address and can update it
  • Agent — nothing was read (an inbound caller is in no list), so each call appends

API

For the API, see Integration connectors API → Call destinations.