8.8 KiB
Workflow Draft Surface Design
Goal
Define the first real MCP-facing workflow draft surface.
The current draft prototype mostly relabels the raw graph model. It proved that draft validation, patching, and artifact creation are useful, but it is not yet a deep authoring interface. This design replaces that prototype before it becomes a compatibility burden.
The new draft surface should feel like wf_authoring over MCP:
- intent-level authoring
- stable patch targets
- explicit semantics
- compilation into the existing core workflow graph
Scope
This pass covers:
- keyed steps
- compact outcome routes
- verb-keyed step shapes such as
use - saved capability/workflow references in
use - stable JSON Patch paths
- parity documentation against current
wf_authoring
This pass does not cover:
- reverse-branch / shared outcome handlers
- draft
routesugar - a new
wf_authoringfluent API - true subgraph support
- new core graph semantics
- migration compatibility for the current draft prototype
The current prototype is not version 1. It is disposable scaffolding.
Design Principles
Drafts Are Authoring Models
Drafts exist to make authoring easier. They are not another runtime model.
Compilation remains:
WorkflowDraft
-> draft-to-authoring adapter
-> WorkflowBuilder
-> core Workflow graph
-> WorkflowArtifact
-> WorkflowDeployment
The draft layer must not grow a second graph builder. Where semantics already
exist in wf_authoring, the adapter should call WorkflowBuilder rather than
reimplementing lowering directly against wf_core.
JSON Should Optimize For LLM Edits
Array-index patch paths are weak:
/steps/3/out/foo
Stable ids are better:
/steps/echo/out/foo
The first real draft surface therefore uses keyed objects where identity matters.
Reuse wf_authoring Semantics
The draft layer owns:
- JSON parsing
- stable-id keyed presentation
- patch-friendly document shape
- translation from capability references into authoring inputs
It should not own:
- graph construction rules
- graph construction rules
- duplicate edge-building machinery
- alternate workflow semantics
If the draft surface needs an authoring operation that wf_authoring does not
yet expose, prefer adding the missing authoring primitive first. Temporary gaps
must stay visibly thin and documented rather than becoming a parallel builder.
Nice Syntax Must Still Be Explicit
Verb-keyed steps are allowed:
{
"use": "demo.echo_tool"
}
but no step kind is inferred.
Exactly one step-kind key must be present. Allowed step-kind keys are:
useforeachinterrupt
Zero kind keys or multiple kind keys are validation errors.
Draft Shape
{
"name": "echo_or_fail",
"input_schema": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": ["text"]
},
"state_schema": {
"fields": {
"echoed": {
"type": "string"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"echoed": {
"type": "string"
}
}
},
"start": "echo",
"steps": {
"echo": {
"use": "demo.personal.echo_tool",
"in": {
"input.text": "text"
},
"out": {
"echoed": "state.echoed"
}
},
"missing_text": {
"use": "wf.std.runtime_error",
"in": {
"message": "message"
}
}
},
"routes": {
"echo": {
"ok": "__end__",
"error": "missing_text"
},
"missing_text": {
"error": "__end__"
}
}
}
Step Shapes
use
Call one workflow capability.
{
"use": "demo.echo_tool",
"in": {
"input.text": "text"
},
"out": {
"echoed": "state.echoed"
},
"desc": "optional",
"retry": 2,
"timeout_seconds": 30
}
use accepts any workflow capability ref:
- generated wrapper around an upstream MCP tool
- local source capability such as
wf.std.runtime_error - saved wrapper capability
- saved workflow capability once graph-as-node is available
The adapter lowers this through WorkflowBuilder.use_ref(...), which exists for
named external capabilities that do not have a local Python callable-backed
NodeSpec.
foreach
Keep the current concept, but key the step by id:
{
"foreach": {
"over": "state.items",
"as": "item",
"mode": "serial",
"on_item_error": "fail"
}
}
This lowers to the current core ForeachNode.
interrupt
{
"interrupt": {
"kind": "input",
"request": {
"state.question": "question"
},
"resume": {
"answer": "state.answer"
},
"outcomes": ["submitted"]
}
}
This lowers to the current core InterruptNode.
Routes
Most ordinary edges should be authored through routes:
"routes": {
"echo": {
"ok": "__end__",
"error": "missing_text"
}
}
The adapter passes these through the same authoring connection path that produces normal core edges.
Outcome keys remain strings in JSON because they are wire values, but they should not be treated as arbitrary text. When the referenced capability is resolvable, draft validation should check route keys against that capability's declared outcomes.
routes is intended for:
- outcome routing from
use - outgoing edges from authored graph steps
- ordinary terminal edges
The raw edge list remains compiler output, not the normal authoring surface.
Patching
Stable ids make targeted patches readable:
[
{
"op": "replace",
"path": "/steps/echo/in/input.text",
"value": "message"
},
{
"op": "replace",
"path": "/routes/echo/error",
"value": "fallback"
}
]
This is a core reason for keyed steps.
Relationship To Current wf_authoring
Python wf_authoring |
MCP Draft Surface | Notes |
|---|---|---|
g.use(spec, ...) |
steps[id].use |
direct conceptual match |
g.connect(step, outcome, target) |
routes[id][outcome] = target |
same graph meaning, better JSON |
g.branch(...) |
routes[...] |
outcome routing is already compact in JSON |
route(...) node |
not in the first draft surface | defer until both front doors agree |
explicit start(...) |
start |
direct match |
END |
"__end__" |
keep wire token explicit |
NodeSpec object |
capability ref string | MCP cannot carry Python callable identity |
| builder object refs | stable string ids | JSON needs durable names |
| reducers | state schema reducer refs | same domain concept |
| Python callable nodes | not representable inline | must already be capabilities |
| fluent/cursor ergonomics | intentionally absent | good in Python, poor in patchable JSON |
Later wf_authoring / Core Work
These are real ideas, but intentionally outside this pass.
Shared Outcome Handlers
Useful reverse-branch sugar:
node_a.error
node_b.error
node_c.unreachable
-> runtime_error
This is not synchronization. It is compressed declaration of several ordinary edges.
Possible later surfaces:
- Python:
g.on(...).to(...) - JSON: a dedicated
handlers/onsection
wf_authoring v2
The MCP draft surface exposes friction in current Python authoring too:
- no fluent shared-handler helper
- no draft parity for route sugar yet
- likely room for better grouped declarations
- outcome names still travel as bare strings even though
NodeSpecalready declares them
Python authoring should eventually expose outcome helpers derived from the
NodeSpec, for example spec.outcomes.ok or equivalent. Those helpers should
come from the declared contract rather than duplicated ClassVar constants that
can drift from it.
That should be handled as its own pass, not smuggled into this MCP draft change.
Core Follow-Ups
Potential later core work:
- true graph-as-node / subgraph support
- explicit fork/gather semantics
- future START-edge support if
Workflow.startchanges
Draft route Sugar
Current wf_authoring.route() already has a specific equality/boolean routing
surface. The draft layer should not invent a richer JSON route language ahead of
Python authoring. Add draft route sugar later, after the shape is chosen
deliberately for both front doors.
Prototype Replacement
The current draft prototype should be deleted or replaced directly.
There is no compatibility guarantee because:
- it is not yet a released/versioned user contract
- it mostly relabels raw graph fields
- preserving it would make the first real authoring surface carry avoidable complexity from day one
Testing Strategy
Tests should cover:
- verb-key validation: exactly one step-kind key
- keyed-step compilation into core nodes
routescompilation into core edges- saved capability refs passing through
use - patching by stable ids
- artifact creation from the new draft surface
- diagnostics with stable draft paths