/workflow will let users create and run deterministic workflow scripts that orchestrate child
agent sessions through a small built-in DSL:
agent(prompt, opts)parallel(thunks)pipeline(items, ...stages)phase(title)log(message)workflow(nameOrRef, args)argsbudget
The script format follows workflow.md. A workflow script exports meta and then runs body code in
an async module context:
export const meta = {
name: "review-changes",
description: "Review changed files and verify findings",
phases: [{ title: "Review" }, { title: "Verify" }],
};
phase("Review");
const findings = await agent("Review the current diff for bugs.", {
label: "review:diff",
schema: FINDINGS_SCHEMA,
});
return findings;The first implementation intentionally avoids a full static parser and avoids promising sandbox security. It uses:
- A lightweight text extractor for
export const meta = .... - A short-lived child process to evaluate only the extracted meta expression.
- Runtime
zodvalidation for meta,agent()args, and other injected DSL calls. - A separate workflow runner child process for script execution.
- Parent-process scheduling, persistence, policy checks, statistics, cancellation, and resume.
This keeps the user-facing format aligned with workflow.md while still letting the CLI collect
workflow names, descriptions, phases, and model hints before a run starts.
Named user workflow scripts are stored by scope:
- Project scope:
<workspace>/.zcode/workflows/<name>.workflow.js - User scope:
~/.zcode/workflows/<name>.workflow.js - Built-in scope: packaged definitions that are loaded only from an allowlist
The default for authored scripts is project scope because scripts usually depend on the current repository layout, test commands, and conventions. Global user scope is for reusable personal templates that should be callable by name across projects.
Lookup priority is:
- Explicit
scriptPath - Project
.zcode/workflows - User
~/.zcode/workflows - Built-in allowlist
Every run records the resolved script path, script hash, source scope, and validated meta in SQLite. The DB is the durable run/status index; the script file is the editable source of truth.
Inline Workflow tool invocations persist the supplied script under a session-scoped storage
directory before launch and return that path in the tool result. The returned path is the edit point
for iteration: edit it with file tools and re-invoke Workflow with scriptPath.
The runtime does not parse the whole module AST in V1. It scans the script text for:
export const meta =Then it extracts the following object expression with balanced braces, preserving quoted strings and
comments well enough for normal JS object literals. The extracted expression is evaluated in a
dedicated child process and validated with WorkflowScriptMetaSchema.
Limitations are explicit:
metamust be near the top of the file.metamust be an object expression.- If extraction or validation fails, the workflow cannot be listed or started.
- Runtime DSL calls are still validated independently; meta is not trusted as an execution policy.
Workflow scripts run in a child process. The child process receives only RPC stubs:
globalThis.agent = (...args) => rpc("agent", args);
globalThis.phase = (title) => rpc("phase", [title]);
globalThis.log = (message) => rpc("log", [message]);
globalThis.parallel = workflowParallel;
globalThis.pipeline = workflowPipeline;The child process does not create sessions, run tools, or write durable state. The parent process owns scheduling, validation, policy, session creation, event emission, and persistence.
This is process isolation, not a security sandbox. Node vm is not used as a trust boundary. User
workflow scripts should be treated as local user code with a narrow RPC surface.
V1 agent() input:
agent(prompt: string, opts?: {
label?: string;
phase?: string;
schema?: JsonSchema;
model?: string;
isolation?: "worktree";
agentType?: string;
tools?: string[];
skills?: string[];
instructions?: string;
systemPrompt?: string;
maxTurns?: number;
timeoutMs?: number;
})The schema accepts these fields, but policy decides which fields are allowed for a given workflow:
- User workflows default to no custom
systemPrompt, no nested subagents, and tool/skill/model values must pass a whitelist. - Trusted built-in workflows may opt into custom system prompts, wider tool sets, nested subagents, and deeper task trees.
- All agent calls are validated in the parent process before dispatch.
Workflow is a task state machine. A session is one execution attempt inside that task tree.
The session table gets a coarse task_type so normal session lists can hide workflow-generated
sessions by default:
interactiveforkworkflow_parentworkflow_childsubagent_childnested_workflow_child
Detailed workflow relationships live in workflow tables, not in session.parent_id. parent_id
remains compatible with existing fork/session lineage behavior.
The persistence model must support arbitrary future nesting even if V1 policy blocks user workflow children from spawning subagents:
workflow_run
workflow_activity
session_task_link
child session
session_task_link
grandchild session
Each link records root_workflow_run_id, parent link id, parent session id, child session id, depth,
role, phase, label, model, and activity id.
Workflow resume does not revive the old runner process. It starts a fresh runner and replays the script from the beginning.
Each dispatchable call gets a deterministic callIndex, callPath, and inputHash. Before
starting a new child session, the parent scheduler checks workflow_activity:
- Completed activity with matching input hash: return cached result immediately.
- Running activity from a dead process: mark lost/cancelled and create a new attempt.
- Failed activity: follow retry policy or pause.
- New or changed activity: dispatch live.
This supports loops as long as the loop condition and agent inputs are deterministic across resume. Runaway loops are bounded by max agent calls, token budget, wall-clock timeout, and child process watchdog.
- A direct
await agent()failure throws into the script. parallel()catches individual thunk failures and returnsnullfor that branch.pipeline()turns a failed item intonulland skips remaining stages for that item.- Uncaught script failure pauses the workflow with structured failure metadata.
- Cancellation is terminal and aborts the runner plus any active child activities.
- Retrying creates a new activity attempt and preserves failed session links for audit.
Workflow status should expose:
- agent calls: total, queued, running, completed, failed, cancelled, cached
- tool calls: total and by tool name
- model requests
- tokens: input, output, reasoning, cache read/write, total
- phase durations and run duration
- permission requests and denials
- budget total/spent/remaining
Stats may be stored as JSON in workflow_run.stats_json and recalculated from events when needed.
Removed (2026-08-22, docs/dynamic-workflow/launch.md). The /workflow and /workflows
slash commands and the TUI workflows panel they opened are gone. The commands below are recorded for
history only — none of them parse today.
and/workflow create|validate|run|resume|stop./workflows [runId]
The script-workflow runtime itself is unchanged and still reachable: the model-facing Workflow
tool is the launch surface (see «Built-in Workflow Tool» below), and WorkflowPort plus the
workflow_* tables are untouched. Desktop keeps its own workflow surface via the app capabilities.
The /dwf command that replaced the namespace serves the dynamic workflow system, which is a
different runtime — see docs/dynamic-workflow/launch.md.
The /workflow create command was removed with the rest of the namespace (see «Command
Surface»). The authoring rules below still hold — they are what the Workflow tool's description
in workflow.md carries, and what any agent writing a script must respect. Read the command framing
in this section as history.
/workflow create <task description> was a script authoring command, not a separate generator
runtime. The command wrapped the request into a normal agent turn so the agent could inspect the
project with read-only tools, write the final script with file tools, and use the built-in
Workflow tool when the user explicitly asked to launch or resume a run.
Default output path:
<workspace>/.zcode/workflows/<generated-name>.workflow.js
Command rules:
- The generated script must keep the
workflow.mdformat and begin withexport const meta = {...}. - The command prompt directs the agent to use project inspection tools before authoring when useful.
- The default destination is project scope.
--userchanges the requested destination to~/.zcode/workflows. --name <name>requests a concrete file/meta name; otherwise the agent chooses a concise kebab-case name from the task.- Existing files should not be overwritten unless
--overwriteis present. - The agent should write the script to disk with normal file tools, then report the path. (It used
to point at
/workflow validate//workflow run; those commands no longer exist — launching now goes through theWorkflowtool withscriptPath.) createitself does not force yolo mode and does not start workflow child sessions.
Authoring prompt constraints:
- Use only the injected DSL:
agent,parallel,pipeline,phase,log,workflow,args, andbudget. - Do not use imports, direct filesystem/process/network APIs, random values, timestamps, or ambient Node globals.
- Prefer simple
pipelineandparallelstructures over custom scheduler code. - Keep loops deterministic and bounded; no unbounded
whileloops. - Do not set custom
systemPrompt,tools,skills, ormodelunless the user explicitly asks for them. The runtime still validates and policy-gates these options before dispatch. - Child workflow agent sessions run with
subagents.enabled = falsein V1, so generated scripts should not assume child agents can create further subagents. - If the user asks to launch the workflow, call the built-in
Workflowtool withscriptPath,script,name,args, orresumeFromRunIdaccording to its schema. Otherwise, do not callWorkflowfor authoring-only requests.
TODO: add the model-facing workflow creation tool schema after the CLI creation flow stabilizes. The launch tool schema is intentionally separate from the authoring command.
The built-in Workflow tool is the model-facing launch surface for the same runtime. Its
description is derived from workflow.md and must preserve the explicit opt-in rule: the model may
only call it when the user requests workflow or multi-agent orchestration.
Input schema:
{
"type": "object",
"properties": {
"script": {
"type": "string",
"maxLength": 524288,
"description": "Self-contained workflow script. Must begin with `export const meta = { name, description, phases }` followed by the script body using agent()/parallel()/pipeline()/phase()."
},
"name": {
"type": "string",
"description": "Name of a predefined workflow resolved from built-ins, project .zcode/workflows, or user ~/.zcode/workflows."
},
"args": {
"description": "Optional input value exposed to the script as the global `args`."
},
"scriptPath": {
"type": "string",
"description": "Path to a workflow script file on disk. Takes precedence over `script` and `name`."
},
"resumeFromRunId": {
"type": "string",
"pattern": "^wf_[a-z0-9-]{6,}$",
"description": "Run ID of a prior Workflow invocation to resume from."
}
},
"additionalProperties": false
}At least one source field is required. The core tool handler only validates and delegates through
WorkflowPort; bootstrap resolves paths, writes inline scripts, validates meta, starts the
background run, and returns { status: "backgrounded", runId, scriptPath, response, traceId }.
Required coverage:
- Meta extraction succeeds for normal
export const meta = {...}scripts. - Invalid meta fails before dispatch.
agent()options are runtime-validated with zod.- User workflow policy rejects forbidden
systemPrompt, tools, skills, models, and nested subagents. - Workflow tables migrate idempotently.
session.task_typepersists and can be filtered.- Resume returns cached completed agent results and creates new attempts for stale active calls.
- Cancelling a workflow updates run, activities, events, and session links.