Skip to main content
Workflows coordinate multiple agents into a sequential pipeline. Each step runs an agent, validates the output, and passes it to the next step via ctx.prev. Approval gates suspend the workflow until a human approves.

Defining workflows

Use the builder pattern: workflow() returns a builder, chain .step() calls, then .build() to finalize.
Each step’s ctx.prev is strongly typed based on preceding steps’ output schemas. No casts needed.

Step options

OptionTypeDescription
agentAgentDefinitionThe agent to execute. Omit for approval-only steps.
input(ctx: StepContext) => string | { message }Transform workflow context into the agent’s message.
outputSchemaSchema for validating the agent’s response (parsed as JSON).
approvalStepApprovalConfigTurns the step into a human approval gate.

Input callbacks

The input callback receives a StepContext with access to the workflow input and all previous step outputs:
The callback can return a plain string or { message: string }:
If no input callback is provided, the agent receives a default message: "Execute step \"step-name\"".

Workflow options

OptionTypeDescription
inputSchemaSchema for validating workflow input.
access{ start?, approve? }Access control for starting or approving the workflow.

Validation rules

  • Workflow names must match /^[a-z][a-z0-9-]*$/
  • Step names must match the same pattern
  • At least one step is required
  • Duplicate step names within a workflow throw an error
  • Both workflow and step definitions are deeply frozen after creation

Running workflows

Use runWorkflow() to execute a workflow. It runs each step sequentially, passing outputs forward via ctx.prev.

Result shape

How output accumulation works

Each step’s output is stored in ctx.prev keyed by step name. If a step has an output schema, the agent’s response is parsed as JSON and validated against it. Validated data is stored in prev. Steps without an output schema store { response: string }. If output validation fails (invalid JSON or schema mismatch), the workflow returns an error — it does not silently fall back.
ctx.prev is strongly typed — each step sees the output types of all preceding steps based on their output schemas. No casts needed.

Approval gates

An approval gate suspends the workflow until a human approves. When runWorkflow() hits an approval step, it returns immediately with status: 'pending'.

Approval config

OptionTypeDescription
messagestring | (ctx: StepContext) => stringMessage shown to the human approver.
timeoutstringHow long to wait (e.g., '7d').

Resuming after approval

When the workflow returns pending, store the step results. After the human approves, call runWorkflow() again with resumeAfter pointing to the approval step:
The resumeAfter step name must match an existing step in the workflow. An invalid name throws an error.

Building the approval UX

The approval primitive is transport-agnostic — runWorkflow() doesn’t dictate how approvals are delivered or collected. Common patterns:
  • HTTP endpoint — Store pending state in a database, expose POST /workflows/:id/approve, render an approval button in a dashboard
  • Webhook — Send the approval message to Slack/Discord, listen for a reaction or command
  • Durable Object — On Cloudflare, hold workflow state in a Durable Object that wakes when an approval event arrives
  • CLI prompt — For dev tooling, prompt in the terminal and resume immediately

Agent-to-agent invocation

Tools can invoke other agents using ctx.agents.invoke(). This enables delegation patterns where a coordinator agent dispatches work to specialized agents.

Invoke options

OptionTypeDescription
messagestringThe message to send to the target agent. Required
instanceIdstringOptional instance ID for the invoked agent.
The invoked agent runs a full ReAct loop with the same LLM adapter as the calling agent. It returns { response: string }.

Session persistence

Agents support persistent sessions via an AgentStore. Pass a store to run() to enable conversation history across multiple calls.

Available stores

StoreImportDescription
memoryStore@vertz/agentsIn-memory, for testing and dev.
sqliteStore@vertz/agentsSQLite-backed via vertz:sqlite.
d1Store@vertz/agentsCloudflare D1 — pass { binding: env.DB }. Tables are created on first call.

Session options

OptionTypeDescription
storeAgentStoreThe persistence backend. Required for sessions
sessionIdstringResume an existing session. Omit to create new.
maxStoredMessagesnumberCap messages per session (default: 200).
userIdstringSession ownership — enforced on resume.
tenantIdstringTenant scoping — enforced on resume.

LLM adapters

Agents communicate with LLMs through adapters. Use createAdapter() to create one:

Available providers

ProviderValueEnv variable
OpenAI'openai'OPENAI_API_KEY
Anthropic'anthropic'ANTHROPIC_API_KEY
Cloudflare AI'cloudflare'CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN
MiniMax'minimax'MINIMAX_API_KEY

Custom adapters

You can provide a custom LLMAdapter directly — any object with a chat() method:

Agent lifecycle

Agents have three lifecycle hooks:
HookCalled when
onStartBefore the ReAct loop begins.
onCompleteAfter the loop completes successfully.
onStuckWhen the agent hits max-iterations or stuck state.

Loop configuration

Control the ReAct loop behavior: