# Workflow Drafts Workflow drafts are the preferred authoring format for LLM and human clients. They sit above the raw `Workflow` model: ```text 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: ```json { "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: - `steps` are keyed by stable ids so patches do not depend on array positions. - `start` names one step id. - `routes` map step outcomes to another step id or `__end__`. - top-level `outcomes` declares public workflow terminal outcomes; if omitted, it defaults to `["ok"]`. - top-level `output` maps final graph values such as `state.result` into the public workflow output payload. Step-level `output` only writes a node result into workflow state. - `capability` may be concrete during exploration, such as `demo.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..output` Step output writes a node's local return payload into workflow state. It uses `source` / `target`: ```json { "source": "text", "target": "state.result_text" } ``` Read this as: ```text 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. ```json { "path": "state.result_text", "target": "result_text" } ``` Read this as: ```text 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: ```json { "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.ok` finishes the workflow with outcome `ok`. - `call.error` executes `end_error`, which finishes the workflow with outcome `error`. - both terminal paths project `state.raw.echoed` into public output field `message`. ## Binding Shape Draft `use` steps use the same canonical binding structs as core `NodeUse`: ```json { "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: ```json { "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 input `user.name` - `state.job.title` -> local input `job.title` - local output `user.age` -> `state.person.age` - local output `job.years` -> `state.experience.years` Do not reverse the direction. This is wrong: ```json { "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: ```json { "input": [ { "target": "value", "path": "input.CLICKED" } ] } ``` Use an input value binding for static node-local values instead. ## Step Kinds ### `use` Calls a workflow capability. ```json { "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: ```json { "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 being silently replaced with a placeholder step. 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: ```json { "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. ```json { "foreach": { "over": "state.items", "as": "item", "mode": "serial", "item_error": "fail" } } ``` Concurrent foreach uses the same canonical policy shape as core: ```json { "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. ```json { "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. ```json { "end": { "outcome": "error" } } ``` Use explicit `end` steps for non-`ok` workflow outcomes. The legacy `__end__` destination remains the shorthand for public workflow outcome `ok`. ### `when` Creates one boolean decision step. The condition uses the same JSON shape as `wf_core.models.conditions.Condition`. ```json { "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. ```json { "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. ```json { "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: ```bash wf draft create report_ws --name report_workflow wf draft add end report_ws --revision 1 --step finish --outcome error wf draft set-start report_ws --revision 2 --step finish wf draft set-contract report_ws --revision 3 --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: ```bash 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: 1. `wf.workflow.create_minimal_draft_workspace` 2. `wf.workflow.get_draft_workspace` 3. `wf.workflow.patch_draft_workspace` 4. repeat get/patch until valid 5. `wf.workflow.create_artifact_from_workspace` for a full workflow, or `wf.workflow.create_wrapper_from_workspace` for 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: ```bash wf draft set-start --revision --step wf draft set-contract --revision --input-schema-file input.schema.json --state-schema-file state.schema.json --output-schema-file output.schema.json --outcome ok wf draft set-name --revision --name wf draft set-route --revision --step --outcome ok --to wf draft set-input --revision --step --map input.text=text wf draft set-output --revision --step --map text=state.text wf draft set-output --revision --step --bindings-file output-bindings.json wf draft set-output --revision --step --clear wf draft set-output --revision --step --merge --map other=state.other wf draft set-workflow-output --revision \ --map state.value=result --value format='"markdown"' wf draft set-workflow-output --revision \ --bindings-file output-bindings.json wf draft set-workflow-output --revision --clear wf draft set-workflow-output --revision \ --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 --revision --step --route ok=__end__ --route error=tool_error wf draft handle --revision --to fail --branch lookup:error --branch transform:error wf draft compile ``` `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: ```json [ { "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: ```text 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: ```json { "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: ```text 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.md`](wf_mcp_operator_manual.md) for the MCP-facing tool family map - [`wf_mcp_end_to_end_runbook.md`](wf_mcp_end_to_end_runbook.md) for a complete connection-to-run example - [`workflow_capabilities.md`](workflow_capabilities.md) for raw capability versus workflow capability - [`workflow_artifacts.md`](workflow_artifacts.md) for artifacts, deployments, and dependency contracts