Skip to main content
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. Two more routes sit beside the agent: List TFU Live Brain templates (GET /api/tfu-live-agents/brain-templates) and Create agent from one of them (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, which still replaces the whole agent, workflows included.

The stored shape

Workflows live at brain.capabilityConfig.workflows, keyed by their id:
  • 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 answers data.workflows, the model as the runtime publishes it: 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 refuses any agent that still has a setup item. See 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:
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:

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 takes a kind and an optional query (a name or an exact id): 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. 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:
  • 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: {{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:
To create an agent from one, send { id, brainTemplateId, values?, name? } to Create 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.
Values are applied when given, all or some of them:
  • 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".
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 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.
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. 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:
error is what to do, in words. Branch on code: 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:
Check a map without saving it: