23 KiB
Workflow Drafts
Workflow drafts are the preferred authoring format for LLM and human clients.
They sit above the raw Workflow model:
WorkflowDraft -> RawWorkflowPlan -> WorkflowArtifact -> Deployment
The draft format is intentionally explicit and patchable. It avoids asking an LLM client to write the full core model directly, while still compiling into the same validated workflow plan used by the runtime.
Raw plans still exist as an escape hatch for advanced clients and compiler outputs. New authoring flows should normally start with drafts.
Why Drafts Exist
The raw workflow model is normalized for execution. That makes it precise, but not always pleasant as an interactive authoring target.
Drafts optimize for:
- stable JSON shapes that are easy to inspect
- explicit step ids instead of implicit Python object references
- targeted fixes through JSON Patch
- a clear place to validate before saving
- preserving the existing raw workflow/runtime boundary
Drafts do not change workflow semantics. They compile into the same core plan shape and then run through normal validation.
Draft Shape
A minimal draft looks like this:
{
"name": "echo",
"input_schema": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": ["text"]
},
"state_schema": {
"type": "object",
"properties": {
"echoed": {
"type": "string",
"reducer": "wf.std.replace"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"echoed": {
"type": "string"
}
},
"required": ["echoed"]
},
"start": "echo",
"steps": {
"echo": {
"use": "demo.personal.echo_tool",
"input": [
{
"target": "text",
"path": "input.text"
}
],
"output": [
{
"source": "echoed",
"target": "state.echoed"
}
]
}
},
"routes": {
"echo": {
"ok": "__end__"
}
}
}
Important details:
stepsare keyed by stable ids so patches do not depend on array positions.startnames one step id.routesmap step outcomes to another step id or__end__.- top-level
outcomesdeclares public workflow terminal outcomes; if omitted, it defaults to["ok"]. - top-level
outputmaps final graph values such asstate.resultinto the public workflow output payload. Step-leveloutputonly writes a node result into workflow state. capabilitymay be concrete during exploration, such asdemo.personal.echo_tool.- When saved with source bindings, concrete refs can be normalized to logical
refs such as
demo.echo_tool.
Two Outputs, Different Shapes
Drafts have two fields named output, but they do different jobs.
Step-Level steps.<id>.output
Step output writes a node's local return payload into workflow state. It uses
source / target:
{
"source": "text",
"target": "state.result_text"
}
Read this as:
node output.text -> state.result_text
Top-Level output
Top-level workflow output projects graph values into the final public workflow
output payload. It uses input-binding shape: path / target, not
source / target. Do not use step output source here; it belongs to
step-level node output bindings only.
{
"path": "state.result_text",
"target": "result_text"
}
Read this as:
state.result_text -> workflow output.result_text
If top-level output is empty, the runtime keeps the legacy same-name fallback:
for every field in output_schema, it copies the top-level state field with the
same name when present. That fallback is convenient, but explicit output
projection is clearer for new workflows. Clearing canonical output bindings
restores this fallback; it does not mean that the public output is always empty.
Canonical workflow-output replacement accepts an ordered union of path and
literal bindings. Nested input.* and state.* sources can project missing
nested output-schema fields from their declared source schemas. Literal values
and context.* paths require an already-declared output target; literals are
validated against that target and never used to infer a schema. Equal or
ancestor/descendant output targets are rejected so the ordered list has one
unambiguous public projection.
Explicit Outputs And Error Outcomes
Use __end__ as the compact terminal path for the normal ok workflow outcome.
For any other public terminal outcome, add an explicit end step and route to
it. The end step itself is terminal; do not add an edge out of it.
This complete draft shape:
{
"name": "echo_with_error",
"input_schema": {
"type": "object",
"properties": {
"text": { "type": "string" },
"fail": { "type": "boolean" }
},
"required": ["text"]
},
"state_schema": {
"type": "object",
"properties": {
"raw": {
"type": "object",
"properties": {
"echoed": { "type": "string" }
}
}
}
},
"output_schema": {
"type": "object",
"properties": {
"message": { "type": "string" }
}
},
"outcomes": ["ok", "error"],
"output": [
{
"target": "message",
"path": "state.raw.echoed"
}
],
"start": "call",
"steps": {
"call": {
"use": "demo.echo",
"input": [
{
"target": "text",
"path": "input.text"
},
{
"target": "fail",
"path": "input.fail"
}
],
"output": [
{
"source": "echoed",
"target": "state.raw.echoed"
}
]
},
"end_error": {
"end": { "outcome": "error" }
}
},
"routes": {
"call": {
"ok": "__end__",
"error": "end_error"
}
}
}
Read it as:
call.okfinishes the workflow with outcomeok.call.errorexecutesend_error, which finishes the workflow with outcomeerror.- both terminal paths project
state.raw.echoedinto public output fieldmessage.
Binding Shape
Draft use steps use the same canonical binding structs as core NodeUse:
{
"input": [
{
"target": "message",
"path": "input.text"
},
{
"target": "limit",
"value": 3
}
],
"output": [
{
"source": "echoed",
"target": "state.echoed"
}
]
}
Legacy draft maps in, with, and out are still accepted as parse-only
compatibility input. Valid drafts are saved and returned with canonical
input and output binding lists.
Graph source paths in in normally start with input., state., or
context.. Node-local paths do not use those prefixes; they are paths inside
the target capability's input or output payload.
In canonical string paths, segments are joined with dots. For nested objects,
use "input.user.name". For literal dots in property names, use TOML quoting:
state."person.name".
For example, this canonical input/output pair:
{
"input": [
{
"target": "user.name",
"path": "input.user.name"
},
{
"target": "job.title",
"path": "state.job.title"
}
],
"output": [
{
"source": "user.age",
"target": "state.person.age"
},
{
"source": "job.years",
"target": "state.experience.years"
}
]
}
Read that as:
input.user.name-> local inputuser.namestate.job.title-> local inputjob.title- local output
user.age->state.person.age - local output
job.years->state.experience.years
Do not reverse the direction. This is wrong:
{
"input": [
{
"target": "input.text",
"path": "message"
}
]
}
That asks the runtime to read from graph path message and write into a
node-local input field literally named input.text.
Do not put constants in path bindings. This is wrong:
{
"input": [
{
"target": "value",
"path": "input.CLICKED"
}
]
}
Use an input value binding for static node-local values instead.
Step Kinds
use
Calls a workflow capability.
{
"use": "demo.personal.echo_tool",
"input": [
{
"target": "text",
"path": "input.text"
}
],
"output": [
{
"source": "echoed",
"target": "state.echoed"
}
]
}
Use this for normal node calls, including generated workflow wrappers around
MCP tools and local wf.std capabilities.
use steps can also provide static node-local input values.
Use this for hardcoded strings, booleans, numbers, and small JSON values that
are part of the graph definition:
{
"use": "wf.std.constant",
"input": [
{
"target": "value",
"value": "CLICKED"
}
],
"output": [
{
"source": "value",
"target": "state.wait_text"
}
]
}
Static values are not path mappings. Use {"target": ..., "value": ...} for
literal JSON values. Invalid draft step shapes are rejected instead of silently
compiling to join.
Generated MCP tool wrappers are intentionally naive. They normally expose both
ok and error outcomes, because MCP tool calls can report transport/provider
errors separately from useful output. Drafts should wire both outcomes:
{
"routes": {
"call_tool": {
"ok": "__end__",
"error": "tool_error"
}
}
}
Use a wrapper node or wf.std.runtime_error for the error path. Do not leave
the generated error outcome dangling.
foreach
Runs a child body over items. Draft foreach mirrors the core foreach policy
model: use item_error and concurrent, not draft-only field names.
{
"foreach": {
"over": "state.items",
"as": "item",
"mode": "serial",
"item_error": "fail"
}
}
Concurrent foreach uses the same canonical policy shape as core:
{
"foreach": {
"over": "state.items",
"as": "item",
"mode": "concurrent",
"concurrent": {
"max_active": 2,
"max_outstanding": 4
},
"item_error": {
"action": "collect",
"collect_to": "state.item_errors"
}
}
}
item_error accepts "fail" and "skip" as shorthand. collect needs an
explicit destination, so item_error: "collect" is invalid; use the object
shape and provide collect_to. Deprecated on_item_error and parallel are
accepted only as parse-only compatibility and dump back to canonical fields.
interrupt
Declares an interrupting step.
{
"interrupt": {
"kind": "input",
"request": [
{
"target": "question",
"path": "state.question"
}
],
"resume": [
{
"source": "answer",
"target": "state.answer"
}
],
"outcomes": ["resumed", "cancelled"]
}
}
Draft interrupts use the same binding shapes as core interrupt nodes:
request builds the public interrupt payload, while resume maps the payload
provided on resume back into workflow state. Older map-shaped request and
resume values are accepted only as parse compatibility and dump back to the
canonical list shape.
Saved interrupting artifacts can pause and resume through deployment runs. The
run response includes a durable run_id; pass that to
wf.workflow.resume_run with the resume payload. Before advancing a resumed
run, the platform revalidates its pinned dependency environment and can return
resume_readiness="blocked" without consuming input.
end
Declares an explicit workflow terminal outcome.
{
"end": {
"outcome": "error"
}
}
Use explicit end steps for non-ok workflow outcomes. The legacy __end__
destination remains the shorthand for public workflow outcome ok.
join
Joins control flow.
{
"join": {}
}
when
Creates one boolean decision step. The condition uses the same JSON shape as
wf_core.models.conditions.Condition.
{
"when": {
"if": {
"op": "ge",
"left": {
"path": "state.count"
},
"right": {
"value": 1
}
},
"then": "positive",
"otherwise": "zero"
}
}
The draft adapter lowers this through WorkflowBuilder.when(). The draft step
id becomes the generated condition entry id, so other routes can target it.
match
Matches one graph value against ordered equality cases.
{
"match": {
"value": "state.status",
"cases": [
{
"equals": "ready",
"then": "run"
},
{
"equals": "waiting",
"then": "pause"
}
],
"default": "__end__"
}
}
Cases are a list rather than a JSON object so values such as 1, "1", and
true are not silently coerced into object keys. The draft adapter lowers this
through WorkflowBuilder.match().
choose
Creates an ordered first-true decision chain.
{
"choose": {
"clauses": [
{
"if": {
"op": "gt",
"left": {
"path": "state.score"
},
"right": {
"value": 80
}
},
"then": "high"
},
{
"if": {
"op": "exists",
"path": "state.fallback"
},
"then": "fallback"
}
],
"default": "__end__"
}
}
choose lowers through WorkflowBuilder.choose() and expands to generated
condition nodes. match, when, and choose replace the deprecated route()
concept for draft JSON; there is intentionally no draft route step kind.
Draft Tools
The workflow MCP surface exposes these draft tools:
| Tool | Purpose |
|---|---|
wf.workflow.validate_draft |
Validate draft shape and compiled workflow without saving. |
wf.workflow.compile_draft |
Return the compiled raw plan plus dependency summaries. |
wf.workflow.patch_draft |
Apply RFC 6902 JSON Patch and validate the patched draft. |
wf.workflow.create_artifact_from_draft |
Compile, normalize, and save a workflow artifact. |
wf.workflow.branch_draft |
Update routes for an existing step in one revision. |
wf.workflow.handle_draft |
Route multiple source step outcomes to a common target. |
wf.workflow.compile_draft_workspace |
Return compiled plan plus capabilities without mutation. |
Use validate_draft before saving. Use patch_draft when an LLM client needs a
small targeted correction instead of rewriting the whole workflow.
Draft Workspaces
Stateless draft tools require the caller to resend the whole draft. Draft workspaces are the preferred LLM authoring flow when a client will patch a workflow over several turns.
Capability-Free CLI Flow
Create an empty workspace when the workflow begins with control flow, an interrupt, an end step, or a subgraph rather than a capability:
wf draft create report_ws --name report_workflow
wf draft add join report_ws --revision 1 --step gate --route done=finish
wf draft set-start report_ws --revision 2 --step gate
wf draft add end report_ws --revision 3 --step finish --outcome error
wf draft set-contract report_ws --revision 4 --outcome error
wf draft validate report_ws
Revision 1 is intentionally incomplete and therefore invalid. Draft edits persist representable intermediate states together with diagnostics, so an entry point or route may refer forward to a step that a later revision adds. The final validation is the gate before saving an artifact.
Pass --input-schema-file, --state-schema-file, or
--output-schema-file to draft create or draft set-contract when the
workflow contract is explicit. Each file must contain one JSON object. These
options replace the complete selected schema; repeated --outcome flags
replace the complete public outcome list. State-schema replacement preserves
JSON Schema annotations such as reducer metadata exactly as supplied.
Use capability-backed creation when the first step should derive its contract and wrapper hints from a known capability:
wf draft create report_ws --capability local.lda_docs.read_documents
For selected fields from a capability contract, prefer wf draft add capability or wf draft bind so schema projection remains tied to that node.
Use set-contract for deliberate whole-schema/outcome replacement. Reserve
JSON Patch for field-level schema surgery that focused commands do not cover.
The capability-bootstrap MCP workspace flow is:
wf.workflow.create_minimal_draft_workspacewf.workflow.get_draft_workspacewf.workflow.patch_draft_workspace- repeat get/patch until valid
wf.workflow.create_artifact_from_workspacefor a full workflow, orwf.workflow.create_wrapper_from_workspacefor a reusable callable wrapper
Workspaces are mutable and revisioned. Artifacts are immutable and versioned.
Patch calls must include the current revision; stale revisions return
revision_conflict and do not mutate the workspace.
create_minimal_draft_workspace is intentionally only a bootstrapper. For
naive MCP wrappers with an error outcome, it wires wf.std.runtime_error with
a static default message unless error_message_source is explicitly provided.
It does not guess that a normal output state path is also an error message.
Provider-specific error envelopes still belong in saved wrapper artifacts or
follow-up patches.
error_message_source accepts the same canonical string path shape used by
other mapping fields, for example "state.error_message". Legacy structural
shapes remain accepted for compatibility.
In MCP Inspector, workspace mutation tools accept a single request object.
This is deliberate: the request object carries descriptions and validation for
the authoring envelope while raw JSON Schema fields remain plain JSON objects.
create_wrapper_from_workspace is intentionally just the wrapper-specific save
path. It validates and compiles the same draft workspace, but fixes the saved
artifact kind to wrapper so clients do not need to pass kind manually.
Focused Edit Commands
For routine edits, prefer focused commands over hand-written JSON Patch:
wf draft set-start <workspace_id> --revision <n> --step <step_id>
wf draft set-contract <workspace_id> --revision <n> --input-schema-file input.schema.json --state-schema-file state.schema.json --output-schema-file output.schema.json --outcome ok
wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --bindings-file output-bindings.json
wf draft set-output <workspace_id> --revision <n> --step <step_id> --clear
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
wf draft set-workflow-output <workspace_id> --revision <n> \
--map state.value=result --value format='"markdown"'
wf draft set-workflow-output <workspace_id> --revision <n> \
--bindings-file output-bindings.json
wf draft set-workflow-output <workspace_id> --revision <n> --clear
wf draft set-workflow-output <workspace_id> --revision <n> \
--merge --map state.other=other
wf draft add capability report --revision 3 --step publish \
--capability local.report.publish --description "Publish report" \
--retry 2 --timeout-seconds 30 \
--input state.report.title=request.title \
--value request.format='"markdown"' --route ok=__end__
wf draft update capability report --revision 4 --step publish \
--clear-description --retry 0 --clear-timeout
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=tool_error
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
wf draft compile <workspace_id>
set-output replaces the step's complete ordered canonical binding list.
Repeat --map LOCAL_SOURCE=STATE_TARGET to preserve order and fan one local
source out to multiple state targets, or use --bindings-file with the exact
JSON list exported by draft inspect --include-draft. --clear is the explicit
empty replacement. The legacy --merge --map path is compatibility-only and
potentially lossy because a map cannot preserve ordering or repeated-source
fan-out.
set-workflow-output follows the same canonical replacement model for the
top-level WorkflowDraft.output list. Use repeated --map and --value
flags for a small ordered edit, or --bindings-file for an exact round trip
from draft inspect --include-draft. --clear writes an empty list and
restores the implicit same-name state fallback. Literal and context.*
bindings need declared output targets; nested input.* and state.* paths
may project their declared source schema into missing nested output fields.
The compatibility --merge --map form remains available only for lossy map
edits and cannot preserve literals, order, or repeated-source fan-out.
add capability accepts metadata plus ordered path and literal input bindings.
update capability is presence-aware: omitted fields remain unchanged, while
--clear-description, --clear-retry, and --clear-timeout remove only the
selected metadata. Any --input/--value update replaces the complete input
list; --clear-input writes an empty list. Use --bindings-file when path and
literal records must retain their exact interleaving.
The focused update preserves the step's use, routes, and outputs. Route and
output changes remain separate focused operations. Replacing use is not an
update operation; remove and re-add the capability step explicitly.
Use draft patch when these focused commands do not cover the structural edit.
Drafts are not raw workflow plans. Drafts use steps, routes, and step field
use. Raw plans use nodes, edges, and node field node.
Patching Drafts
patch_draft accepts JSON Patch operations.
Example:
[
{
"op": "replace",
"path": "/steps/echo/in/input.text",
"value": "message"
},
{
"op": "add",
"path": "/routes/echo/error",
"value": "__end__"
}
]
Patch the draft, not the compiled raw plan. The raw plan is an implementation boundary and may be harder for an LLM client to repair correctly.
Saving And Running
The normal path is:
wf.workflow.validate_draft
wf.workflow.create_artifact_from_draft
wf.workflow.save_deployment
wf.workflow.validate_deployment
wf.workflow.run_deployment
create_artifact_from_draft may return suggested bindings for local system
sources such as:
{
"wf.std": "wf.std",
"wf.mcp": "wf.mcp"
}
Keep those explicit when deployment validation reports binding_missing.
Raw Plan Escape Hatch
Use wf.workflow.create_artifact_from_plan only when the caller already has a
compiled raw workflow plan or is intentionally bypassing the draft layer.
For normal interactive authoring, prefer:
draft -> validate -> patch if needed -> create artifact from draft
That keeps errors local and gives the author a smaller object to reason about.
Read Next
wf_mcp_operator_manual.mdfor the MCP-facing tool family mapwf_mcp_end_to_end_runbook.mdfor a complete connection-to-run exampleworkflow_capabilities.mdfor raw capability versus workflow capabilityworkflow_artifacts.mdfor artifacts, deployments, and dependency contracts