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 atbrain.capabilityConfig.workflows, keyed by their id:
- A workflow is
{ id, name, description, steps }.idiswf_and 8 lowercase letters or digits, stable for the workflow’s life.nameis 1 to 60 characters,description1 to 3000. - A step is
{ id, type, label?, config, outputs? }.idis up to 40 ofa-z 0-9 _ -, unique in the workflow.labelis up to 120 characters. outputsexists 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 answersdata.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, andresponse.<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
instantandafternodes 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.nativeCrmistrue,tagNodesisfalse, and a save holding a tag node is refused. The overview of a sub-account also saysnativeCrm. - 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’sfield) by id; the custom fields it reads (a Condition case’sfield, Check zip code’sfield, 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 (repsandteamslookups). 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.itemsas a setup item. An agent that is set up on every save (not an explicit draft) refuses it assetup_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’sarguments. 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 optionalquery (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 }:
-
descriptionsays the business logic in words: who it is for, what the call does, the fallback, and that texting is off. -
directionisoutbound,inboundoreither: the campaigns it suits. -
logicis the same logic in a few values, so a builder can match an agent’s logic to a template: -
workflowsare keyed by a name local to the template. In a step’sconfig,{ "$fill": id }stands for a setting filled fromvalues.<id>(assays how: a list, the answers, the calendars, routing rules), and{ "$workflow": key }for the id another of its workflows gets in the agent. -
promptis 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, soplaceholdersis always empty: the builder writes the business’s own lines and FAQs. -
toFilllists the settings the client fills for their business:id(its key invalues),label,kind,value(its JSON Schema) andsettings, every{ workflow, step, setting }it fills. Every other setting is valid as published.
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:
{ 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.
- 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).
namedefaults to the template’s name. - Its workflow ids are derived from the agent
id: a retry with the sameidand body is the same agent; other values for thatidare a409. - It is an explicit draft (
provisioningPolicy: "explicit",setupStarted: false), linked to a paused campaign like every new agent, and itssetup.itemssay what it still needs (below). - The response is the create’s:
data.agent(with its workflows) anddata.campaign.
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".
Setup items
What an agent still needs before it can be prepared for real calls. Every read of an agent carries them insetup.items, each { kind, workflowId?, workflowName?, stepId?, node?, field?, placeholder?, message }:
workflow_setting: a setting still to fill, where it is (workflowId,stepId,fieldsuch ascalendarId,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.
- 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 readssetup.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 insetupItems; a campaign whose agent takes test calls only cannot be activated; and the runtime refuses its real calls and texts.
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.
