Skip to content

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:

StatusMeaning
pendingCreated but queued behind another active run
activeCurrently driving the agent loop
completedReached a terminal state successfully
abandonedSuperseded 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.

TypeBehavior
ConversationMulti-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.
ActionThe assistant acts autonomously without waiting for user input. Used for processing, transformations, API calls, or any step that does not require human interaction.
LogicDeterministic 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.
OutputRender 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.
ParseExtract 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.
ApprovalRequest 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.
WaitPause 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.
TaskFan 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.
DelegateHand off the conversation to another assistant. The delegated assistant runs independently, and the workflow resumes when it finishes. Useful for specialized sub-processes.
SubworkflowNest 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 TypePorts
Outputdone
Parsecomplete, incomplete
Approvalapproved, rejected, discussion, timeout
Waitreceived, 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.

States that use named output ports (Output, Parse, Approval, Wait) do not use traditional guard-based transitions. Their outgoing connections are always port-based. Other state types (Conversation, Action, Logic, Task, Delegate, Subworkflow) continue to use guard-based transitions as before.

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:

FieldDescription
templateA Jinja2 body template rendered against gathered data. Use {{ gathered_data.field_key }} for interpolation.
subject_templateAn optional subject line template (used for email and webhook targets).
targetWhere to deliver the rendered content: conversation (reply in the chat), email (send an email), or webhook (POST to an endpoint).
recipientsA 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_typeThe 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:

FieldDescription
fieldsA 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).
sourceWhere to read the input text: last_user_message (the most recent user message) or a gathered data reference like gathered_data.email_body.
instructionsOptional additional instructions for the LLM to guide extraction (e.g., “Dates should be in YYYY-MM-DD format”).
modelThe 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:

FieldDescription
approversA list of approver addresses or gathered data references. Supports static email addresses, {gathered_data.key} references, and conversation_contact.
request_templateThe 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_hintsOptional 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"}).
channelThe delivery channel for the approval request: email (default).
escalationOptional 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:

FieldDescription
wait_forThe 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_hoursHow long to wait before the timeout port fires. Required for email_reply and webhook wait types. For timer, this is the delay duration.
classifyOptional 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:

FieldDescription
on_errorThe 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_policyRetry 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 boundaries work with all state types. For states that already have named output ports, the 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.

GuardFires when…Configuration
all_required_answeredAll required data fields for the current state have been recorded via record_data.keys: list of required field names
tool_calledA 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_containsA gathered data key exists and optionally matches a pattern.key: data field name; pattern: optional regex to match against the value
data_equalsA gathered data key exactly equals a specified value.key: data field name; value: exact value to match
data_greater_thanA gathered data key is numerically greater than a value.key: data field name; value: numeric threshold
data_less_thanA gathered data key is numerically less than a value.key: data field name; value: numeric threshold
data_in_listA gathered data key is one of a set of allowed values.key: data field name; values: list of allowed values
user_confirmedThe 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_judgeAn 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_completeAll sub-tasks created by a Task state have been completed.No configuration needed
external_eventAn external system has sent an event via the webhook endpoint.event_key: the event type string to match in gathered data
alwaysFires unconditionally. Use for automatic transitions out of Action or Logic states.No configuration needed
compositeCombines multiple child guards with AND/OR logic.operator: and or or; guards: list of child guard definitions
Use the llm_judge guard sparingly – it makes an additional LLM call on each evaluation. For most cases, data_equals, data_contains, or tool_called are cheaper and more predictable.

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 budget is 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.

ReducerBehavior
overwriteReplace the previous value entirely (default)
appendTreat the field as a list and append new items
unique_appendAppend but skip duplicate values
merge_dictShallow-merge new keys into an existing dictionary
incrementAdd 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}).

OperationPurpose
set_dataAssign a static or template-interpolated value to a gathered data key.
transformApply a transformation to a data value: uppercase, lowercase, trim, split, join, round, to_number, to_string.
http_requestCall an external API. Supports GET/POST/PUT/DELETE, template interpolation in URL and body, and auto-parses JSON responses.
send_notificationSend an email, chat message, or webhook. Recipients can be static addresses, gathered data references, the conversation contact, or all task participants.
conditional_branchRoute 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.
waitPause execution for a specified duration (in hours).
loopIterate 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.

Logic states with a conditional_branch operation can route to different states based on data values – this works like a switch statement in the workflow graph. The operation’s 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:

  1. The workflow pauses and sends an approval request to the configured approval_contacts
  2. The conversation enters a waiting state until an approver acts
  3. Approvers review the request via a dedicated approval page (/workflows/approve/<conversation_id>)
  4. On approval, the workflow resumes and fires the pending transition
  5. 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
  }
}
FieldDefaultDescription
max_attempts1Total attempts allowed (1 means no retries)
backoff_seconds0Suggested 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.