Core Concepts
A workflow is a state machine. It defines a set of states connected by transitions, with guards controlling when transitions fire. As the workflow executes, it accumulates gathered data that subsequent states can reference. Each execution of a workflow is tracked as a workflow run.
Workflow Runs
A workflow run represents a single execution of a workflow within a conversation. Runs track the current state, gathered data, transition history, and completion result independently. A conversation can have multiple runs – queued, active, completed, or abandoned – which enables multi-intent handling and workflow switching without losing history.
Run lifecycle:
| Status | Meaning |
|---|---|
| pending | Created but queued behind another active run |
| active | Currently driving the agent loop |
| completed | Reached a terminal state successfully |
| abandoned | Superseded by a workflow switch or discarded |
When a conversation starts, the agent detects intent from the user’s opening message and creates one or more pending runs. The first run is activated immediately. When it completes, the next pending run (if any) is activated automatically. If the user’s first message matches multiple workflows, all matching runs are queued and executed in sequence.
States
States are the phases of your workflow. Each state has a name, optional instructions for the assistant, and a type that determines its behavior.
| Type | Behavior |
|---|---|
| Conversation | Multi-turn interaction with a user. The assistant follows the state’s instructions, uses available tools, and collects information. The workflow stays in this state until a transition guard is satisfied. |
| Action | The assistant acts autonomously without waiting for user input. Used for processing, transformations, API calls, or any step that does not require human interaction. |
| Logic | Deterministic processing without LLM involvement. Executes a sequence of operations (data transforms, HTTP requests, notifications, conditional branching) and advances automatically. Use for steps that don’t need AI reasoning. |
| Output | Render a template and deliver it to a target. The workflow renders a Jinja2 template against gathered data, sends the result to the configured target (conversation, email, or webhook), and advances through the done port. See Output State. |
| Parse | Extract structured fields from text using an LLM. Define the fields you need (name, type, description, required), point at a source text, and the LLM fills them in. Exits through complete when all required fields are extracted, or incomplete when some are missing. See Parse State. |
| Approval | Request human approval via email. The workflow sends an approval request to the configured approvers and waits for a response. Exits through approved, rejected, discussion, or timeout depending on the outcome. See Approval State. |
| Wait | Pause the workflow until an external event arrives. The workflow waits for an email reply, a webhook call, or a timer to expire. Exits through received and timeout by default, or through custom classification ports if response classification is enabled. See Wait State. |
| Task | Fan out work to multiple participants. The workflow creates a sub-task for each participant (resolved from a task template), waits for all to complete, then advances. |
| Delegate | Hand off the conversation to another assistant. The delegated assistant runs independently, and the workflow resumes when it finishes. Useful for specialized sub-processes. |
| Subworkflow | Nest another workflow as a child process. The child workflow runs to completion, then control returns to the parent. Input and output mappings transfer data between parent and child gathered data. |
Transitions
Transitions are directed edges between states. Each transition has a source state, a target state, and one or more guards. When all guards on a transition evaluate to true, the workflow advances to the target state.
A state can have multiple outgoing transitions. The engine evaluates them in order and takes the first one whose guards pass. If no transition fires, the workflow remains in the current state.
Named Output Ports
Some state types have named output ports – predefined exit points that determine which transition the workflow follows when the state completes. Instead of configuring guards on outgoing transitions, you connect each port to a target state directly. The workflow engine routes execution to the correct target based on which port fires.
| State Type | Ports |
|---|---|
| Output | done |
| Parse | complete, incomplete |
| Approval | approved, rejected, discussion, timeout |
| Wait | received, timeout (or custom classification ports) |
Port-based transitions require no guard configuration. The state itself decides which port to activate based on its outcome. In the visual editor, ports appear as colored labels on the node, and you drag connections from a specific port to the target state.
Output State
An Output state renders a template and delivers the result to a target. Use it for sending confirmation messages, notification emails, webhook payloads, or any formatted content that doesn’t require further interaction.
Configuration:
| Field | Description |
|---|---|
| template | A Jinja2 body template rendered against gathered data. Use {{ gathered_data.field_key }} for interpolation. |
| subject_template | An optional subject line template (used for email and webhook targets). |
| target | Where to deliver the rendered content: conversation (reply in the chat), email (send an email), or webhook (POST to an endpoint). |
| recipients | A list of recipient addresses or gathered data references. Required for email and webhook targets. Supports static addresses, {gathered_data.key} references, and conversation_contact. |
| content_type | The format of the rendered template: text, html, or markdown. Defaults to text. |
After delivery, the state exits through the done port.
Parse State
A Parse state uses an LLM to extract structured fields from a source text. Define the fields you need and the engine prompts the LLM to fill them from the source. This is useful for pulling structured data out of freeform user messages, email bodies, or any text stored in gathered data.
Configuration:
| Field | Description |
|---|---|
| fields | A list of field definitions. Each field has: name (the gathered data key to store the value), type (string, number, boolean, date, list), description (what the field represents – given to the LLM as guidance), and required (whether the field must be present for extraction to be considered complete). |
| source | Where to read the input text: last_user_message (the most recent user message) or a gathered data reference like gathered_data.email_body. |
| instructions | Optional additional instructions for the LLM to guide extraction (e.g., “Dates should be in YYYY-MM-DD format”). |
| model | The LLM model to use for extraction. Defaults to claude-haiku-4-5. |
Ports:
- complete – All required fields were successfully extracted. The extracted values are stored in gathered data under their respective field names.
- incomplete – One or more required fields could not be extracted. Partial results are still stored. Connect this port back to a Conversation state to ask the user for the missing information, then loop back to the Parse state.
Approval State
An Approval state sends an approval request to one or more approvers and waits for a response. Unlike the approval gate feature (which is a modifier on any state), the Approval state is a dedicated state type with email-native approval, response classification, and escalation support.
Configuration:
| Field | Description |
|---|---|
| approvers | A list of approver addresses or gathered data references. Supports static email addresses, {gathered_data.key} references, and conversation_contact. |
| request_template | The approval request content, with two sub-fields: subject_template (email subject line) and body_template (email body). Both support Jinja2 template interpolation against gathered data. |
| classification_hints | Optional hints to help the LLM classify the approver’s response into the correct port (e.g., {"approved": "yes, approve, looks good", "rejected": "no, deny, reject"}). |
| channel | The delivery channel for the approval request: email (default). |
| escalation | Optional escalation configuration: timeout_hours (how long to wait before escalating), escalation_contacts (who to escalate to), and max_escalations (maximum number of escalation rounds before the timeout port fires). |
Ports:
- approved – The approver approved the request.
- rejected – The approver rejected the request.
- discussion – The approver replied with a question or comment rather than a clear approval or rejection. Connect this port to a Conversation state to handle the discussion, then loop back to the Approval state.
- timeout – The approval timed out after all escalation attempts were exhausted.
Wait State
A Wait state pauses the workflow until an external event occurs. Use it when the workflow needs to wait for an email reply, a webhook callback, or a timer to expire before continuing.
Configuration:
| Field | Description |
|---|---|
| wait_for | The type of event to wait for: email_reply (a reply to a previously sent email), webhook (an incoming webhook call), or timer (a time-based delay). |
| timeout_hours | How long to wait before the timeout port fires. Required for email_reply and webhook wait types. For timer, this is the delay duration. |
| classify | Optional response classification configuration. When enabled, the incoming response is classified into custom ports instead of the default received port. Sub-fields: enabled (boolean), classification_ports (list of custom port names), and classification_hints (hints for each port to guide classification). |
Ports (default):
- received – The expected event arrived within the timeout window.
- timeout – The timeout period elapsed without receiving the expected event.
Ports (with classification enabled):
When classify.enabled is true, the received port is replaced by the custom ports defined in classify.classification_ports. The incoming response is classified by the LLM using the provided hints, and the workflow exits through the matching custom port. The timeout port remains available.
Error Handling
Each state can define an error boundary that controls what happens when the state encounters an error during execution. This provides per-state error handling without requiring manual intervention.
Configuration:
| Field | Description |
|---|---|
| on_error | The error handling strategy: port (immediately route to the error port), retry_then_port (retry according to the retry policy, then route to the error port if all retries are exhausted), or ignore (log the error and continue as if the state succeeded). |
| retry_policy | Retry settings used when on_error is retry_then_port: max_attempts (total attempts before giving up) and backoff_seconds (delay between retries). |
When on_error is set to port or retry_then_port, the state gains an additional error output port. Connect this port to a recovery state (e.g., a Conversation state that informs the user, or an Output state that sends an error notification).
{
"on_error": "retry_then_port",
"retry_policy": {
"max_attempts": 3,
"backoff_seconds": 10
}
}error port is added alongside the existing ports. For guard-based states, the error port provides a port-based exit in addition to the normal guard-based transitions.Guards
Guards are conditions attached to transitions. They control when the workflow moves forward.
| Guard | Fires when… | Configuration |
|---|---|---|
| all_required_answered | All required data fields for the current state have been recorded via record_data. | keys: list of required field names |
| tool_called | A specific tool has been called during the current state. | tool_name: tool to watch for; require_success: whether the call must succeed (default true) |
| data_contains | A gathered data key exists and optionally matches a pattern. | key: data field name; pattern: optional regex to match against the value |
| data_equals | A gathered data key exactly equals a specified value. | key: data field name; value: exact value to match |
| data_greater_than | A gathered data key is numerically greater than a value. | key: data field name; value: numeric threshold |
| data_less_than | A gathered data key is numerically less than a value. | key: data field name; value: numeric threshold |
| data_in_list | A gathered data key is one of a set of allowed values. | key: data field name; values: list of allowed values |
| user_confirmed | The user has explicitly confirmed or agreed (detected by keyword matching). | patterns: optional list of custom confirmation phrases (defaults to yes, sure, ok, confirmed, etc.) |
| llm_judge | An LLM evaluates a custom rubric against the conversation and returns a verdict. | rubric: natural language condition to evaluate; model: LLM model to use (default claude-haiku-4-5) |
| all_tasks_complete | All sub-tasks created by a Task state have been completed. | No configuration needed |
| external_event | An external system has sent an event via the webhook endpoint. | event_key: the event type string to match in gathered data |
| always | Fires unconditionally. Use for automatic transitions out of Action or Logic states. | No configuration needed |
| composite | Combines multiple child guards with AND/OR logic. | operator: and or or; guards: list of child guard definitions |
Gathered Data
As the workflow executes, the assistant collects key-value data using the record_data tool. This data persists across states and is available to:
- Guard conditions – e.g., transition when
budgetis greater than 10000 - State instructions – reference gathered data in prompts using
{gathered.field_key} - Task templates – resolve participant lists from gathered data
- Operations – template interpolation using
{gathered_data.field_key}with dot-path notation for nested values - Downstream states – any later state can read what earlier states recorded
Gathered data is stored on the workflow run and visible in the workflow progress panel.
Reducers
By default, recording a value to an existing key overwrites it. A state schema on the workflow lets you define alternative merge strategies (reducers) for specific keys. This is useful when multiple states contribute to the same field.
| Reducer | Behavior |
|---|---|
| overwrite | Replace the previous value entirely (default) |
| append | Treat the field as a list and append new items |
| unique_append | Append but skip duplicate values |
| merge_dict | Shallow-merge new keys into an existing dictionary |
| increment | Add a numeric value to the existing total |
Define reducers in the workflow’s state_schema:
{
"emails": {
"type": "list",
"reducer": "append",
"description": "Email addresses collected across states"
},
"approval_count": {
"type": "number",
"reducer": "increment"
},
"preferences": {
"type": "dict",
"reducer": "merge_dict"
}
}Valid types are string, number, list, dict, boolean, and any.
Global Tools
Global tools are tools available in every state of the workflow, regardless of the state’s own tool configuration. Use these for capabilities that should always be accessible – for example, record_data is typically a global tool so the assistant can collect information in any state.
Configure global tools in the workflow editor’s settings panel.
Operations
Logic states execute a sequence of deterministic operations without involving the LLM. Operations run in order and can read from and write to gathered data using template interpolation ({gathered_data.key.path}).
| Operation | Purpose |
|---|---|
| set_data | Assign a static or template-interpolated value to a gathered data key. |
| transform | Apply a transformation to a data value: uppercase, lowercase, trim, split, join, round, to_number, to_string. |
| http_request | Call an external API. Supports GET/POST/PUT/DELETE, template interpolation in URL and body, and auto-parses JSON responses. |
| send_notification | Send an email, chat message, or webhook. Recipients can be static addresses, gathered data references, the conversation contact, or all task participants. |
| conditional_branch | Route to different target states based on conditions. Supports operators: eq, neq, gt, gte, lt, lte, in, not_in, contains, matches (regex). First matching condition wins. |
| wait | Pause execution for a specified duration (in hours). |
| loop | Iterate over a list in gathered data, executing child operations for each item. |
Operations are configured in the state definition’s operations list. Each entry has a type and a config object specific to that operation type.
target_state in each condition overrides the state’s normal transitions.Approval Gates
Any state can require human approval before its outgoing transition fires. Set require_human_approval: true on the state definition to enable this.
When the workflow reaches an approval gate:
- The workflow pauses and sends an approval request to the configured
approval_contacts - The conversation enters a waiting state until an approver acts
- Approvers review the request via a dedicated approval page (
/workflows/approve/<conversation_id>) - On approval, the workflow resumes and fires the pending transition
- On rejection, the workflow records the rejection reason and the rejector
Approval contacts use the same recipient syntax as the send_notification operation – static email addresses, gathered data references, or conversation_contact.
Retry Policies
States can define a retry policy that controls how many times the state can be retried if an action fails. The assistant uses the retry_state tool to request a retry, and the engine enforces the configured limits.
{
"retry": {
"max_attempts": 3,
"backoff_seconds": 5
}
}| Field | Default | Description |
|---|---|---|
| max_attempts | 1 | Total attempts allowed (1 means no retries) |
| backoff_seconds | 0 | Suggested delay between retry attempts |
When the retry budget is exhausted, the retry_state tool returns an error and the workflow remains in the current state for manual intervention or an alternative transition.
Workflow Switching
When an assistant has multiple workflows, the agent can switch between them mid-conversation using the switch_workflow tool. This is useful when a user starts a conversation about one topic but shifts to another.
When a switch happens:
- The current workflow run is marked as abandoned and recorded in the history
- A new workflow run is created and activated with the target workflow’s starting state
- Previously gathered data is preserved and carried over to the new run
- The conversation continues seamlessly
The agent sees all available workflows in its system prompt and decides whether to switch based on the conversation context. Switching is only available when the assistant has more than one workflow attached.
Terminal States
A terminal state is any state with no outgoing transitions. When the workflow reaches a terminal state, the process is complete. The assistant delivers any final response defined in the terminal state’s instructions and the workflow is marked as finished.
Technical Details
State machine evaluation – After each agent turn, the workflow engine evaluates all outgoing transitions from the current state in defined order. Guard evaluation is synchronous and blocking – the engine must determine the next state before the conversation continues. Guards that require LLM calls (like llm_judge) add latency to this evaluation.
Deterministic state draining – When a transition leads to a Logic or Output state (or any state with execution_mode: deterministic), the engine executes its operations immediately without returning to the LLM. If the resulting state is also deterministic, the engine continues draining until it reaches an LLM-driven state or a terminal state. This means a chain of Logic and Output states executes in a single pass.
Port-based routing – States with named output ports (Output, Parse, Approval, Wait) bypass the guard evaluation system entirely. The state itself determines which port fires based on its execution outcome. The engine looks up the target state connected to that port and transitions directly. This is simpler and more predictable than guard-based routing for states with well-defined outcomes.
Error boundary execution – When a state with an on_error configuration encounters an error, the engine catches the exception and applies the configured strategy. For retry_then_port, the engine re-enters the state up to max_attempts times with backoff_seconds delay between attempts. If all retries are exhausted, the engine routes to the state connected to the error port. For port, the engine routes to the error port immediately. For ignore, the engine logs the error and proceeds as if the state completed normally.
Gathered data storage – Gathered data is stored as a JSON object on the workflow run record. The record_data tool merges new key-value pairs into this object, applying the configured reducer if a state schema is defined. Keys are strings; values can be strings, numbers, booleans, arrays, or objects.
Delegate mechanics – When a Delegate state is entered, the engine creates a child conversation with the target assistant. The child assistant runs through its own agent loop (which may itself be a workflow). When the child conversation completes, control returns to the parent workflow and the next transition is evaluated.
Subworkflow mechanics – When a Subworkflow state is entered, the engine initializes a child workflow run using the referenced workflow definition. The input_mapping copies specified keys from the parent’s gathered data into the child run. When the child run completes, the output_mapping copies keys back to the parent. The parent then evaluates its outgoing transitions.
Run history – Each workflow run maintains a workflow_history list tracking every state entry and exit with timestamps and the guard that triggered the transition. This provides a full audit trail of the run’s execution path.
Workflow tools – Four tools are always available during workflow execution: record_data (collect information), signal_completion (signal that the current state’s work is done), retry_state (retry a failed state within its retry budget), and switch_workflow (switch to a different workflow). See Built-in Tools for details.