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

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, campaigns, projects (GoHighLevel sub-accounts), contacts, calls, outcomes, skills, cadences, phone numbers.
> Scope requests by locationId rather than projectName.
> Read the guide linked from each API reference group before recommending an endpoint.

# Workflows contract

> Every shape, rule, limit and error of a TFU Live agent's workflows, for editors and API clients building on the TFU Live API.

A TFU Live agent's abilities are **workflows**: a name (the AI's tool name), a
description (when the AI calls it) and steps built from nodes. Why the model is
shaped this way is recorded in the architecture decision
`operations/infrastructure/v2/gpt-live/docs/adr/0001-abilities-are-workflows.md`;
this page is the contract a client builds against. The runtime validates and
runs workflows and stays the authority: a client's own checks are early
feedback, never a substitute for the save.

Every example on this page is checked against the real validators by
`tfu-live-workflows-contract.test.js`.

## Endpoints

All paths are relative to one agent, `/api/tfu-live-agents/{id}`, and take
`?locationId=`. Browser sessions and API keys use the same routes; a key needs
`agents:read` to read and `agents:write` to write, and is held to its own
sub-accounts like every TFU Live route.

| Operation | What it does |
| - | - |
| [Get TFU Live catalog](/api-reference/tfu-live/get-tfu-live-catalog) | The workflow model (`data.workflows`) and the sub-account's CRM (`data.subAccount`) |
| [List TFU Live workflows](/api-reference/tfu-live/list-tfu-live-workflows) | Every workflow with its tool name, and the `ifVersion` to review |
| [Get TFU Live workflow](/api-reference/tfu-live/get-tfu-live-workflow) | One workflow |
| [Create TFU Live workflow](/api-reference/tfu-live/create-tfu-live-workflow) | From `{ ifVersion, template }` or `{ ifVersion, workflow }`; the id is assigned |
| [Replace TFU Live workflow](/api-reference/tfu-live/replace-tfu-live-workflow) | `{ ifVersion, workflow }`; a workflow keeps its id |
| [Delete TFU Live workflow](/api-reference/tfu-live/delete-tfu-live-workflow) | `?ifVersion=` in the query |
| [Validate TFU Live workflows](/api-reference/tfu-live/validate-tfu-live-workflows) | Every check a save makes on `{ workflows }`, nothing saved |
| [Look up TFU Live resources](/api-reference/tfu-live/look-up-tfu-live-resources) | What a setting may name in this sub-account |

Two more routes sit beside the agent: [List TFU Live Brain templates](/api-reference/tfu-live/list-tfu-live-brain-templates)
(`GET /api/tfu-live-agents/brain-templates`) and
[Create agent](/api-reference/tfu-live/create-tfu-live-agent) from one of them
([Brain templates](#brain-templates) below).

Every write names the `ifVersion` it read, saves a new revision and answers with
the agent (its next `ifVersion`). A stale version is `409`. On an agent that is
not an explicit draft, setup starts as after
[Update agent](/api-reference/tfu-live/update-tfu-live-agent), which still
replaces the whole agent, workflows included.

## The stored shape

Workflows live at `brain.capabilityConfig.workflows`, keyed by their id:

```json theme={"dark"}
{
  "wf_route001": {
    "id": "wf_route001",
    "name": "Talk to the team",
    "description": "Use when the caller wants a person, or agrees to be connected to the team.",
    "steps": [
      {
        "id": "ask",
        "type": "collect_information",
        "label": "Language and reason",
        "config": {
          "items": [
            { "id": "language", "label": "Preferred language", "type": "choice", "choices": ["English", "Spanish"] },
            { "id": "reason", "label": "Why they want a person", "type": "text", "required": false }
          ]
        }
      },
      {
        "id": "route",
        "type": "condition",
        "config": { "cases": [{ "id": "spanish", "field": "collected.language", "op": "equals", "value": "Spanish" }] },
        "outputs": {
          "spanish": [
            {
              "id": "to_team",
              "type": "transfer",
              "config": { "destination": { "kind": "team", "teamId": "team_es" }, "introduction": "{{contact.firstName}} would like help in Spanish. {{collected.reason}}" },
              "outputs": {
                "connected": [{ "id": "note", "type": "add_note", "config": { "note": "Transferred to the Spanish team." } }],
                "nobody_available": [{ "id": "later", "type": "steer_agent", "config": { "guidance": "Nobody is free. Offer a callback." } }],
                "not_connected": []
              }
            }
          ],
          "otherwise": [
            {
              "id": "to_roster",
              "type": "transfer",
              "config": { "destination": { "kind": "roster" }, "introduction": "I have {{contact.firstName}} on the line. {{collected.reason}}" }
            }
          ]
        }
      }
    ]
  }
}
```

* A workflow is `{ id, name, description, steps }`. `id` is `wf_` and 8
  lowercase letters or digits, stable for the workflow's life. `name` is 1 to
  60 characters, `description` 1 to 3000.
* A step is `{ id, type, label?, config, outputs? }`. `id` is up to 40 of
  `a-z 0-9 _ -`, unique in the workflow. `label` is up to 120 characters.
* `outputs` exists only on a node that has outputs, one list of steps per
  output. A node with outputs ends its list: what follows it goes on one of its
  outputs. An output may be left empty or absent.

## The catalogue

[Get TFU Live catalog](/api-reference/tfu-live/get-tfu-live-catalog) answers
`data.workflows`, the model as the runtime publishes it:

| Key | What it holds |
| - | - |
| `nodes[]` | `type`, `name`, `timing`, `outputs`, `dynamicOutputs` (Condition), `summary`, `settings` (each setting in words), `settingsSchema` (JSON Schema 2020-12 of `config`), `arguments` and `argumentSchemas` (what the node takes from the AI on a later call) |
| `templates[]` | `key`, `name`, `description`, `steps`, and `mustFill`: the `{ step, setting }` pairs left empty for the client, refused until filled |
| `rules` | The path rules below, in words |
| `limits` | `workflows` 20, `outputDepth` 5, `stepsPerWorkflow` 60, `nameLength` 60, `descriptionLength` 3000, `labelLength` 120, `totalBytes` 180000 (the whole workflows map), `toolNameLength` 64, `toolDescriptionSent` 1024 |
| `fieldReferences`, `contactFields` | The reference grammar, and which contact fields are readable and writable |
| `webhook` | Header rules: the credential pattern, reserved names, secret forms and limits |
| `timing`, `shape`, `outputLabels` | The same, in words |

`settingsSchema` holds each node's shape and limits (a Transfer introduction is
at most 1000 characters, a Condition has 1 to 10 cases, Check zip code up to
10000 codes). What depends on a node's place in its workflow is a rule, checked
on save, and a config the schema accepts can still be refused for it.

The node types are `collect_information`, `condition`, `check_tag`, `add_tag`,
`remove_tag`, `set_field`, `move_stage`, `add_note`, `send_sms`, `send_email`,
`send_webhook`, `notify_teammate`, `webhook_request`, `transfer`, `dnd`,
`find_slots`, `book_appointment`, `cancel_appointment`, `schedule_ai_call`,
`check_zip_code`, `cancel_scheduled` and `steer_agent`. Each has a `timing`:
`waits` (it runs before the AI's answer and is part of it), `instant` (it changes
the call's working copy of the lead at once; the CRM write follows), `after` (it
runs after the answer, durably, in path order) or `control` (a Condition: it only
chooses an output).

`outputs` is always a list. A Condition's are dynamic: one output per case,
keyed by the case `id`, in case order, then `otherwise`
(`dynamicOutputs: { from: "config.cases[].id", position: "before" }`). The
others are fixed: Check tag `yes`, `no`; Webhook with response `success`,
`failed`; Transfer `connected`, `nobody_available`, `not_connected`; Book
appointment `booked`, `rescheduled` (its `mode` decides which it can reach);
Cancel appointment `cancelled`; Check zip code `in_area`, `out_of_area`,
`missing`.

## Rules a save enforces

* Collect information is the only way the AI supplies inputs. A node reads
  `collected.<id>` only after the Collect information node that gathers it, and
  `response.<key>` only on the Success output of the Webhook with response that
  maps it. An item id has one definition in a workflow, and no item may be
  named like a node's argument (`slot_id`, `event_id`, `confirmed`, `from_date`,
  `days`, `earliest`, `latest`).
* After a Transfer's Connected output only `instant` and `after` nodes and
  Conditions: the AI has left the call. A call makes one transfer attempt; a
  fallback transfer goes only on Nobody available.
* Nothing follows Put on DND. Book appointment needs a Find slots node earlier
  on its path. Find slots has one calendar or routing rules over required
  collected items, with no default calendar.
* Tags are chosen in their node, one per node, never by the AI. On the native
  CRM there are no tags: `data.subAccount.nativeCrm` is `true`, `tagNodes` is
  `false`, and a save holding a tag node is refused. The overview of a
  sub-account also says `nativeCrm`.
* Every step that names something in the sub-account is checked when it is new
  or changed: the custom fields it writes (Set field `field`, a Collect
  information item's `field`) by id; the custom fields it reads (a Condition
  case's `field`, Check zip code's `field`, a Webhook with response field from a
  contact field) by id or key; Move stage's pipeline and stage; the calendars of
  Find slots and Cancel appointment, which must be active; the team or rep a
  Transfer names (`destination.teamId`, `destination.repId`), which must be the
  sub-account's agency's (`reps` and `teams` lookups). A reference inside text
  (`{{contact.custom.…}}`) is not checked: an unknown one is left empty.
* A setting the client fills for their business (a calendar, a tag, who takes a
  transfer, the covered zip codes, a choice item's choices, an item's label) may
  be left empty on an explicit draft: it is **incomplete, not invalid**, and it
  is listed in the agent's `setup.items` as a setup item. An agent that is set up
  on every save (not an explicit draft) refuses it as `setup_incomplete`, and
  [Prepare agent](/api-reference/tfu-live/prepare-tfu-live-agent) refuses any
  agent that still has a setup item. See [Setup items](#setup-items).
* Every new agent starts with **Schedule a callback** and **Stop contact**,
  unless it already holds a node that schedules an AI call or puts the lead on
  DND, and always has the three always-on tools: **End the current call**,
  **Return the delegated result** and **Dropped-call recovery**
  (`end_call`, `respond_to_voice`, `resume_call_after_drop`). Dropped-call
  recovery is shown with its settings (its label and description); none of the
  three can be removed, and a save adds any an older agent lacks. Earlier
  conversations and Messages stay optional.

## Tool names and renames

A workflow's tool name is its name lowercased, every run of other characters
turned into `_`, trimmed, at most 64, starting with a letter: "Talk to the
team" is `talk_to_the_team`. A name that makes no tool name (one starting with a
digit) is refused. Two workflows may not share a tool name, and none may take a
built-in tool's (`send_message`, `search_call_history`, `end_call`,
`resume_call_after_drop`, `respond_to_voice`, `use_skill`, `record_call_summary`)
or one of the agent's equipped tools. Renaming renames the tool the AI calls
once the saved revision is set up; on a live agent the previous revision keeps
answering until then. The id never changes, so history and references hold.

## How a workflow runs across AI turns

The AI calls the tool with arguments: every Collect information item of the
workflow (all optional, typed from the item) plus each node's `arguments`. Every
call walks the workflow from the start; finished side effects are not repeated,
and collected values, webhook responses and node memory belong to that workflow
for the call. The answer is one shape:

```json theme={"dark"}
{
  "success": false,
  "detail": "Ask for what is missing, then call again.",
  "needs_information": [{ "id": "reason", "label": "Why they want a person", "question": "What would you like to talk to the team about?" }]
}
```

`success`, `detail`, `unknown` and `outcome` come from the node whose result
decides (a booking, a transfer, a webhook); any other step that failed is listed
in `failed: [{ step, label, detail?, unknown? }]`. `needs_information` lists what
to ask; `options` carries times from Find slots or appointments from Cancel
appointment; `guidance` is Steer the agent's; `checkedTags` and `responses` are
what Check tag and a webhook found. `pending` lists what is saved but not yet
confirmed: `crm_write`, `queued` (an after step), `unconfirmed` (held because an
earlier run's outcome is unknown) and `waiting` (a transfer still ringing). The AI
is never told those completed. A transfer that settles after the answer continues
its workflow on that output (Connected, Nobody available or Not connected) once,
without another AI call. After Put on DND the answer adds `journeyEnded: true`.

Booking takes several turns: Find slots answers `options`, the AI calls again
with the `slot_id` the caller agreed to and `confirmed: true`, and Book
appointment books it (or, with an `event_id` of the caller's appointment, moves
it). Cancel appointment lists the caller's appointments and cancels one on a
call with its `event_id` and `confirmed: true`.

Book appointment's `mode` setting says what it may do: `book_or_move` (the
default, and the template's) books a new appointment or moves the caller's
existing one; `book_only` never moves one, so its workflow's tool takes no
`event_id` and a caller who already has an appointment is told so; `move_only`
only moves the caller's existing appointment and never books one. A book-only
workflow uses only the Booked output, a move-only one only Rescheduled:

```json theme={"dark"}
{
  "workflow": {
    "name": "Book a first visit",
    "description": "Use when a caller with no appointment wants to book a visit.",
    "steps": [
      { "id": "slots", "type": "find_slots", "config": { "calendarId": "cal_visits" } },
      { "id": "book", "type": "book_appointment", "config": { "mode": "book_only" }, "outputs": { "booked": [] } }
    ]
  }
}
```

## Field references

`contact.<field>`, `contact.custom.<id>` (a custom field key also reads),
`contact.tags`, `collected.<item id>`, `response.<key>` and `location.<key>` (a
sub-account custom value). Text settings use them as `{{collected.reason}}`.
Readable standard fields are `contactFields.readable`; the writable ones (Set
field, a Collect information item's `field`) are `firstName`, `lastName`,
`email`, `phone`, `address1`, `city`, `state`, `postalCode`, `dateOfBirth` and
`timezone`, plus any `contact.custom.<id>`.

## Secret headers

A Webhook with response header whose name matches the credential pattern
(auth, api key, token, secret, credential, password, signature) must be a
secret: `{ "secret": { "value": "…" } }` when typed. It is stored encrypted and
reads back only as `{ "secret": { "id": "…" } }`; send that back to keep it. A
saved secret is bound to the step it was typed at and the origin (scheme, host,
port) that step sends to: sending it back on another step, or after pointing the
step at another host, is refused with `workflow_secret_refused`, and the value
must be typed again. Build with AI never types or moves one. Host,
Content-Length, Content-Type, cookies, the connection headers and the proxy
headers are reserved (`webhook.reservedHeaders`).

## Lookups

[Look up TFU Live resources](/api-reference/tfu-live/look-up-tfu-live-resources)
takes a kind and an optional `query` (a name or an exact id):

| Kind | Items | Fills |
| - | - | - |
| `calendars` | `{ id, name, calendarType, isActive }` | Find slots, Cancel appointment |
| `tags` | `{ id, name }` | Check tag, Add tag, Remove tag |
| `custom_fields` | `{ id, name, fieldKey, dataType, picklistOptions?, model? }` | `contact.custom.<id>` |
| `pipelines` | `{ id, name, stages: [{ id, name, position }] }` | Move stage |
| `reps` | `{ id, name, phone, status, teamId, timezone }` | Transfer to a rep |
| `teams` | `{ id, name, memberCount }` | Transfer to a team |

`status` is `available` or `unavailable`. Unavailable means the lookup could not
run: it is unknown, never an empty list. At most 200 items come back
(`hasMore`, `totalMatches`).

## Brain templates

A Brain template is business logic materialised as a configuration of
workflows: a ready-made agent. Templates are data composed of the workflow
templates above, niche agnostic, and every one holds **Schedule a callback**
and **Stop contact**, equips the always-on tools and Earlier conversations,
and keeps texting off. An agent made from one is an ordinary agent: nothing
remembers the template, and every workflow is edited like any other.

| Template | Its workflows | `logic` | `toFill` |
| - | - | - | - |
| `booking_one_calendar` | Book appointment (book or move) and Cancel appointment on one calendar | booking `single`, cancel, transfer `none` | `calendar` |
| `booking_several_calendars` | Book appointment asks one choice question; Find slots routes each answer to its calendar, with no default calendar; Cancel appointment looks on every calendar the answers name | booking `routed`, cancel, transfer `none` | `question`, `answers` |
| `inbound_receptionist` (inbound) | Live transfer to one destination, Book appointment on one calendar | booking `single`, transfer `primary` | `destination`, `calendar` |
| `transfer_then_booking` | Live transfer; on Nobody available and Not connected, Steer the agent tells the AI to offer an appointment and names Book appointment (it never runs it); Book appointment on one calendar | booking `single`, transfer `first_with_booking_fallback` | `destination`, `calendar` |

[List TFU Live Brain templates](/api-reference/tfu-live/list-tfu-live-brain-templates)
answers `data.brainTemplates`, each `{ id, name, description, direction, logic,
tools, workflows, prompt, placeholders, toFill }`:

* `description` says the business logic in words: who it is for, what the call
  does, the fallback, and that texting is off.

* `direction` is `outbound`, `inbound` or `either`: the campaigns it suits.

* `logic` is the same logic in a few values, so a builder can match an agent's
  logic to a template:

  | Key | Values |
  | - | - |
  | `booking` | `none`, `single` (one calendar) or `routed` (one question's answer picks the calendar; there is no default calendar) |
  | `cancel` | whether a lead can cancel an appointment on the call |
  | `transfer` | `none`; `primary`: a transfer is a main path whenever the caller wants a person, side by side with booking; `first_with_booking_fallback`: an interested lead is transferred, and booking is offered only when the transfer ends Nobody available or Not connected |
  | `direction` | `inbound`, `outbound` or `either` |
  | `texting` | whether it texts: `false` in every template |

* `workflows` are keyed by a name local to the template. In a step's `config`,
  `{ "$fill": id }` stands for a setting filled from `values.<id>` (`as` says
  how: a list, the answers, the calendars, routing rules), and
  `{ "$workflow": key }` for the id another of its workflows gets in the agent.

* `prompt` is the starting Brain in the shared format: `# Identity` (who the
  agent is, how it carries itself, the call's details and a few don'ts),
  `# Steps to follow` (the Hook, said word for word when the call starts, then
  each step's line and goal) and `# Conversational FAQ`. It names the agent as
  `{{agent_name}}`, its saved name, and speaks with the
  [Layer 1 variables](#layer-1-variables): `{{first_name}}`, `{{full_name}}`,
  `{{business_name}}`, `{{direction}}` and the rest. A template states no
  business fact and holds no `[bracketed]` placeholder, so `placeholders` is
  always empty: the builder writes the business's own lines and FAQs.

* `toFill` lists the settings the client fills for their business: `id` (its
  key in `values`), `label`, `kind`, `value` (its JSON Schema) and `settings`,
  every `{ workflow, step, setting }` it fills. Every other setting is valid as
  published.

The kinds: `calendar` is a calendar id from the `calendars` lookup;
`transfer_destination` is one Transfer destination (a team or rep must be the
sub-account's); `text` is 1 to 150 characters; `answer_calendars` is 2 to 30
`{ answer, calendarId }` with distinct answers, which become the question's
choices, one routing rule each and the calendars Cancel appointment looks on.
The single calendar template's value, as the catalogue gives it:

```json theme={"dark"}
{
  "id": "booking_one_calendar",
  "toFill": [
    {
      "id": "calendar",
      "label": "The calendar appointments are booked on",
      "kind": "calendar",
      "value": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,200}$", "description": "A calendar id from the sub-account’s calendars lookup." },
      "settings": [
        { "workflow": "book", "step": "slots", "setting": "calendarId" },
        { "workflow": "cancel", "step": "cancel", "setting": "calendarIds" }
      ]
    }
  ]
}
```

To create an agent from one, send `{ id, brainTemplateId, values?, name? }` to
[Create agent](/api-reference/tfu-live/create-tfu-live-agent). No values are
needed: the agent is created at once with every workflow in place, each setting
to fill empty and listed as a setup item.

```json theme={"dark"}
{ "id": "front_desk", "brainTemplateId": "transfer_then_booking", "name": "Front desk" }
```

Values are applied when given, all or some of them:

```json theme={"dark"}
{
  "id": "front_desk",
  "brainTemplateId": "booking_several_calendars",
  "name": "Front desk",
  "values": {
    "question": "Is this your first visit with us?",
    "answers": [
      { "answer": "First visit", "calendarId": "cal_first_visits" },
      { "answer": "Been before", "calendarId": "cal_follow_ups" }
    ]
  }
}
```

* The runtime builds the agent the template makes and the create checks it
  like any save: its workflows, and every calendar, team and rep it names must be
  the sub-account's (calendars active). `name` defaults to the template's name.
* Its workflow ids are derived from the agent `id`: a retry with the same `id`
  and body is the same agent; other values for that `id` are a `409`.
* It is an explicit draft (`provisioningPolicy: "explicit"`, `setupStarted:
  false`), linked to a paused campaign like every new agent, and its
  `setup.items` say what it still needs (below).
* The response is the create's: `data.agent` (with its workflows) and
  `data.campaign`.

A refused value names itself: `field` is `values.<id>` (or `values`,
`brainTemplateId`), and `workflowId` and `stepId` are where it goes in the agent
being created. An unknown template is `invalid_agent` with `field:
"brainTemplateId"`.

```json theme={"dark"}
{
  "success": false,
  "error": "Choose an active calendar verified for this sub-account.",
  "code": "workflow_resource_not_found",
  "workflowId": "wf_mqe8x9v8",
  "stepId": "slots",
  "field": "values.calendar"
}
```

Build with AI lists the templates and starts the agent it is building from one
straight away: the template's workflows replace the agent's, its tools are
added, and its prompt is used when the agent has none yet. Values are optional
there too; it gives the ones it knows or looks up, never invents a calendar or
a destination, and reports what is left as setup items.

## Setup items

What an agent still needs before it can be prepared for real calls. Every read
of an agent carries them in `setup.items`, each `{ kind, workflowId?,
workflowName?, stepId?, node?, field?, placeholder?, message }`:

* `workflow_setting`: a setting still to fill, where it is (`workflowId`,
  `stepId`, `field` such as `calendarId`, `destination`, `items[0].choices`)
  and what to do (`message`);
* `prompt_placeholder`: a Brain template `[bracketed]` placeholder still in the
  prompt (`field: "brain.prompt"`, `placeholder`). Templates no longer hold any,
  so this appears only for an older prompt that still does.

An agent with setup items:

* **saves** as an explicit draft, and can be edited step by step until none is
  left;
* **takes test calls.** A browser or phone test runs every workflow; a setting
  not filled yet runs on test data, marked as test data in the workflow's answer
  (`testData`, and a sentence in its detail): a test calendar for a calendar not
  chosen, nobody available for a transfer with nobody chosen (even on a
  real-effect phone test), any answer for a question with no answers yet, and
  nothing done for a tag not chosen. Build with AI's roleplay sets it up for
  test calls; the agent then reads `setup.testOnly: true`;
* **is not prepared or put live.** [Prepare agent](/api-reference/tfu-live/prepare-tfu-live-agent)
  is refused with `setup_incomplete`, naming the first item (`workflowId`,
  `stepId`, `field`) and listing every one in `setupItems`; a campaign whose
  agent takes test calls only cannot be activated; and the runtime refuses its
  real calls and texts.

```json theme={"dark"}
{
  "success": false,
  "error": "Fill what this agent still needs before it is set up: Book appointment: Choose the calendar to search.",
  "code": "setup_incomplete",
  "workflowId": "wf_book0001",
  "stepId": "slots",
  "field": "calendarId",
  "setupItems": [
    { "kind": "workflow_setting", "workflowId": "wf_book0001", "workflowName": "Book appointment", "stepId": "slots", "node": "find_slots", "field": "calendarId", "message": "Choose the calendar to search." }
  ]
}
```

Once every setting is filled, `setup.items` is
empty and the agent is prepared and goes live like any other.

## Layer 1 variables

Every TFU Live conversation carries the Layer 1 variables an ordinary call gets,
with the same names and meanings, on every channel: outbound calls and callbacks,
inbound calls, texts and tests. Write them as `{{token}}` in Brain or Voice;
each is always present, empty when unknown. The runtime's list is
`LAYER_ONE_VARIABLES` in `operations/infrastructure/v2/gpt-live/template-variables.js`.

| Variable | Belongs to | Meaning | On a browser test |
| - | - | - | - |
| `first_name` | lead | The lead's first name | The tester's name, or Tester |
| `full_name` | lead | The lead's first and last name | The same as first\_name |
| `email` | lead | The lead's email address | Empty |
| `phone` | lead | The lead's phone number | Empty |
| `caller_pref` | lead | What the lead asked us to remember about how to speak with them | Empty |
| `contact_id` | lead | The lead's contact id in the CRM | The test contact's id |
| `date_created` | lead | When the lead was created in the CRM | When the test started |
| `business_name` | sub-account | The business the agent speaks for | The sub-account's own |
| `knowledge_base` | sub-account | The sub-account's knowledge base text | The sub-account's own |
| `location_id` | sub-account | The sub-account's id | The sub-account's own |
| `timezone` | call | The timezone times are spoken in: the lead's when the campaign checks it, otherwise the business's | The business's |
| `current_time` | call | The date and time the conversation started, in `timezone` | The real time |
| `direction` | call | `outbound` when we reached out, `inbound` when the lead called or texted us | `outbound` |
| `is_callback` | call | `true` when this conversation continues an earlier one (a callback the lead asked for, a dropped call being resumed, the lead returning our missed call), otherwise `false` | `false` |
| `context` | call | Why this conversation is happening, in a few plain sentences | That the person started a test |
| `treatment` | call | The one treatment (and location) a treatment campaign is about; empty otherwise | The bound campaign's |
| `campaign_id` | call | The campaign this conversation belongs to | The agent's campaign |
| `campaign_name` | call | That campaign's name | The agent's campaign |

A phone test calls a real contact and carries its real values. Contact fields
(`{{email}}`, a custom field key, `custom_values.<key>`) are filled beside them.
An ordinary call's objective variables are not carried, because workflows do
that work on the call: `availability` and `start_time` (Find slots and Cancel
appointment read the calendar), and `transfer_number` (the Transfer node holds
who takes it).

## Errors

Every refusal is one shape:

```json theme={"dark"}
{
  "success": false,
  "error": "Choose an active calendar verified for this sub-account.",
  "code": "workflow_resource_not_found",
  "workflowId": "wf_book0001",
  "stepId": "slots",
  "field": "routing[1].calendarId"
}
```

`error` is what to do, in words. Branch on `code`:

| Code | Status | Meaning |
| - | - | - |
| `invalid_workflow` | 422 | A workflow's structure or a node's settings, from the product's checks or the runtime's |
| `setup_incomplete` | 422 | A setting still to fill or a template placeholder still in the prompt, where the agent must be complete: preparing it, or a save that sets it up. `setupItems` lists every one ([Setup items](#setup-items)) |
| `workflow_resource_not_found` | 422 | A step names a calendar, custom field, pipeline stage, team or rep the sub-account does not have, or an inactive calendar |
| `workflow_resource_unavailable` | 503 | The sub-account's resources could not be read to check a step; retry |
| `workflow_secret_refused` | 422 | A secret header rule (above) |
| `invalid_agent` | 422 | Any other refused part of the agent |
| `invalid_request` | 400 | A malformed query or body |
| `unauthorized`, `forbidden` | 401, 403 | Credentials or access |
| `insufficient_balance` | 402 | The wallet cannot pay for a Build with AI turn |
| `not_found` | 404 | The agent, sub-account or workflow is not available to this caller |
| `conflict` | 409 | The agent changed: read it and review again |
| `too_large` | 413 | The body is too large |
| `provider_unavailable`, `internal_error` | 502, 503, 500 | Try again after reading the current state |
| `AGENT_OPERATION_UNSUPPORTED`, `TFU_LIVE_CAMPAIGN_PENDING` | 422, 503 | Named product states, unchanged |

`workflowId`, `stepId` and `field` name where a workflow refusal is. A workflow
being created has no id yet, so its refusals name only the step. A create from
a Brain template names the value: `field` is `values.<id>`. A
`setup_incomplete` refusal also carries `setupItems`.

## Examples

Create a workflow the client built:

```json theme={"dark"}
{
  "ifVersion": "REPLACE_WITH_LOADED_VERSION",
  "workflow": {
    "name": "Look up the account",
    "description": "Use when the caller gives an account number.",
    "steps": [
      { "id": "ask", "type": "collect_information", "config": { "items": [{ "id": "account", "label": "Account number", "type": "text" }] } },
      {
        "id": "lookup",
        "type": "webhook_request",
        "config": {
          "url": "https://api.example.com/accounts",
          "method": "POST",
          "headers": { "Authorization": { "secret": { "value": "Bearer REPLACE_WITH_TOKEN" } } },
          "fields": [{ "name": "account", "source": "collected", "item": "account" }],
          "resultMapping": { "plan": "account.plan" }
        },
        "outputs": {
          "success": [{ "id": "found", "type": "steer_agent", "config": { "guidance": "Their plan is {{response.plan}}." } }],
          "failed": [{ "id": "missing", "type": "steer_agent", "config": { "guidance": "The account was not found. Ask them to check the number." } }]
        }
      }
    ]
  }
}
```

Check a map without saving it:

```json theme={"dark"}
{
  "workflows": {
    "wf_callbk01": {
      "id": "wf_callbk01",
      "name": "Schedule a callback",
      "description": "Use when the lead wants the AI to call them back later.",
      "steps": [
        { "id": "time", "type": "collect_information", "config": { "items": [{ "id": "callback_time", "label": "When to call back", "type": "text" }] } },
        { "id": "schedule", "type": "schedule_ai_call", "config": { "timeFrom": "callback_time" } }
      ]
    }
  }
}
```

## Related

* [Build a TFU Live agent](/tfu-live/authoring)
* [Capabilities and workflows](/agents/tfu-live-capabilities)
* [TFU Live API overview](/api-reference/tfu-live)
