> ## Documentation Index
> Fetch the complete documentation index at: https://docs.retellai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent workflow

> Run integration tools before and after a Retell AI call or chat: look up the caller before the agent speaks, then log the outcome and create follow-ups.

An agent's **Workflow** page is where you run tools outside the conversation: before the agent speaks, so it starts already knowing who it's talking to, and after the session ends, so your systems are updated without anyone touching them. Voice agents call these **pre-call** and **post-call functions**; chat agents get the same two slots, named **pre-chat** and **post-chat functions**.

Each slot is a dependency graph rather than a flat list. Functions with no dependency all start at once, and a function that depends on another waits for its output.

<Frame caption="The agent's Workflow page: adding a Salesforce tool as a pre-call function, with post-call functions at the end of the flow.">
  <div style={{ aspectRatio: '16 / 9', width: '100%', background: 'rgba(128,128,128,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', overflow: 'hidden' }}>
    <img src="https://mintcdn.com/retellai/eQ17pyeTZnmNJIsQ/images/integration/workflow-pre-call-functions.png?fit=max&auto=format&n=eQ17pyeTZnmNJIsQ&q=85&s=8b891f73bf0c05bde04115846e9771e5" alt="The agent's Workflow page, with Workflow highlighted in the left rail next to Agent and Simulation. A flow runs from Dial in/out through pre-call functions, Call started, the Agent node with a knowledge base attached and an Add webhook slot below, Call ended, Post-call data extraction, and Post-call functions. A tool menu for a connection named salesforce retell is open over the pre-call functions node, listing Salesforce tools with Search Contact selected and its description shown. Blue rings mark the Workflow nav item and the Add buttons on the pre-call and post-call function nodes." style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} width="1792" height="867" data-path="images/integration/workflow-pre-call-functions.png" />
  </div>
</Frame>

Everything below works the same on both channels except where noted.

## When to use it

Reach for a pre-call or pre-chat function when the agent needs a fact it can't ask for, or shouldn't have to:

* **Greet an identified person by name.** Look the caller's number, or the chat's contact details, up in your CRM before the first word, instead of asking "can I take your name?"
* **Load account state.** Pull their open tickets, last order, or policy status so the agent answers the first question without a lookup mid-conversation.
* **Decide how to open.** Fetch a flag (overdue balance, VIP tier, do-not-call) and branch the greeting on it.

Reach for a post-call or post-chat function when the work happens after the conversation, not in it:

* **Log the outcome.** Write the summary and disposition back to the CRM timeline.
* **Create the follow-up.** Open a task, a ticket, or a callback when the conversation agreed to one.
* **Notify.** Text the caller a confirmation, or push the result to your own endpoint.

Keep a tool in the conversation itself when the agent needs the result *while* it's talking, like checking calendar availability for a time the caller just named. See [integration tools for prompt agents](/build/single-multi-prompt/integration-tools) and [integration tools in conversation flow](/build/conversation-flow/integration-tools) for that surface.

<Note>
  Workflow functions are configured on the dashboard only (as of August 2026). They are not part of the public API, so you can't set them through `create-agent` or `update-agent`.
</Note>

## What you can run

Both slots take the same four kinds of function, from the same **+** menu:

| Function             | What it does                                                                                                                              | Available in                      |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| **Integration tool** | A tool from a [connected provider](/integrations/overview) — HubSpot, Salesforce, Dynamics 365, GoHighLevel, Zendesk, Calendly, Cal.com   | Both slots, both channels         |
| **Custom function**  | An HTTP request to your own endpoint. The request body carries the session object — under `call` on a voice agent, `chat` on a chat agent | Both slots, both channels         |
| **Code**             | JavaScript run in Retell's sandbox — reshape a value, compute a date, call a small API                                                    | Both slots, both channels         |
| **SMS**              | Send a text message, with the body written by the LLM from the transcript                                                                 | Post-call only, voice agents only |

Saving an SMS function in the pre-call slot, or on a chat agent, is rejected: the session is over before an SMS can make sense, and a chat agent has no number to send from.

## Add functions on the Workflow page

<Steps>
  <Step title="Open the Workflow page">
    In the agent editor, select **Workflow** in the left rail. The canvas lays the whole session out end to end: **Dial in/out**, **Pre-call functions**, **Call started**, the agent, **Call ended**, **Post-call data extraction**, and **Post-call functions**. A chat agent shows the same flow with **Any trigger**, **Pre-chat functions**, **Chat started**, **Chat ended**, **Post-chat data extraction**, and **Post-chat functions**.
  </Step>

  <Step title="Add the first function">
    Click **+ Add** on the opening or closing functions node and pick **SMS**, **Code**, **Custom function**, or a tool from one of your connected providers. **Add integration** connects a new provider without leaving the page.
  </Step>

  <Step title="Configure it">
    Give it a **Name**. This is how other functions refer to it, so it must be unique within that slot. Then set its inputs and outputs in the **Function fields** card. Every input is either a literal you set now, which may be a `{{variable}}` reference, or a description the LLM fills in; every output you keep is what the agent sees and, optionally, a dynamic variable. On a pre-call function the inputs start on a literal rather than a description, since there's no conversation to infer from yet, but you can switch any of them. [Configure a tool's inputs and outputs](/integrations/overview#configure-a-tools-inputs-and-outputs) covers the modes and the test run in full.
  </Step>

  <Step title="Chain the next one">
    Hover a function's row and click its **+**, then choose **Add sequential function** to run the new one *after* it, or **Add parallel function** to run it *alongside* it. Use the node's own **+ Add** instead to add a function that starts immediately, independent of the rest.

    <Frame caption="A function's row menu, offering a sequential or a parallel function.">
      <div style={{ aspectRatio: '16 / 9', width: '100%', background: 'rgba(128,128,128,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', overflow: 'hidden' }}>
        <img src="https://mintcdn.com/retellai/wpYimq3MbpJ26a1D/images/integration/workflow-function-add-menu.png?fit=max&auto=format&n=wpYimq3MbpJ26a1D&q=85&s=926fa0bc998082d5e9c01fc9923822ae" alt="The Pre-call functions node on the agent's Workflow page, reached from Dial in/out. It holds a function named search_salesforce_c... with edit, delete, and + buttons, and below it an indented child row named get_salesforce_u... that runs after it, plus an + Add button. The first row's + button is ringed in blue and its dropdown is open, showing two items: Add sequential function, with a corner-down-right icon, and Add parallel function, with a stacked-lines icon." style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} width="1215" height="359" data-path="images/integration/workflow-function-add-menu.png" />
      </div>
    </Frame>
  </Step>
</Steps>

## Run functions in parallel or in sequence

Retell runs the graph in batches: every function whose dependencies have finished runs concurrently, then the next batch, until the graph is done. Total time is the longest chain, not the sum of every function.

* **Parallel.** A function with no dependency starts as soon as the phase begins, so two independent lookups cost as long as the slower one. **Add parallel function** starts the new function at the same point as the row you clicked, and anything that was waiting on that row now waits for both.
* **Sequential.** **Add sequential function** makes the new function depend on every function in the row's card, so it starts only once they've all succeeded. That's what lets it use their output.

A chain can be at most 4 functions deep, and each slot holds at most 15 functions (as of August 2026). Saving is rejected if a function depends on itself, on a name that doesn't exist, or on a cycle.

## Pass a response from one function to the next

Chaining works through [dynamic variables](/build/dynamic-variables). On the function's **Output** tab, give a response field a name in the **Dynamic variable** column; downstream functions then reference it as `{{that_name}}` in any input, and the agent's prompt can use it too.

<Frame caption="Pinning a function input to a dynamic variable: typing `{{` in a value field opens the picker.">
  <div style={{ aspectRatio: '16 / 9', width: '100%', background: 'rgba(128,128,128,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', overflow: 'hidden' }}>
    <img src="https://mintcdn.com/retellai/wpYimq3MbpJ26a1D/images/integration/workflow-function-variable-input.png?fit=max&auto=format&n=wpYimq3MbpJ26a1D&q=85&s=1615741fd16caeccdc8686e572076727" alt="The configuration dialog for a Salesforce Search Contact function named search_salesforce_contact, whose description tells the agent to find the contact by phone number at the start of the conversation using the caller's number. In the Function fields card, on the Input tab beside an Output tab, the required phone_number field's value row is ringed in blue, showing a curly-brace value-mode pill and an input where two open braces have been typed. Below it the dynamic variable picker is open with Agent, System, and Contacts tabs, System selected, listing session_duration_ms, current_node, previous_node, direction, user_number highlighted, agent_number, call_id, and call_type. A Run a test button sits at the bottom of the card, with Cancel and Save below." style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} width="862" height="1120" data-path="images/integration/workflow-function-variable-input.png" />
  </div>
</Frame>

Variables become available as the graph runs, so a function only ever sees output from functions that finished before it. The dashboard enforces the same rule as you type: when you pick a variable, it offers only the outputs of the functions this one waits on, not the ones running beside it or after it.

Two more things to know about how the data reaches the agent:

* **The result itself lands in the agent's context**, not just the variables you named. Every tool call is woven into the transcript the LLM reads, with its arguments and its response; what a workflow function changes is only *where* — a pre-conversation call is prepended, so the agent has it from its first turn. Responses are capped at 30,000 characters for an integration tool and 15,000 for a custom function or code tool (as of August 2026). Use **Select fields** on the **Output** tab to send only the fields that matter, rather than letting a large provider payload crowd the prompt.
* **Dynamic variables are extracted from the raw response**, before that field selection. Narrowing what the agent sees never breaks a variable you mapped.

If a `{{variable}}` in an input has no value — the function it came from didn't run, or returned nothing for that field — the input is left out. If that input was required, the function is skipped rather than sent with a gap in it.

### Variables a post-conversation function can use

Post-call and post-chat functions run after [post-call analysis](/features/post-call-analysis-overview), so they can read its results on top of everything available during the session:

| Voice                         | Chat                          | What it holds                                                                                      |
| ----------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------- |
| `{{call_summary}}`            | `{{chat_summary}}`            | The analysis summary                                                                               |
| `{{call_successful}}`         | `{{chat_successful}}`         | Whether the analysis judged the session successful                                                 |
| `{{user_sentiment}}`          | `{{user_sentiment}}`          | The user's sentiment                                                                               |
| One per custom analysis field | One per custom analysis field | Named after the field, from your [post-call analysis](/features/post-call-analysis-overview) setup |
| `{{disconnection_reason}}`    | `{{disconnection_reason}}`    | Why the session ended — see [call disconnect reasons](/reliability/debug-call-disconnect)          |
| `{{call_status}}`             | `{{chat_status}}`             | The session's final status                                                                         |

These are available to post-conversation functions only; nothing earlier can read them, because the analysis hasn't run yet.

## Gate a post-conversation function on a condition

Every post-call and post-chat function can carry an **only when** gate, so you don't write a task on a call that never connected. In the function's config, set the match to **All** or **Any**, then add rows:

* **Variables condition** — compare a `{{variable}}` against a value with `=`, `≠`, `>`, `<`, `≥`, `≤`, contains, not contains, exists, or not exists. Picking exists or not exists drops the value field, since there's nothing to compare against.
* **Disconnection reason** — pick one or more reasons the session must have ended with.
* **Call status** — require a specific final status.

<Frame caption="A post-call function gated on a condition: log the call only when the analysis produced a summary.">
  <div style={{ aspectRatio: '16 / 9', width: '100%', background: 'rgba(128,128,128,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', overflow: 'hidden' }}>
    <img src="https://mintcdn.com/retellai/wpYimq3MbpJ26a1D/images/integration/workflow-post-call-condition.png?fit=max&auto=format&n=wpYimq3MbpJ26a1D&q=85&s=aa72203e2a0296509ab2f32b61d03322" alt="The configuration dialog for a HubSpot Log Call Activity post-call function named log_hubspot_call. Its conditions card sits below the Description: an All selector heads the line All of the following conditions match, above one row testing the variable call_summary with the operator Exists, whose value field is greyed out because the operator takes no comparison. Below that row, an Add button ringed in blue has its menu open, offering the three kinds of row: Variables condition, Disconnection reason, and Call status. The Function fields card below shows a required contact_id input, with Run a test and Add input buttons, and Cancel and a disabled Save close the dialog." style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} width="862" height="1120" data-path="images/integration/workflow-post-call-condition.png" />
  </div>
</Frame>

A function with no rows always runs. A gated-off function counts as not having run, so anything depending on it is skipped too. That's usually what you want: gate the lookup, and the write that follows it drops with it.

## Examples

### Greet an identified caller and load their tickets

A support line answers with the caller's name and their open ticket already in context.

**Pre-call functions:**

1. `find_user` — Zendesk **Search User**, with `phone_number` pinned to `{{user_number}}`. On the **Output** tab, map the user's `id` to `zendesk_user_id` and `name` to `caller_name`.
2. `list_tickets` — Zendesk **List User Requested Tickets**, added as a *sequential* function under `find_user`, with `user_id` set to `{{zendesk_user_id}}`.

The prompt then opens with `Greet {{caller_name}} by name.` and the agent already has the ticket list in context, so "what's the status of my ticket?" is answered on the first turn instead of the third.

The same pair works on a [chat agent](/build/create-chat-agent) as pre-chat functions. Swap `{{user_number}}` for whichever [dynamic variable](/build/dynamic-variables) your chat is started with, such as an email address passed in when the widget opens.

### Log the session and open a follow-up task

A sales agent writes every conversation back to HubSpot, and creates a task only when there's something to follow up on.

**Pre-call function:**

* `search_contact` — HubSpot **Search Contact** on `{{user_number}}`, mapping the record ID to `hubspot_contact_id`.

**Post-call functions:**

1. `log_hubspot_call` — HubSpot **Log Call Activity** on `{{hubspot_contact_id}}`, with the body set to `{{call_summary}}`. Gated on **All**: `{{call_summary}}` exists, so a call that never got far enough to analyze writes nothing.
2. `create_followup` — HubSpot **Create Task**, added as a *parallel* function beside `log_hubspot_call` so both writes go out at once. Gated on **All**: `{{call_status}}` = `ended` **and** `{{followup_needed}}` = `true`, where `followup_needed` is a custom [post-call analysis](/features/post-call-analysis-overview) field. Its subject is a description the LLM fills in from the transcript.

The session is logged every time it completes; the task appears only on the conversations that earned one.

### Confirm a booking by text

A clinic texts the caller after the call, but only when a booking actually happened.

**Post-call functions:**

1. `get_booking` — Cal.com **Get Booking**, with the UID set to `{{booking_uid}}`, a variable an in-conversation **Book Appointment** tool mapped earlier in the call. Gated on **All**: `{{booking_uid}}` exists.
2. `send_confirmation` — an **SMS** function, sequential under `get_booking`, whose content the LLM writes from the transcript and the booking response.

No booking means `{{booking_uid}}` never gets set, the gate fails, and both functions drop, so no text goes out. On a chat agent, replace the SMS with a **Custom function** that posts to your own notification endpoint.

## Timing and limits

A call waits for its pre-call graph before the conversation begins, up to a budget that depends on the call:

* **Inbound and web calls wait 1 minute.**
* **Outbound calls wait 5 minutes, before Retell dials.** The extra room is there because nothing is ringing yet, so nobody is sitting on the line while a CRM lookup runs.

Exceeding the budget doesn't cancel anything. The remaining functions **keep running in the background**, and what they return still reaches the agent: each call and its response are added to the transcript the LLM reads, and the variables they map stay available to everything downstream, post-call functions included. What a late arrival misses is the initial prompt and the agent's opening message, which are already built by the time it lands. So the budget is the deadline for anything the agent's first words depend on, not for the data itself.

Post-call functions are capped at **5 minutes** total. That one is a hard stop: whatever hasn't finished is cancelled so teardown can complete.

|                     | Pre-conversation                                                                                                                                         | Post-conversation                  |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| When it runs        | Before the agent's first message; for outbound calls, before Retell dials                                                                                | After post-call analysis completes |
| Time budget (voice) | 1 minute inbound and web, 5 minutes outbound                                                                                                             | 5 minutes                          |
| Time budget (chat)  | None; the chat waits for the graph                                                                                                                       | None                               |
| Past the budget     | The session starts; remaining functions finish in the background and still reach the transcript, but too late for the initial prompt and opening message | Remaining functions are cancelled  |
| Max functions       | 15                                                                                                                                                       | 15                                 |
| Max chain depth     | 4                                                                                                                                                        | 4                                  |

Figures are as of August 2026.

## When a function fails

A failure stays inside the function it happened in. A function that errors, times out, is missing a required input, or is gated off is marked unsuccessful, and:

* **Everything waiting on it is skipped**, and everything waiting on those. A lookup that fails takes down the write that needed its ID, rather than sending a request with a blank field.
* **Independent branches carry on.** A failed CRM lookup doesn't stop the calendar check running beside it.
* **The session continues.** A pre-conversation failure still connects the call or opens the chat, just without those variables, so write the prompt to read correctly when a lookup came back empty. A post-conversation failure still completes teardown.

## See what ran

Open the call in [call and chat history](/features/session-history) and read the transcript. Pre-call function calls sit under a **Before call** divider at the top, the conversation follows **Call started**, and post-call ones sit under **Call ended** at the bottom. Each entry shows the arguments sent and the response returned, so you can tell a bad input apart from a provider error. During a live call, the same entries collapse under **Before conversation** in the live transcript.

Chat sessions run their pre-chat and post-chat functions the same way, but the dashboard's chat transcript doesn't show those entries yet (as of August 2026).

## FAQ

<AccordionGroup>
  <Accordion title="Can the agent decide not to run a pre-call function?">
    No. Every function in the slot runs whenever its dependencies are met. Unlike a tool in the conversation, there is no LLM decision about *whether* to call it. The LLM is only involved in filling inputs you described rather than pinned, and post-conversation functions add an **only when** gate you control.
  </Accordion>

  <Accordion title="Where does the LLM get values for described inputs, before the conversation has started?">
    From the agent's prompt and the dynamic variables available at that point, including outputs from earlier functions in the graph. There's no conversation yet, so an input that can only come from the user belongs on an in-conversation tool instead. Post-conversation functions also see the full transcript.
  </Accordion>

  <Accordion title="Do pre-call functions delay the call?">
    Yes, by as long as the graph takes, up to 1 minute on an inbound or web call, and a chat waits for its pre-chat graph with no budget at all. Outbound calls absorb their 5-minute budget before dialing, so nobody hears the delay. Keep the chain shallow: two functions running in parallel cost one function's latency, while two chained cost both. If a lookup is slow and not needed for the opening line, run it as an in-conversation tool instead.
  </Accordion>

  <Accordion title="Can a post-call function read what a pre-call function returned?">
    Yes. Pre-conversation outputs stay in the session's dynamic variables for the whole session, so a post-conversation function can reference them directly. That's how the HubSpot example above reuses `{{hubspot_contact_id}}` without looking the contact up twice.
  </Accordion>

  <Accordion title="What happens if two functions have the same name?">
    Saving is rejected. Names are the handle other functions depend on, so they have to be unique within a slot. The same name may appear once in the pre-conversation slot and once in the post-conversation slot.
  </Accordion>
</AccordionGroup>
