Workflows
Multi-agent pipelines with DAG execution. Define a graph of steps once, run it many times with different inputs. Independent branches execute in parallel automatically. Steps can fan out, branch on conditions, wait for humans, call webhooks, transform data, and dynamically splice new steps into a running DAG.
primitivesEnabled: true on your platform config. Available on Pro and Enterprise plans.Creating a workflow
Use the typed builder API to construct a workflow. Call .build() at the end to produce a WorkflowCreateOpts object you pass to theazo.workflows.create().
import { Theazo, workflow } from 'theazo'
const theazo = new Theazo({ apiKey: 'th_live_...' })
const wf = await theazo.workflows.create(
workflow('lead-enrichment')
.step('scrape', {
agent: 'web-researcher', // what the agent does comes from its definition
input: { website: '$.input.website' }, // input feeds it data (JSONPath)
})
.step('enrich', {
agent: 'data-enricher',
dependsOn: ['scrape'],
input: {
companyData: '$.scrape.output.company',
rawHtml: '$.scrape.output.html',
},
})
.condition('check-fit', {
if: '$.enrich.output.icp_score >= 70',
then: 'send-to-crm', // step id (or { agent: 'closer' })
else: 'archive',
dependsOn: ['enrich'],
})
.build()
)
console.log(wf.id) // 'wf_a1b2c3'
console.log(wf.version) // 1Input and output schemas
Attach JSON Schema validators to inputSchema and outputSchema. Theazo validates the run input at trigger time and the final output before marking the run complete. Invalid inputs are rejected with a 422 before any agents start.
const wf = await theazo.workflows.create(
workflow('lead-enrichment')
.input({
type: 'object',
required: ['website', 'userId'],
properties: {
website: { type: 'string', format: 'uri' },
userId: { type: 'string' },
priority: { type: 'string', enum: ['low', 'high'] },
},
})
.output({
type: 'object',
required: ['score', 'enrichedLead'],
properties: {
score: { type: 'number', minimum: 0, maximum: 100 },
enrichedLead: { type: 'object' },
},
})
// ... steps ...
.build()
)Step types
Each step has a type that controls how it executes. All step types share id, dependsOn, inputMap, and onStepFailure. There are 10 step types.
agentdefaultRuns an agent definition in its own sandbox and model loop. What the agent does comes from its definition; inputMap feeds it data via JSONPath.
{
id: 'scrape',
type: 'agent',
agent: 'web-researcher',
inputMap: { website: '$.input.website' },
}parallelFans out to multiple sub-steps simultaneously. All sub-steps must complete (or fail per policy) before downstream dependents proceed.
{
id: 'research-all',
type: 'parallel',
branches: [
{ id: 'twitter', type: 'agent', agent: 'social-agent', inputMap: { company: '$.input.company' } },
{ id: 'linkedin', type: 'agent', agent: 'social-agent', inputMap: { company: '$.input.company' } },
{ id: 'news', type: 'agent', agent: 'news-agent', inputMap: { company: '$.input.company' } },
],
}conditionBranches execution based on a JSONPath expression in if, evaluated against the current run state. Resolves to the then or else target — a step id, or an inline { agent } target.
{
id: 'check-score',
type: 'condition',
dependsOn: ['score'],
if: '$.score.output.score >= 70',
then: 'send-to-crm', // step id (or { agent: 'closer' })
else: 'archive',
}delayPauses execution for a fixed duration before proceeding. The agent sandbox is released during the wait — no compute cost accrues.
{
id: 'wait-24h',
type: 'delay',
duration: '24h', // duration string: ms | s | m | h | d
dependsOn: ['send-intro-email'],
}approvalPauses the run and waits for a human approval decision via the Approvals API or dashboard. On timeout, the defaultAction applies.
{
id: 'approve-send',
type: 'approval',
action: 'send_email',
timeout: '4h',
defaultAction: 'deny',
dependsOn: ['draft-email'],
}webhookPOSTs a payload to an external URL and waits for a callback (or fires-and-forgets). Body is constructed from inputMap values.
{
id: 'notify-crm',
type: 'webhook',
url: 'https://hooks.yourapp.com/lead-created',
method: 'POST',
waitForCallback: false,
inputMap: { lead: '$.enrich.output.lead' },
headers: { 'X-Source': 'theazo' },
}transformRuns a pure JSONPath reshaping operation — no agent, no sandbox. Use to restructure data between steps without paying for compute. Transform expressions are serializable JSONB; never use JS functions here.
{
id: 'reshape',
type: 'transform',
dependsOn: ['research-all'],
expression: {
twitterHandle: '$.twitter.output.handle',
linkedinUrl: '$.linkedin.output.url',
latestHeadline: '$.news.output.articles[0].title',
company: '$.input.company',
},
}mapIterates an array from a previous step and runs a sub-step for each element. Controlled concurrency via concurrency. Produces an array output in the same order as the input.
{
id: 'score-each',
type: 'map',
dependsOn: ['scrape'],
over: '$.scrape.output.leads', // JSONPath to the array
concurrency: 5,
step: { // each array item is passed as this agent's input
id: 'score-lead',
type: 'agent',
agent: 'scorer',
},
}plannerDynamic DAG expansion. Runs an agent whose output is a list of new step definitions. Theazo splices those steps into the running DAG at runtime. Useful when the number or shape of tasks is not known at workflow-definition time.
{
id: 'plan',
type: 'planner',
agent: 'task-planner',
description: 'Given the input leads, produce a list of research tasks',
// Agent must output: { steps: WorkflowStep[] }
// Theazo validates the schema and splices them into the DAG
dependsOn: ['pre-filter'],
}waitSuspends the run until a named external event is delivered via POST /v1/workflow-runs/:runId/resume (or workflows.resumeRun()). The event payload becomes the step's output, so downstream steps can read it via JSONPath. If timeout elapses before the event arrives, a background sweep fires automatically: the run continues from onTimeout (the wait resolves with output.timedOut = true), or fails if no onTimeout is set — so a run never hangs forever. Use for long-running async integrations. Unlike delay (fixed duration) and approval (human decision), wait blocks on an arbitrary external signal.
{
id: 'wait-payment',
type: 'wait',
event: 'payment.confirmed', // resumeRun({ event: 'payment.confirmed', payload }) continues the run
timeout: '48h', // optional
onTimeout: 'send-reminder', // optional: step id to continue from on timeout
dependsOn: ['send-invoice'],
}Data passing with JSONPath
Steps pass data forward using inputMap. Keys become named variables available to the step. Values are JSONPath strings evaluated against the full run state. All values are serializable to JSONB — never use JavaScript functions in step definitions.
// JSONPath reference patterns
inputMap: {
leads: '$.scrape.output.leads', // array from prior step
companyName: '$.scrape.output.company.name', // nested field
rawInput: '$.input.website', // original workflow input
firstItem: '$.scrape.output.leads[0]', // array index
}Shared state
Steps can declare which keys they read and write from a shared state bag. The state bag persists across the entire run and survives hibernation. Declare stateReads and stateWrites per step so Theazo can detect conflicts and serialize concurrent writers.
const wf = await theazo.workflows.create(
workflow('pipeline')
.step('writer', {
agent: 'data-agent',
stateWrites: ['company', 'metadata'], // keys this step writes
})
.step('reader', {
agent: 'scorer',
dependsOn: ['writer'],
stateReads: ['company'], // keys this step reads
})
.build()
)
// Shared state is written/read by steps (stateWrites/stateReads above);
// it starts empty on each run.
const run = await theazo.workflows.run(wf.id, {
userId: 'user_123',
sessionId: 'ses_abc123', // the end-user's session
input: { website: 'https://acme.com' },
})Workflow policy
Attach a WorkflowPolicy to enforce tool restrictions, cost budgets, and approval gates across all steps. Policy applies to every agent launched by the workflow — individual step overrides are not supported.
const wf = await theazo.workflows.create(
workflow('secure-pipeline')
.withPolicy({
allowTools: ['exec_code', 'read_file', 'list_files'],
denyTools: ['shell_exec', 'write_file'],
requireApprovalFor: ['send_email', 'update_crm'],
maxCostPerStep: { amount: 50, currency: 'usd' }, // $0.50 per step
maxTotalCost: { amount: 500, currency: 'usd' }, // $5.00 per run
})
// ... steps ...
.build()
)If a step exceeds maxCostPerStep, it is paused and a workflow.step.cost_exceeded webhook fires. If the run total exceeds maxTotalCost, the entire run is cancelled.
Per-step error routing
By default a failing step stops the run (respecting the top-level onFailure policy). Override per step with onStepFailure to route to a recovery step instead.
{
id: 'enrich',
type: 'agent',
agent: 'data-enricher',
dependsOn: ['scrape'],
inputMap: { company: '$.scrape.output.company' },
onStepFailure: { action: 'goto', target: 'enrich-fallback' },
},
{
id: 'enrich-fallback',
type: 'agent',
agent: 'data-enricher-lite',
inputMap: { company: '$.scrape.output.company' },
}Running a workflow
const run = await theazo.workflows.run(wf.id, {
userId: 'user_123',
sessionId: 'ses_abc123', // the end-user's session
input: {
website: 'https://acme.com',
priority: 'high',
},
idempotencyKey: 'lead-acme-2026-06-05', // safe to retry — won't double-run
})
console.log(run.id) // 'wfrun_abc123'
console.log(run.status) // 'running'idempotencyKey is scoped per platform. Submitting the same key within 24 hours returns the existing run rather than starting a new one. Use it to safely retry on network errors.Checking run status
const run = await theazo.workflows.getRun('wfrun_abc123')
console.log(run.status) // 'completed' | 'running' | 'failed' | 'partial' | 'cancelled'
console.log(run.steps) // per-step: { id, status, cost, output, startedAt, completedAt }
console.log(run.cost) // { amount: 2340, currency: 'usd' }
console.log(run.duration) // 184 (seconds)
console.log(run.canRetryFrom) // 'enrich' — resume from this step on partial failurePartial results
When a non-critical step fails and the run continues, the run status becomes partial. The canRetryFrom field names the first failed step. retryRun() resets failed steps and re-runs — already-completed steps are skipped, not re-executed.
if (run.status === 'partial') {
// Retry — failed steps are reset; completed steps are not re-run
await theazo.workflows.retryRun(run.id)
}Cancelling a run
await theazo.workflows.cancelRun('wfrun_abc123')
// In-progress agents are paused; partial results preservedSSE streaming
Subscribe to a run's live event stream using theazo.workflows.streamRun(runId). Events are Server-Sent Events delivered as an AsyncIterable<StreamEvent>.
for await (const event of theazo.workflows.streamRun('wfrun_abc123')) {
switch (event.type) {
case 'step.started':
console.log('Started:', event.stepId)
break
case 'step.completed':
console.log('Done:', event.stepId, '— cost', event.cost)
break
case 'step.failed':
console.error('Failed:', event.stepId, event.error)
break
case 'run.completed':
console.log('Workflow done. Total cost:', event.totalCost)
break
case 'run.partial':
console.warn('Partial result. Retry from:', event.canRetryFrom)
break
case 'approval.pending':
console.log('Waiting for approval on:', event.action)
break
}
}Cost estimation
Before running an expensive workflow, call theazo.workflows.estimate() with a sample input. Theazo runs a dry-pass — resolving the DAG, counting expected steps and agent calls — and returns a cost estimate without executing any agents.
const estimate = await theazo.workflows.estimate('wf_a1b2c3', {
input: { website: 'https://acme.com', priority: 'high' },
})
console.log(estimate.minCost) // { amount: 80, currency: 'usd' }
console.log(estimate.maxCost) // { amount: 420, currency: 'usd' }
console.log(estimate.steps) // per-step estimate breakdownVersioning
Every call to theazo.workflows.update() increments the version integer. In-flight runs continue on the version they started on; new runs pick up the latest version automatically.
// Update the workflow definition — version increments atomically
const updated = await theazo.workflows.update('wf_a1b2c3', {
steps: [ /* new step list */ ],
})
console.log(updated.version) // 2
// New runs use the latest version; the run records which one it ran on
const run = await theazo.workflows.run(updated.id, {
userId: 'user_123',
sessionId: 'ses_abc123',
input: { website: 'https://acme.com' },
})
console.log(run.workflowVersion) // 2Complete example
A full lead pipeline: parallel research, data transform, map-score each lead, conditional routing, and approval gate before CRM push.
import { Theazo, workflow } from 'theazo'
const theazo = new Theazo({ apiKey: 'th_live_...' })
const wf = await theazo.workflows.create(
workflow('lead-pipeline')
.input({
type: 'object',
required: ['company', 'website'],
properties: {
company: { type: 'string' },
website: { type: 'string', format: 'uri' },
},
})
.withPolicy({
allowTools: ['exec_code', 'read_file'],
requireApprovalFor: ['update_crm'],
maxTotalCost: { amount: 1000, currency: 'usd' },
})
// Step 1 — parallel social + news research
.parallel('research', [
{ id: 'twitter', type: 'agent', agent: 'social-agent', inputMap: { company: '$.input.company' } },
{ id: 'linkedin', type: 'agent', agent: 'social-agent', inputMap: { company: '$.input.company' } },
{ id: 'news', type: 'agent', agent: 'news-agent', inputMap: { company: '$.input.company' } },
])
// Step 2 — transform into a clean shape (no agent, no cost)
.transform('reshape', {
twitter: '$.twitter.output',
linkedin: '$.linkedin.output',
news: '$.news.output.articles',
company: '$.input.company',
}, { dependsOn: ['research'] })
// Step 3 — score each news article (concurrency=3); each array item is the agent input
.map('score-news', {
over: '$.reshape.output.news',
agent: 'relevance-scorer',
concurrency: 3,
dependsOn: ['reshape'],
})
// Step 4 — final scoring with all signals
.step('score', {
agent: 'lead-scorer',
dependsOn: ['score-news'],
input: {
twitter: '$.reshape.output.twitter',
linkedin: '$.reshape.output.linkedin',
newsScores: '$.score-news.output',
company: '$.input.company',
},
})
// Step 5 — conditional routing on the score
.condition('route', {
if: '$.score.output.score >= 70',
then: 'approve-crm', // step id
else: 'archive',
dependsOn: ['score'],
})
// Step 6a — high score → approval gate before CRM
.approval('approve-crm', 'update_crm', { timeout: '4h', defaultAction: 'deny' })
// Step 6b — low score → archive
.step('archive', {
agent: 'archiver',
input: { lead: '$.reshape.output', score: '$.score.output' },
})
.build()
)
// Estimate cost before running
const estimate = await theazo.workflows.estimate(wf.id, {
input: { company: 'Stripe', website: 'https://stripe.com' },
})
console.log('Estimated cost:', estimate.maxCost) // { amount: 420, currency: 'usd' }
// Run it
const run = await theazo.workflows.run(wf.id, {
userId: 'user_456',
sessionId: 'ses_abc123',
input: { company: 'Stripe', website: 'https://stripe.com' },
idempotencyKey: 'lead-stripe-2026-06-05',
})
// Stream live events
for await (const event of theazo.workflows.streamRun(run.id)) {
if (event.type === 'step.completed') {
console.log(event.stepId, 'done — cost:', event.cost)
}
if (event.type === 'run.completed') {
console.log('Total cost:', event.totalCost)
break
}
}API reference
/v1/workflowsCreate a workflow definition. Returns the workflow with version=1.
Parameters
namestringHuman-readable workflow name.descriptionstringWhat the workflow does.stepsobject | object | object | object | object | object | object | object | object | object[]requiredThe workflow’s steps — a DAG. See the step-type catalog.inputSchemaRecord<string, unknown>JSON Schema for the run input, validated on each run.outputSchemaRecord<string, unknown>JSON Schema describing the run output.policyobjectTool / cost / concurrency guardrails applied to every step.onFailure'pause' | 'retry' | 'skip' | 'abort'What to do when a step fails. Default: pause.retriesobjectAutomatic step-retry policy.timeoutstringMax wall-clock for the whole run, e.g. "1h".concurrencynumberMax steps run in parallel. Default: unbounded.plannerPolicyRecord<string, unknown>Configuration for planner steps (dynamic step generation).Response
WorkflowExample
curl -X POST https://api.theazo.com/v1/workflows \
-H "Authorization: Bearer th_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "lead-enrichment",
"steps": [{ "id": "scrape", "type": "agent", "agent": "web-researcher", "inputMap": { "website": "$.input.website" } }]
}'/v1/workflowsList all workflow definitions for this platform. Ordered by createdAt descending.
Response
Workflow[]Example
curl https://api.theazo.com/v1/workflows \
-H "Authorization: Bearer th_live_..."/v1/workflows/:idFetch a single workflow definition by ID.
Response
WorkflowExample
curl https://api.theazo.com/v1/workflows/wf_a1b2c3 \
-H "Authorization: Bearer th_live_..."/v1/workflows/:idUpdate a workflow definition (all fields optional; omitted fields keep their stored value). Increments version atomically. In-flight runs are unaffected.
Parameters
namestringHuman-readable workflow name.descriptionstringWhat the workflow does.stepsobject | object | object | object | object | object | object | object | object | object[]requiredThe workflow’s steps — a DAG. See the step-type catalog.inputSchemaRecord<string, unknown>JSON Schema for the run input, validated on each run.outputSchemaRecord<string, unknown>JSON Schema describing the run output.policyobjectTool / cost / concurrency guardrails applied to every step.onFailure'pause' | 'retry' | 'skip' | 'abort'What to do when a step fails. Default: pause.retriesobjectAutomatic step-retry policy.timeoutstringMax wall-clock for the whole run, e.g. "1h".concurrencynumberMax steps run in parallel. Default: unbounded.plannerPolicyRecord<string, unknown>Configuration for planner steps (dynamic step generation).Response
WorkflowExample
curl -X PUT https://api.theazo.com/v1/workflows/wf_a1b2c3 \
-H "Authorization: Bearer th_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "lead-enrichment-v2" }'/v1/workflows/:idDelete a workflow definition. Does not cancel in-progress runs.
Response
voidExample
curl -X DELETE https://api.theazo.com/v1/workflows/wf_a1b2c3 \
-H "Authorization: Bearer th_live_..."/v1/workflows/:id/runsStart a new run of a workflow. Returns 202 with the run object. Execution is async.
Parameters
userIdstringrequiredUser the run is attributed to.sessionIdstringrequiredSession the run executes under — its environment is inherited.inputRecord<string, unknown>Input payload passed to the workflow.idempotencyKeystringDedupe key — a repeat with the same key returns the existing run instead of starting a new one.Response
WorkflowRunExample
curl -X POST https://api.theazo.com/v1/workflows/wf_a1b2c3/runs \
-H "Authorization: Bearer th_live_..." \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"sessionId": "ses_abc123",
"input": { "website": "https://acme.com" },
"idempotencyKey": "lead-acme-2026-06-05"
}'/v1/workflows/:id/runsList all runs for a workflow. Ordered by createdAt descending.
Parameters
statusstringFilter by status: running | completed | failed | partial | cancelled.limitnumberMax results to return. Default 50.Response
WorkflowRun[]Example
curl "https://api.theazo.com/v1/workflows/wf_a1b2c3/runs?status=completed&limit=20" \
-H "Authorization: Bearer th_live_..."/v1/workflow-runs/:runIdFetch current status, per-step results, and cost for a run.
Response
WorkflowRunExample
curl https://api.theazo.com/v1/workflow-runs/wfrun_abc123 \
-H "Authorization: Bearer th_live_..."/v1/workflow-runs/:runId/streamSSE stream of run events. Returns text/event-stream. Events: step.started, step.completed, step.failed, approval.pending, run.completed, run.partial.
Response
text/event-streamExample
curl https://api.theazo.com/v1/workflow-runs/wfrun_abc123/stream \
-H "Authorization: Bearer th_live_..." \
-H "Accept: text/event-stream"/v1/workflow-runs/:runId/cancelCancel an in-progress run. In-flight agents are paused, partial results preserved.
Response
WorkflowRunExample
curl -X POST https://api.theazo.com/v1/workflow-runs/wfrun_abc123/cancel \
-H "Authorization: Bearer th_live_..."/v1/workflow-runs/:runId/resumeResume a run paused on a wait step. Supply the event name (must match the step's event field) and an optional payload, which becomes the step's output. Returns 409 if the run is not paused.
Parameters
eventstringrequiredName of the awaited event being delivered to a paused `wait` step.payloadRecord<string, unknown>Event payload made available to the resumed workflow.Response
WorkflowRunExample
curl -X POST https://api.theazo.com/v1/workflow-runs/wfrun_abc123/resume \
-H "Authorization: Bearer th_live_..." \
-H "Content-Type: application/json" \
-d '{ "event": "payment.confirmed", "payload": { "txId": "txn_999" } }'/v1/workflows/:id/estimateDry-run cost estimation. No agents are executed. Returns min/max cost and per-step breakdown.
Parameters
inputobjectrequiredSample input used to resolve the DAG shape.Response
WorkflowEstimateExample
curl -X POST https://api.theazo.com/v1/workflows/wf_a1b2c3/estimate \
-H "Authorization: Bearer th_live_..." \
-H "Content-Type: application/json" \
-d '{ "input": { "website": "https://acme.com" } }'SDK method reference
workflow(name)WorkflowBuilderCreate a typed builder. Chain .step(id, opts), .condition(id, opts), .parallel(id, branches), .transform(id, expr), .map(id, opts), .delay(id, dur), .approval(id, action), .wait(id, opts), .input(schema), .output(schema), .withPolicy(policy), then .build().theazo.workflows.create(opts)Promise<Workflow>Create a workflow definition. Accepts the output of workflow().build() or a raw WorkflowCreateOpts.theazo.workflows.list()Promise<Workflow[]>List all workflow definitions for this platform.theazo.workflows.get(workflowId)Promise<Workflow>Fetch a workflow definition by ID.theazo.workflows.update(workflowId, opts)Promise<Workflow>Update steps, policy, or name. Version increments atomically.theazo.workflows.delete(workflowId)Promise<void>Delete a workflow definition. Does not cancel in-progress runs.theazo.workflows.estimate(workflowId, { input })Promise<WorkflowEstimate>Dry-run cost estimate. No agents are executed.theazo.workflows.run(workflowId, { userId, sessionId, input?, idempotencyKey? })Promise<WorkflowRun>Start a new run in the user's session. Async — returns 202 immediately.theazo.workflows.getRun(runId)Promise<WorkflowRun>Fetch current status, per-step results, and total cost.theazo.workflows.listRuns(workflowId, opts?)Promise<WorkflowRun[]>List runs for a workflow, newest first. Filterable by status.theazo.workflows.cancelRun(runId)Promise<void>Cancel an in-progress run. In-flight agents are paused.theazo.workflows.retryRun(runId)Promise<void>Retry a failed/partial run. Failed steps are reset; completed steps are not re-run.theazo.workflows.resumeRun(runId, { event, payload? })Promise<void>Resume a run paused on a wait step by delivering the awaited event. The payload becomes the step's output.theazo.workflows.streamRun(runId)AsyncIterable<StreamEvent>SSE stream of run events: step.started, step.completed, step.failed, approval.pending, run.completed, run.partial.Roadmap
The following capabilities are planned but not yet available. They're documented here so you know what's coming — the current engine and step types above are the complete shipped surface today.
A workflow step type that invokes another workflow and passes its output downstream — for composing pipelines out of reusable workflows.
Generate a workflow definition from a plain-language description, then refine it as config.
Drop-in workflow builder and run-progress widgets AgentCos can embed directly in their own product.
Pre-built, parameterized workflow templates for common patterns (research, enrichment, triage).
Execute a workflow against mocked agent outputs to test branching and data flow without live compute. (Cost estimation via workflows.estimate() is available today.)
First-class handling of large intermediate outputs (files, blobs) passed between steps without inlining them into run state.
Run against a specific historical workflow version for reproducibility. Requires version snapshots — today runs always use the latest version (and record it as run.workflowVersion); update() increments the version.
Pass an initial shared-state bag into run() that steps read via stateReads. Today shared state is written/read between steps and starts empty each run.