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. Setup starts as after
Update agent, which still
replaces the whole agent, workflows included. An explicit draft with setup items
is set up for test calls only.
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?, outputLabels? }.idis up to 40 ofa-z 0-9 _ -, unique in the workflow.labelis up to 120 characters. outputLabelsoptionally maps existing output ids to display names of up to 120 characters. It changes only the labels in the editor and canvas; conditions, ids and execution stay the same. Omit a label to use its default.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, its optional instructions at most 1500 and its
optional holdSound one of soft_pad, the default, light_piano or silence;
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, check_roof, hand_over 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. - Put on DND ends the lead’s journey. Only internal steps can follow it on its
path, in one list, and they still run for the stopped lead:
add_tag,remove_tag,set_field,move_stage,add_note,notify_teammateandsend_webhook. Nothing that contacts the lead or needs the AI on the call can follow it, and no Condition. 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. It has no pipelines or email either, so Move stage and Send email are refused there too. The two rules compose: on the native CRM only Set field, Add note, Notify teammate and Send webhook can follow Put on DND, and the refusal names only those. 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 and listed by thecalendarslookup for their system; 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 workflow books on calendars from one system: every Find slots
calendarIdandrouting[].calendarIdand every Cancel appointmentcalendarIdsentry, on any branch, is native (cal_and 20 hex characters), Cal.com (calcom:<event type id>), Calendly (calendly:<event type uuid>) or GoHighLevel (any other id without aname:prefix), never two of them. A calendar still to fill counts for none, and so does one the sub-account’s CRM cannot book on (a GoHighLevel calendar on a native sub-account, which going live refuses withcalendar_not_on_crm), so it can be replaced one setting at a time. A new or changed workflow that mixes them is refused withinvalid_workflow. - A Book appointment whose Find slots names a Cal.com or Calendly calendar needs the
caller’s email first: a Collect information item, required, saving to
contact.email, earlier on its path (before it in its own steps, or before the branch it is in). A new or changed workflow without one is refused withinvalid_workflow, withstepIdpointing at the Book appointment. The booking itself asks the caller only for the time, and for their timezone when times are offered in it and none is on file. - What a step saves to a field must fit the field’s kind (the
kindthecustom_fieldsandcontact_fieldslookups give). A Collect information item saved to a field takes the answer type of its kind:phonefor a phone field,email,date,numberfor a number or amount,choiceof the field’s options for a dropdown,multi_choice(orchoice) of its options for a multi-select,booleanfor yes or no; a text field takes any single answer. A fixed Set field value must be one the field holds: a number written plainly, a full phone number with its country code (+14155550100), an email address, a date asYYYY-MM-DD, one of a dropdown’s options, or a multi-select’s options separated by commas. A file upload, signature or text box list field (kindunsupported) cannot be set. A value made from{{references}}is checked on the call, where a value that does not fit is not saved. An item saved before, unchanged, is not checked again. An item saved to no field is kept for the call only. - On a call, a phone number is saved in E.164: one given without its country
code takes the sub-account’s country (its calling line’s, else its
timezone’s) where that country’s numbers are known, and is asked for again
otherwise (a save made after the call keeps one it cannot place as given). A
trunk 0 after the country code (
+44 07700 900123) and an extension are left out. An email is saved lowercased, a date asYYYY-MM-DD, several choices as the list of options. - 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 not an explicit draft refuses it on save assetup_incomplete, and Prepare agent refuses any agent that still has a setup item. See Setup items. - Every agent must retain Stop contact, including conversation-only agents. Its workflow id, name and first Put on DND action cannot be deleted, replaced or placed behind a condition. API and AI edits follow the same rule. It has its own slot alongside twenty custom workflows; an older agent missing it gains it on save/build. Internal steps after DND remain editable.
- New agents also start with Schedule a callback unless already covered or
their configuration cannot use it, and always have two always-on tools:
Return the delegated result (
respond_to_voice) andend_call. Neither can be removed, and a save adds them to an older agent that lacks them.end_callno longer ends a call: the agent says goodbye and the call ends on its own. Earlier conversations and Messages stay optional.
When the call ends
The agent never decides to hang up: it says goodbye, and the call ends on its own a moment later. Brain always includes When the call ends, the actions that run once as that happens. Its name and its last step, Call ends, are fixed and never stored; there is no trigger to write. These actions do not run when the caller hangs up first. For API edits, these actions arebrain.capabilityConfig.beforeEndCall.steps:
conditions, tag checks and direct actions (tags, fields, notes, pipeline changes,
alerts, SMS, email, webhooks and cancelling queued outreach). Do not write
brain.capabilityConfig.toolDescriptions.end_call; an agent’s existing one is
kept as it is.
After the call
After the call runs once the call has ended and post-call processing is complete, including when the caller hangs up. Its steps arebrain.capabilityConfig.afterCall.steps; do not store a hangup node there.
Supported actions include conditions, tags, fields, notes,
pipeline changes, alerts, SMS, email, postcode checks, webhooks with responses,
DND and cancellation of queued outreach. Normal CRM and delivery restrictions apply.
After DND, only internal actions may follow. Transfers, collecting answers, speech
steering, bookings, appointment cancellation and scheduling AI calls are unavailable
here because they need a live conversation or an AI-confirmed decision.
These steps read what the call was as call.<fact>: in text as {{call.summary}},
in a Condition as call.duration_seconds, and in a Webhook with response field
(source: "call"). Only these steps can read them, and a save refuses an unknown fact.
A Set field whose value names a call fact that comes back empty is skipped, so it
never clears the field. These steps save the name the caller gave on the contact,
and leave the contact’s name as it is when the caller gave none:
brain.capabilityConfig.opening as
{ waitSeconds }. An answered outbound call waits that long for the lead to speak first: 0 to 10 seconds, in half
seconds. Leaving it out waits 2 seconds, and 0 starts the agent the moment the call connects. Inbound calls and
browser tests always start right away.
Settings → On the call includes End silent calls, stored at brain.capabilityConfig.silence: an enabled flag,
a list of up to five check-ins (each with waitSeconds and message), and
endAfterSeconds. Each wait is 5–300 whole seconds. A caller response restarts the
sequence. Timers pause during agent speech, pending work and transfers.
New agents start with a check-in after 10 seconds, another after 10 more, then hangup
after another 5 seconds, measured after the last message finishes. Speaking and paused work do not count toward these waits.
Existing agents without a policy remain unchanged until you enable it.
Testing restrictions remain unchanged; a saved workflow does not prove execution.
Settings → On the call includes Maximum call duration, stored independently as
brain.capabilityConfig.maxCallDurationMinutes (1–90 whole minutes, default 15). It caps the agent’s call even with silence
handling disabled. Live phone calls, phone tests, ordinary browser tests and
website visitor calls all use the saved limit, or 15 minutes if it is omitted.
Clearing it restores that shared default. Builder coaching sessions retain
their own session cap. This caps the agent’s portion of a call; a completed
human transfer retains its separate line limit, never shorter than the agent limit.
It adds no model instructions, changes no after-call actions and does not rewrite
limits already saved on other agents.
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,
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;
the internal steps behind it still run, the ones that run after the answer listed
in pending as queued.
A run that did nothing (a workflow with no steps, or a Condition output with none)
answers success: false with nothing: true and a detail telling the AI not to
say anything was done; it is not a refusal.
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):
A field’s
kind is what its values are: text, long_text, number,
money, phone, email, date, yes_no, one_of (one of options),
many_of (several of options) or unsupported (a workflow cannot set it).
field is the reference a step names (a custom field of another model, such as
an opportunity, has none).
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. - Creation automatically requests preparation for testing (
setupStarted: true), with a paused campaign like every new agent. While settings remain incomplete, it keepsprovisioningPolicy: "explicit"and runs only test calls;setup.itemslists what still needs filling before real calls. Readsetup.statusfor progress. - 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.
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 the code, and code holds the same value, kept for integrations
built before error carried it. Branch on either. message is one plain
sentence for a person: show it, never parse it, because its wording can change.
A shared check (a missing key, a missing scope, a rate limit) answers with its
own error, such as Unauthorized or InsufficientScope, and no code. The
rest of the envelope is in Conventions.
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.
