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. 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. 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 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?, outputLabels? }. id is up to 40 of a-z 0-9 _ -, unique in the workflow. label is up to 120 characters.
  • outputLabels optionally 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.
  • 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, 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, 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.
  • 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_teammate and send_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.nativeCrm is true, tagNodes is false, 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 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 and listed by the calendars lookup for their system; 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 workflow books on calendars from one system: every Find slots calendarId and routing[].calendarId and every Cancel appointment calendarIds entry, 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 a name: 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 with calendar_not_on_crm), so it can be replaced one setting at a time. A new or changed workflow that mixes them is refused with invalid_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 with invalid_workflow, with stepId pointing 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 kind the custom_fields and contact_fields lookups give). A Collect information item saved to a field takes the answer type of its kind: phone for a phone field, email, date, number for a number or amount, choice of the field’s options for a dropdown, multi_choice (or choice) of its options for a multi-select, boolean for 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 as YYYY-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 (kind unsupported) 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 as YYYY-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.items as a setup item. An agent that is not an explicit draft refuses it on save as setup_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) and end_call. Neither can be removed, and a save adds them to an older agent that lacks them. end_call no 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 are brain.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 are brain.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:
On the call in Settings includes When the call connects, stored at 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’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; 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 optional query (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 }:
  • 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.
  • Creation automatically requests preparation for testing (setupStarted: true), with a paused campaign like every new agent. While settings remain incomplete, it keeps provisioningPolicy: "explicit" and runs only test calls; setup.items lists what still needs filling before real calls. Read setup.status for progress.
  • 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. 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.

Examples

Create a workflow the client built:
Check a map without saving it: