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 (string or array)"Ask for the user's full name."
inputsParameters to collect (JSON Schema)[{name: "user_name", type: "string"}]
onLifecycle hooks for actions{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. Can be a string or array of strings:
{
  "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": {
    "enter": [...],
    "presubmit": [...],
    "submit": [...]
  }
}

Event Summary

EventWhen TriggeredAllowed ActionsPrimary Use Cases
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.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": "!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 value_fromall 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", "value_from": "inputs.status"}
{"action": "set", "name": "full_name", "value_from": {"type": "cel", "expression": "first + ' ' + last"}}
ParameterTypeDescription
namestringTarget variable path (e.g., local.x, user_name)
valueanyStatic value to set
value_fromExpressionDynamic 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 tool call to be executed. The call is injected into the conversation.
{"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
ifstringOptional condition
Note: The call action queues the tool call; it doesn’t execute immediately. The call is processed in the next middleware pass.

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 (value_from), 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:
{
  "value_from": {
    "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
value_fromNoUse 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 workflow cannot proceed (error state)
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

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.allow_go_to_stepbooleanfalseExpose 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

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:
  • Sets tool_choice: required in the agent request
  • Combined with queued call actions, ensures specific tools are called
  • Useful for fetching data before proceeding

tools.allow_go_to_step

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": {
    "allow_go_to_step": 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 schema includes:
{
  "go_to_step": {
    "type": "string",
    "description": "Step ID to transition to",
    "enum": ["CHECK_BALANCE", "MAKE_PAYMENT", "TRANSFER", ...]
  }
}
Notes:
  • The agent chooses the step by name
  • Invalid step IDs return a validation error
  • Bypasses normal next evaluation when used

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", "value_from": "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