Skip to main content
This reference documents all configuration options for Step Workflows. For an introduction to what Step Workflows are and when to use them, see the Overview.

Step Configuration

Each step in a workflow is defined with the following properties:
PropertyPurposeExample
idUnique step identifier"COLLECT_NAME"
goalBrief description (shown to the agent)"Collect the user's name"
instructionsAgent guidance (array of strings)["Ask for the user's full name."]
inputsParameters to collect (JSON Schema)[{name: "user_name", type: "string"}]
onLifecycle hooks for actions{start: [...], enter: [...], submit: [...]}
nextTransition routing["NEXT_STEP"] or [{if: "...", id: "..."}]
toolsTool visibility control{allow: [...], call: true}

id

A unique string identifier for the step. Used in:
  • Transition routing (next entries)
  • Manual navigation (go_to_step)
  • Debugging and logging
Best practice: Use SCREAMING_SNAKE_CASE for step IDs (e.g., COLLECT_EMAIL, VERIFY_DOB).

goal

A brief description of what this step accomplishes. This text becomes the submit tool’s description, helping the agent understand the purpose of the current step.
{
  "id": "COLLECT_NAME",
  "goal": "Collect the user's full name for the contact form"
}

instructions

Guidance for the agent on how to execute this step. The current implementation expects an array of instruction strings, even for a single line:
{
  "instructions": [
    "Ask the user for their full name. Be polite and professional."
  ]
}

{
  "instructions": [
    "Ask the user for their full name.",
    "If they only provide a first name, ask for their last name as well.",
    "Confirm the spelling if the name is unusual."
  ]
}
Instructions support template substitution for dynamic content:
{
  "instructions": [
    "Welcome back, {{user_name}}! Let's continue where we left off."
  ]
}

inputs

Defines the parameters the agent should collect before advancing. See Inputs Schema for full details.
{
  "inputs": [
    {"name": "first_name", "type": "string", "required": true},
    {"name": "email", "type": "string", "format": "email", "required": true},
    {"name": "phone", "type": "string", "required": false}
  ]
}

next

Controls workflow routing after successful submission. See Step Transitions for full details.
{
  "next": ["CONFIRM"]
}

{
  "next": [
    {"if": "inputs.contact_preference == 'phone'", "id": "SCHEDULE_CALL"},
    {"if": "inputs.contact_preference == 'email'", "id": "SCHEDULE_EMAIL"},
    {"id": "DEFAULT_FOLLOWUP"}
  ]
}

tools

Controls which tools the agent can access during this step. See Tool Configuration for full details.
{
  "tools": {
    "allow": ["submit_contact_form", "validate_email"],
    "call": true
  }
}

Lifecycle Hooks

Lifecycle hooks execute actions at specific points during step execution. Define them in the on property:
{
  "on": {
    "start": [...],
    "enter": [...],
    "presubmit": [...],
    "submit": [...]
  }
}

Event Summary

EventWhen TriggeredAllowed ActionsPrimary Use Cases
on.startFirst workflow turn, before the first on.enterset, inc, say, callOne-time initialization, first-turn side effects
on.enterEntering a step (before input collection)get, set, inc, say, callStep welcome messages, pre-populate inputs
on.presubmitAfter agent submits, before validationget, set, inc, saveDefault missing values, data transformation
on.submitAfter validation passesset, inc, say, save, callPersist data, trigger side effects

on.start

Executes once at workflow initialization, before on.enter on the first step. Common uses:
  • Initialize workflow-local state
  • Queue first-turn say or call actions
  • Seed values that the first step’s instructions depend on
{
  "on": {
    "start": [
      {"action": "set", "name": "local.user_language", "value": "English"}
    ]
  }
}
Restriction: Only the first step may define on.start.

on.enter

Executes when the workflow enters this step, before the agent begins input collection. Common uses:
  • Display step-specific greetings or progress indicators
  • Pre-populate inputs from existing data
  • Initialize step-local counters
{
  "on": {
    "enter": [
      {"action": "say", "text": "Step 2 of 3: Email collection"},
      {"action": "get", "inputs": ["user_email"]}
    ]
  }
}

on.presubmit

Executes after the agent calls the submit tool, but before input validation runs. Restriction: No say or call actions (data mutation only). This prevents confusing UX where the agent says “Saved!” but validation then fails. Common uses:
  • Default missing optional fields
  • Transform input values before validation
{
  "on": {
    "presubmit": [
      {
        "if": "is_false(inputs.middle_name)",
        "action": "set",
        "name": "inputs.middle_name",
        "value": ""
      }
    ]
  }
}

on.submit

Executes after validation passes, before evaluating transitions. Common uses:
  • Persist inputs to global variables
  • Update counters
  • Confirm submission to user
  • Trigger external tool calls
{
  "on": {
    "submit": [
      {"action": "save"},
      {"action": "inc", "name": "local.steps_completed"},
      {"action": "say", "text": "Information saved!"}
    ]
  }
}

Actions Reference

Actions are the operations executed during lifecycle events. All actions support an optional if condition.

Action Summary

ActionDescriptionKey ParametersAvailable In
sayQueue a verbatim messagetext, roleon.enter, on.submit
setSet a variable valuename, value or valueFromall events
incIncrement a countername, byall events
getPre-populate inputs from variablesinputs, overwriteon.enter, on.presubmit
savePersist inputs to global variablesname, inputson.presubmit, on.submit
callQueue a tool callname, argumentson.enter, on.submit

say

Queues text that the agent must include verbatim in its response.
{"action": "say", "text": "Welcome! Step 1 of 3."}
{"action": "say", "text": "Please hold while I check...", "role": "assistant"}
ParameterTypeDefaultDescription
textstringrequiredVerbatim text to include
rolestring"assistant"Message role
ifstring-Optional condition
Note: Agent compliance with say text is approximately 95%. The agent may occasionally add minor variations.

set

Sets a variable to a static value or computed expression.
{"action": "set", "name": "local.counter", "value": 0}
{"action": "set", "name": "user_status", "valueFrom": "inputs.status"}
{"action": "set", "name": "full_name", "valueFrom": {"type": "cel", "expression": "first + ' ' + last"}}
ParameterTypeDescription
namestringTarget variable path (e.g., local.x, user_name)
valueanyStatic value to set
valueFromExpressionDynamic value from expression (mutually exclusive with value)
ifstringOptional condition

inc

Increments a numeric variable. Creates the variable with value 0 if it doesn’t exist.
{"action": "inc", "name": "local.attempt_count"}
{"action": "inc", "name": "local.score", "by": 10}
{"action": "inc", "name": "local.retries", "if": "!inputs.is_valid"}
ParameterTypeDefaultDescription
namestringrequiredVariable to increment
bynumber1Amount to increment by
ifstring-Optional condition

get

Copies values from existing variables into step inputs. Useful for pre-filling forms with known data.
{"action": "get", "inputs": ["user_email", "user_phone"]}
{"action": "get", "inputs": ["user_name"], "overwrite": true}
ParameterTypeDefaultDescription
inputslist[string]all inputsWhich inputs to populate
overwritebooleanfalseIf true, overwrite existing input values
ifstring-Optional condition

save

Saves step inputs to global variables for use in later steps or after workflow completion.
{"action": "save"}
{"action": "save", "inputs": ["user_name", "user_email"]}
{"action": "save", "name": "contact_info.name", "inputs": ["user_name"]}
ParameterTypeDefaultDescription
inputslist[string]all inputsWhich inputs to save
namestringinput nameTarget variable name (for single input)
ifstring-Optional condition
Without parameters: Saves all step inputs to global variables with matching names.

call

Queues a pending tool call in the workflow state. The LLM is informed of the pending call on the next turn, but is not forced to execute it — it may respond to the user instead.
{"action": "call", "name": "get_current_datetime"}
{"action": "call", "name": "lookup_patient", "arguments": {"patient_id": "{{inputs.patient_id}}"}}
ParameterTypeDescription
namestringTool name to call
argumentsobjectTemplate-rendered arguments
autoPopulateboolean or "required"Planned call-prefill strategy hint (not yet enforced — see note below)
ifstringOptional condition
Forcing execution: To guarantee the LLM executes the queued call rather than responding to the user, set tools.call: true on the step. For the most deterministic behaviour, also restrict tools.allow to just the target tool name. autoPopulate status: This field is accepted by the runtime but has no effect yet. Intended future semantics: true = prefer queuing the call when required arguments can be rendered; false = always let the LLM decide via tool_choice; "required" = queue the call only when all required arguments are available, otherwise fall back to tool_choice. This field does not automatically copy tool results into inputs.* or workflow variables — that is never done automatically. Important: The workflow engine does not automatically copy the tool result into inputs.* or local.*. If later branching depends on the tool result, either:
  • have the endpoint persist known values into vars.*, or
  • have the agent resubmit normalized result fields through the submit tool.

Conditional Actions

All actions support an optional if field for conditional execution:
{
  "action": "say",
  "text": "That doesn't match our records. Please try again.",
  "if": "inputs.provided_dob != patient_dob"
}
{
  "action": "inc",
  "name": "local.retry_count",
  "if": "!inputs.is_valid && local.retry_count < 3"
}
The if condition is evaluated as a JMESPath expression by default. See Expressions for syntax details.

Expressions

Expressions are used in conditions (if fields), computed values (valueFrom), and step transitions (next[].if).

JMESPath (Default)

JMESPath is the default expression language. When you write a string condition, it’s evaluated as JMESPath. Common Patterns:
// Boolean check (truthy/falsy)
"if": "is_valid"
"if": "inputs.confirmed"

// String comparison
"if": "status == 'active'"
"if": "inputs.contact_time == 'morning'"

// Boolean literal comparison (note backticks!)
"if": "inputs.can_sign == `true`"
"if": "inputs.opted_out == `false`"

// Numeric comparison
"if": "local.attempt_count >= `3`"
"if": "inputs.age < `18`"

// Logical operators
"if": "is_valid && has_consent"
"if": "status == 'morning' || status == 'afternoon'"
"if": "!inputs.opted_out"

// Nested property access
"if": "inputs.address.city == 'Boston'"

// Null/missing check
"if": "inputs.middle_name"       // truthy check
"if": "!inputs.optional_field"   // missing or empty
Gotchas:
IssueWrongCorrect
Boolean literals need backticksflag == trueflag == \true“
Number literals need backtickscount > 3count >= \3“
String quotesname == morningname == 'morning'

CEL (Common Expression Language)

CEL is useful when you need features JMESPath doesn’t support: arithmetic, string concatenation, or ternary operators. Syntax:
{
  "valueFrom": {
    "type": "cel",
    "expression": "counter + 1"
  }
}

{
  "if": {
    "type": "cel",
    "expression": "age >= 18 ? 'adult' : 'minor'"
  }
}
CEL Capabilities:
// Arithmetic
"expression": "price * 0.9"
"expression": "local.attempts + 1"

// String concatenation
"expression": "first_name + ' ' + last_name"

// Ternary operator
"expression": "is_vip ? 'priority' : 'standard'"

// Boolean logic
"expression": "is_valid && has_consent"
CEL Limitation: CEL doesn’t support nested dict access (inputs.address.city). Use JMESPath for nested structures.

When to Use Which

Use CaseRecommendedWhy
Simple condition checksJMESPathDefault, no syntax overhead
String comparisonsJMESPathCleaner syntax
Nested property accessJMESPathCEL doesn’t support it
ArithmeticCELJMESPath can’t compute
String buildingCELJMESPath can’t concatenate
Ternary logicCELClean conditional values

Inputs Schema

Each step can define inputs—parameters that the agent should collect before advancing.

Input Parameters

The inputs array defines parameters that become the submit tool’s function schema:
{
  "inputs": [
    {
      "name": "first_name",
      "type": "string",
      "description": "The user's first name",
      "required": true
    },
    {
      "name": "date_of_birth",
      "type": "string",
      "format": "date",
      "description": "Date of birth (YYYY-MM-DD)",
      "required": true
    },
    {
      "name": "preferred_language",
      "type": "string",
      "enum": ["English", "Spanish", "French"],
      "description": "Preferred language for communication",
      "required": false
    }
  ]
}

Available Fields

FieldTypeRequiredDescription
namestringYesParameter name
typestringNoJSON Schema type: string, number, integer, boolean, object, array. Defaults to string
descriptionstringNoHuman-readable description shown to agent
requiredbooleanNoWhether parameter must be collected. Defaults to true
enumlist[str]NoAllowed values (agent constrained to these options)
formatstringNoFormat hint: date, time, date-time, email, uri, etc.
patternstringNoRegex pattern for validation

Accumulation Behavior

Inputs accumulate across multiple submissions until all required fields are collected:
Turn 1: Agent submits (first_name="Alice")
        → Validation fails: missing date_of_birth
        → inputs.first_name = "Alice" (retained)

Turn 2: Agent submits (date_of_birth="1990-05-15")
        → Validation passes: all required fields present
        → Step advances successfully
Key behaviors:
  • New values overwrite: If the agent provides a new value for an existing input, it replaces the old value
  • Missing values retained: Inputs not included in a call keep their previous values
  • Empty string = missing: For string types, "" and whitespace-only strings are treated as empty

Reset on Transition

Important: Input variables (inputs.*) are cleared when transitioning to a new step. To preserve values across steps, use the save or set actions:
{
  "on": {
    "submit": [
      {"action": "save", "inputs": ["first_name", "email"]}
    ]
  }
}

Templates

Template substitution lets you inject dynamic values into text fields using variable placeholders.

Supported Syntax

SyntaxBehaviorExample
{{var}}Replace with value, or empty string if missing{{user_name}}"Alice" or ""
${var}Replace with value, or empty string if missing${user_name}"Alice" or ""
${var=default}Replace with value, or default if missing${name=Guest}"Alice" or "Guest"
Recommendation: Use {{var}} (handlebars style) for most cases.

Where Templates Are Expanded

FieldTemplate ExpansionNotes
instructions[]YesStep instructions are rendered with current context
say action textYesMessage text is rendered before queuing
set action valueYes (if string)Static string values are rendered
call action argumentsYes (recursive)All string values in arguments are rendered
goalNoUsed as-is
if conditionsNoUse expression evaluation instead
valueFromNoUse expression evaluation (JMESPath/CEL)

Available Variables

ScopeAccess PatternExample
Global variables{{variable_name}}{{user_name}}
Task-local state{{local.key}}{{local.retry_count}}
Step inputs{{inputs.field}}{{inputs.provided_dob}}
Nested values{{scope.path.to.value}}{{inputs.address.city}}

Examples

In instructions:
{
  "instructions": [
    "Confirm with {{inputs.user_name}} that their email is {{user_email}}."
  ]
}
In say action:
{
  "action": "say",
  "text": "Hello {{inputs.user_name}}! You have {{local.remaining_attempts}} attempts remaining."
}
In call action:
{
  "action": "call",
  "name": "lookup_patient",
  "arguments": {
    "patient_id": "{{patient_id}}",
    "dob": "{{inputs.provided_dob}}"
  }
}

Step Transitions

The next field controls workflow routing after a step is successfully submitted.

Transition Basics

// Simple unconditional transition
"next": ["NEXT_STEP"]
"next": [{"id": "NEXT_STEP"}]

// Conditional transition
"next": [
  {"if": "inputs.choice == 'A'", "id": "PATH_A"},
  {"if": "inputs.choice == 'B'", "id": "PATH_B"},
  {"id": "DEFAULT_PATH"}  // Fallback (no condition)
]

// Terminal step (workflow ends)
"next": []
// Or simply omit the `next` field

Evaluation Order

Transitions are evaluated in order. The first matching entry wins:
  1. Iterate through next array from first to last
  2. For each entry: if it has an if condition, evaluate it
  3. If condition is true (or no condition), transition to that step
  4. If no entries match, the submission is treated as terminal and the workflow completes in place
Best Practice: Always include a fallback entry without if as the last item:
"next": [
  {"if": "score >= `90`", "id": "EXCELLENT"},
  {"if": "score >= `70`", "id": "GOOD"},
  {"id": "NEEDS_IMPROVEMENT"}  // Catches everything else
]

Terminal Steps

A step is terminal if:
  • It has no next field, OR
  • It has "next": [] (empty array)
When a terminal step is submitted:
  1. The workflow is marked as completed
  2. The submit tool is removed from the agent’s available tools
  3. The workflow state is preserved for reference
Important: A terminal step does not complete just because the workflow enters it. Completion happens when the submit tool is called on that step. For terminal steps with no required inputs, your instructions or tools.call: true still need to drive that final submit.

Loop-Back Transitions

Steps can transition back to themselves or earlier steps for retry patterns:
{
  "id": "VERIFY_INFO",
  "inputs": [{"name": "provided_dob", "type": "string", "required": true}],
  "on": {
    "submit": [
      {"action": "inc", "name": "local.attempts", "if": "inputs.provided_dob != patient_dob"}
    ]
  },
  "next": [
    {"if": "inputs.provided_dob == patient_dob", "id": "VERIFIED"},
    {"if": "local.attempts >= `3`", "id": "FAILED"},
    {"id": "VERIFY_INFO"}  // Loop back for retry
  ]
}

Tool Configuration

The tools field on a step controls which tools the agent can access and whether to force specific tool behavior.

Configuration Options

FieldTypeDefaultDescription
tools.allowlist[string]null (all)Whitelist of allowed tool names
tools.callbooleanfalseForce immediate tool call
tools.allowGoToStepbooleanfalseExpose manual step navigation

tools.allow

Restricts which tools the agent can see during this step. The submit tool is always available regardless of this setting.
{
  "id": "COLLECT_NAME",
  "tools": {
    "allow": ["submit_contact_form"]  // Only the submit tool
  }
}

{
  "id": "VERIFY_EMAIL",
  "tools": {
    "allow": ["submit_contact_form", "validate_email_domain"]  // Submit + one external tool
  }
}
Use cases:
  • Prevent agent from calling irrelevant tools during sensitive steps
  • Progressive disclosure: unlock tools as workflow progresses
  • Security: restrict access to sensitive operations
Current behavior: Omitting tools.allow or using an empty list means “no restriction.” If you want “submit tool only,” list the submit tool name explicitly.

tools.call

When true, forces the agent to make a tool call before responding to the user. Typically used with on.enter call actions.
{
  "id": "FETCH_DATA",
  "tools": {"call": true},
  "on": {
    "enter": [
      {"action": "call", "name": "get_patient_info", "arguments": {"id": "{{patient_id}}"}}
    ]
  }
}
Behavior:
  • Forces a tool call on the next model turn
  • If a queued call action is pending and allowed, that pending call is prioritized
  • If no pending external call is available, the runtime may force the submit tool instead
  • Useful for fetching data, triggering mandatory side effects, or making no-input steps deterministic
Bridge steps (non-terminal zero-input steps): If a step has no inputs and a next transition, the LLM has nothing to collect and will not automatically submit. This causes the workflow to stall. You must add "tools": {"call": true} to force submission:
{
  "id": "ROUTE",
  "goal": "Determine next step based on caller data",
  "inputs": [],
  "tools": {"call": true},
  "on": {
    "enter": [
      {"action": "call", "name": "lookup_caller", "arguments": {"ani": "{{vars.session.source}}"}}
    ]
  },
  "next": [
    {"if": "matched_caller", "id": "MATCHED"},
    {"id": "UNMATCHED"}
  ]
}
Similarly, terminal steps with no inputs still require the submit tool to be called to mark the workflow complete. Include a clear instruction or tools.call: true to ensure submission happens.

tools.allowGoToStep

When true, adds a go_to_step parameter to the submit tool schema, allowing the agent to manually jump to a specific step.
{
  "id": "MENU",
  "goal": "Present options to the user",
  "tools": {
    "allowGoToStep": true
  },
  "instructions": [
    "Ask the user what they'd like to do:",
    "1. Check balance → go to CHECK_BALANCE",
    "2. Make payment → go to MAKE_PAYMENT",
    "3. Speak to agent → go to TRANSFER"
  ]
}
Generated tool parameters schema includes:
{
  "go_to_step": {
    "type": "string",
    "description": "Optional: jump to a specific step ID"
  }
}
Notes:
  • The agent chooses the step by name
  • Invalid step IDs return a validation error
  • Bypasses normal next evaluation when used
  • The current implementation validates the target at submit time; it does not enumerate valid step IDs in the generated schema
  • Use with caution: letting the LLM model choose the next step makes the workflow more flexible, but also less predictable

Variables

Step Workflows use multiple variable scopes for different purposes.

Variable Scopes

ScopePrefixLifetimeUse Case
Global(none) or vars.*ConversationShared data, final outputs
Task-locallocal.*WorkflowCounters, flags, intermediate state
Step inputsinputs.*Current stepCollected input parameters

Global Variables

Global variables persist for the entire conversation and are accessible to all tools and workflows.
{"action": "save"}  // Saves inputs to global scope
{"action": "set", "name": "user_name", "valueFrom": "inputs.name"}
Use global variables for:
  • Data needed after the workflow completes
  • Sharing data between concurrent workflows
  • Final outputs that other systems will use

Task-Local Variables

Task-local variables (local.*) are scoped to a single workflow instance.
{"action": "set", "name": "local.counter", "value": 0}
{"action": "inc", "name": "local.retry_attempts"}
Use task-local variables for:
  • Retry counters within the workflow
  • Flags and intermediate state
  • Data that shouldn’t persist after workflow completion

Step Inputs

Step inputs (inputs.*) contain the current step’s collected parameters. Key behaviors:
  • Cleared when transitioning to the next step
  • Accumulated across multiple submissions
  • Read-only in expressions; use set to modify
ContextAvailable Inputs
on.enterEmpty (step just started)
on.presubmitValues from current submit call
on.submitValidated values (all required present)
if conditionsCurrent accumulated values
TemplatesCurrent accumulated values

When to Use Which Scope

ScenarioScopeWhy
Data needed after workflow completesGlobalPersists beyond workflow
Retry counter within workflowlocal.*Workflow-specific state
Sharing between concurrent workflowsGlobalTask-local is isolated
Sensitive intermediate datalocal.*More contained scope