AI Steps
Five ways to put AI into a workflow:
| Construct | Use for | Conversation | Tools |
|---|---|---|---|
ai_agent step | Classify, extract, summarize | A durable agent run, one answer | The agent's own tools |
ai_sequence container | Agentic work where the agent decides which tools to call | Within the loop | Yes (tool loop plus workflow-level tool steps) |
AI-routed or container | Let an agent pick the branch when no REL rule matched | None | No |
competition container | Several agents answer the same task and a referee judges | None | Each competitor's own tools |
chat step (experimental) | Long-running multi-turn conversations with a user | Persisted as nodes | Via the agent |
A sixth pattern, the AI agent as a human-task assignee, is covered in Human-in-the-Loop.
Agents themselves are raisin:AIAgent nodes (system prompt, provider, model, temperature, tools), typically under /agents in the functions workspace. See AI Provider Configuration.
ai_agent Steps
One agent, one answer. The step runs the agent as a durable agent run: the same runtime every conversation uses, with the agent's own tools, budgets, leases and recovery. The step does not call the model itself:
- On entry it calls
/lib/raisin/ai/flow-agent-run(fromai-tools), which opens a conversation with the prompt and creates a run pointed back at this flow instance. - The flow parks on an
agent_runwait. The run can be followed, steered or stopped like any other run while it works. - When the run ends, a resume job hands its result to the flow, on whichever cluster node picks it up. The flow accepts only the result of the run it is waiting for.
A failed or stopped run fails the step through the ordinary error handling
(retries, error edge, rollback). Use ai_sequence when you need
workflow-level tools, explicit tool steps, or orchestration. The model to keep
in mind: an agent's tools travel with the agent; an ai_sequence's children
are extra workflow tools layered on top.
- id: summarize
node_type: raisin:FlowStep
properties:
action: Summarize request
step_type: ai_agent
agent_ref: /agents/summarizer
prompt: "Summarize this refund request: {{ input.reason }} ({{ input.amount }} CHF)"
# max_model_calls: 8 # the run's model-call budget (default 8)
# timeout_ms: 300000 # how long the flow waits for the run
prompt(ormessage) is template-resolved against the flow context. Without one, the handler falls back to the triggering node'scontentproperty (input.node.properties.contentfor a trigger-started flow), theninput.message, theninput.input.agent_workspacenames the workspace ofagent_ref(defaultfunctions).max_model_callsbounds the run (default 8). The oldermax_tool_iterations: nis still read, asn + 1model calls. The run's budgets are set to fail rather than pause, so an exhausted budget fails the step instead of parking the flow.- Output:
{ response, agent_run_id, outcome, usage }.responseis the run's final answer; reference it downstream as{{ steps.summarize.response }}.outcomeis the run's outcome kind (for examplesucceeded,partialorblocked), andagent_run_idlets a client open the run and its events. response_formatis passed to the run as the output schema. The final answer is parsed as JSON and added asstructured_output.- Each visit of the step in a loop starts a new run.
- The run acts as the agent. The user who triggered the flow is not passed through to it, so give the agent the rights its tools need.
Giving Agents Context
An agent only sees what reaches its prompt. There are four complementary ways to give it workflow context:
- Templates (precise): inject exactly the fields the agent needs, such as
{{ input.customer }},{{ steps.reserve.total }}, or whole objects:"Order data: {{ input }}". include_context(broad): setinclude_context: "input"(the flow input) orinclude_context: "full"(input, all step outputs, trigger info, and flow variables) on the step. The engine appends the workflow state to the prompt as a fenced JSON block under a# Workflow contextheading.truemeansfull. Available onai_agentsteps,ai_sequencecontainers (insideai_config),orrouters, andcompetitionreferees. Agent-as-assignee human tasks always inject the full context.- Tools (pull): give the agent node-read or search tools and let it fetch details on demand. Best when the relevant data is large or unknown upfront.
- Skills (procedures): a step gets its agent's skills and can add its own with
skills:, for that step only. The prompt carries a one-line index per skill and the agent loads the full instructions withload-skillwhen it needs them. Use this for how to do something, where templates andinclude_contextsupply what to work on.
- id: review
node_type: raisin:FlowStep
properties:
action: Review order risk
step_type: ai_agent
agent_ref: /agents/reviewer
prompt: "Review this order for risk."
include_context: full # the agent sees the input and every previous step's output
Prefer templates or include_context: "input" for routine steps. "full" includes every step output and grows with the flow.
A step with agent_ref but without step_type: ai_agent is treated as a full AI container (tool loop and conversation persistence). This keeps older definitions working. Set step_type: ai_agent when you want the one-answer behavior.
ai_sequence Containers (Agentic Tool Loop)
An AI agent runs in a loop: it is called, may request tool calls, the tools execute, the results are fed back, and the loop continues. The loop ends when the agent responds without tool calls, or when max_iterations is reached, in which case the last response is used as the final answer.
- id: assistant
node_type: raisin:FlowContainer
container_type: ai_sequence
prompt: "Help the customer with: {{ input.request }}" # optional task prompt (templated)
ai_config:
agent_ref: /agents/helper
tool_mode: auto # auto | explicit | hybrid (default auto)
explicit_tools: [] # tool names exposed as explicit child steps (hybrid mode)
max_iterations: 10 # default 10
thinking_enabled: false
on_error: stop # stop | continue | retry
timeout_ms: 30000 # per-call timeout
total_timeout_ms: 300000 # across all iterations
# max_retries: 2 # retries for failed AI calls (default 2)
# retry_delay_ms: 1000 # base delay between AI-call retries
# include_context: input # append the workflow context to the task
# response_format: json_schema # text | json_object | json_schema
# output_schema: { ... } # JSON schema for json_schema output
# conversation_ref: <reference> # continue an existing conversation
children: [] # explicit tool steps (explicit and hybrid modes)
Tool modes:
auto: the agent's configured tools are executed internally by the loop.explicit: every tool call appears as an explicit child step.hybrid: tools listed inexplicit_toolsare explicit; the rest run internally.
Output: { response, iterations, message_count } under steps.<container_id>.*. With response_format and output_schema, the model is asked for structured output and response contains the JSON text.
While the agent works, the flow's event stream emits text_chunk, tool_call_started, and tool_call_completed events alongside the usual step events.
AI-Routed or Containers
Add a router to an or container and an agent picks the child. Deterministic REL rules always run first; the agent decides only when none matched, or when there are no rules at all. The agent receives your routing instructions plus the list of branches (each child's id and action text) and answers with structured output { branch, reasoning, confidence }. branch is schema-constrained to the declared child ids, so the model cannot route to an invented target.
- id: route
node_type: raisin:FlowContainer
container_type: or
rules: # optional, deterministic first
- { condition: "input.amount > 10000", next_step: escalate }
router:
agent_ref: /agents/dispatcher
prompt: "Order from {{ input.customer }} for {{ input.amount }} CHF. Route it."
min_confidence: 0.6 # below this: default_branch, or skip
default_branch: standard # omit to skip the container instead
# include_context: input
children:
- id: escalate
node_type: raisin:FlowStep
properties: { action: Escalate to ops, function_ref: /lib/escalate }
- id: vip
node_type: raisin:FlowStep
properties: { action: VIP handling, function_ref: /lib/vip }
- id: standard
node_type: raisin:FlowStep
properties: { action: Standard handling, function_ref: /lib/standard }
The router's decision is recorded as a step output (under the id <container>__router when there are rules, or the container id when there are none): { routed_to, routed_by_agent, reasoning, confidence }. Downstream steps can reference and audit it.
Typical use: a node-event trigger starts the flow and the agent routes by looking at the changed node in {{ input.node }}.
competition Containers
Every child agent (each possibly backed by a different model) answers the same task. A referee agent judges the answers and either accepts a winner or sends per-competitor feedback for another round, bounded by max_rounds (default 1 refinement round). On the final round the referee must accept. The referee's declared confidence travels in the step output, so downstream rules can gate on it the same way they gate on a human decision.
- id: compete
node_type: raisin:FlowContainer
container_type: competition
prompt: "Write a tagline for {{ input.product }}." # shared task (templated)
referee:
agent_ref: /agents/referee
min_confidence: 0.7 # below this: output.confident = false (default 0.7)
max_rounds: 2 # refinement rounds after the initial one (default 1)
# prompt: optional judging instructions
# include_context: input
children:
- id: writer_claude
node_type: raisin:FlowStep
properties: { action: Claude writer, step_type: ai_agent, agent_ref: /agents/writer-claude }
- id: writer_gpt
node_type: raisin:FlowStep
properties: { action: GPT writer, step_type: ai_agent, agent_ref: /agents/writer-gpt }
# children may override the shared task with their own `prompt`
Refinement: the referee answers { action: accept|refine, winner, confidence, feedback: {<competitor>: text} } (schema-enforced). On refine, only competitors with feedback answer again. They see their previous answer plus the referee's notes, and the other answers stand.
Output under steps.compete.*: { response, winner, confidence, confident, reasoning, rounds, answers, models }. response is the winning answer; answers and models keep every competitor's final answer and model for auditing.
A common pattern is a follow-up or gate into a human task when confidence is low:
- id: confidence_gate
node_type: raisin:FlowContainer
container_type: or
rules:
- { condition: "steps.compete.confidence < 0.7", next_step: human_review }
children:
- id: human_review
node_type: raisin:FlowStep
properties:
action: "Review the AI tagline (referee confidence {{ steps.compete.confidence }})"
step_type: human_task
task_type: review
assignee: /users/editor
Competition children must be ai_agent steps with an agent_ref. Children without one are not entered into the competition.
Chat Steps (Experimental)
Chat steps are experimental; the configuration shape below may change between releases.
A long-running, multi-turn conversation step. The flow waits between turns and the conversation history is persisted as nodes.
- id: chat_session
node_type: raisin:FlowStep
properties:
step_type: chat
action: Chat Session
chat_config:
agent_ref: /agents/support # primary agent
system_prompt: "You are a helpful support agent." # optional
max_turns: 50 # default 50
session_timeout_ms: 600000 # how long to wait for the user each turn
handoff_targets: # optional sub-agent delegation
- agent_ref: /agents/billing
description: "Billing and invoice questions"
condition: "input.topic == \"billing\"" # optional REL expression
termination:
allow_user_end: true # a user keyword can end the session (default true)
allow_ai_end: true # the AI may declare the session complete (default true)
end_keywords: ["goodbye", "exit"]
# output_schema: { ... } # JSON schema for the result the agent returns when it ends the session
# approval: # let the agent park the flow on a human decision
# enabled: true
# assignee: /users/support-lead
# due_in_seconds: 3600
Behavior:
- Each user message increments the turn counter; reaching
max_turnscompletes the step withcompletion_reason: max_turns_reached. end_keywordsare matched case-insensitively against the user message whenallow_user_endis true.allow_ai_endlets the agent end the session. Withoutput_schema, the agent's final result must match the schema and lands atsteps.<id>.result.approval.enabledlets the agent open an inbox task for a person during the session. It is off unless the flow author turns it on.- The designer UI's alternative spellings are accepted: handoff targets may use
trigger_conditionandtrigger_phrases, andterminationmay usemodesandtermination_phrases.
For building chat UIs against flows, see JavaScript Client: Chat.
Putting It Together
The ai-approval-flow example combines an agent step with an agent-as-assignee approval:
- An
ai_agentstep summarizes the refund request with a templated prompt. - A
human_taskstep assigned to/agents/refund-approverdecides the approval, completing it automatically on a confident decision or escalating to/users/adminotherwise.
- id: summarize
node_type: raisin:FlowStep
properties:
action: Summarize the refund request
step_type: ai_agent
agent_ref: /agents/refund-approver
prompt: >
Summarize this refund request in one sentence:
customer {{ input.customer }}, amount {{ input.amount }} CHF,
reason: {{ input.reason }}
- id: approve
node_type: raisin:FlowStep
properties:
action: "Refund {{ input.amount }} CHF for {{ input.customer }}?"
step_type: human_task
task_type: approval
task_description: "Summary: {{ steps.summarize.response }}"
assignee: /agents/refund-approver
min_confidence: 0.75
escalation_assignee: /users/admin
options:
- { value: approve, label: Approve refund, style: success }
- { value: reject, label: Reject, style: danger }
The approval's task_description references {{ steps.summarize.response }}, so the agent's summary becomes the context the approver sees, whether that approver is a person or an agent.
In test runs, agents can be mocked by agent path with mock_agents.