draft: REALLY ergonomic, not full impl

things that stop it from being complete: workflow builder route needs work

is this the call for normal branch/PR styled work?
This commit is contained in:
lda
2026-05-19 00:44:39 +07:00 Verified
parent 3002a893b3
commit 8d9796dd04
18 changed files with 1285 additions and 654 deletions
@@ -0,0 +1,692 @@
# Workflow Draft Surface Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the disposable draft prototype with the first real MCP-facing workflow draft surface: keyed, patchable JSON that adapts into `wf_authoring.WorkflowBuilder` instead of rebuilding graph semantics itself.
**Architecture:** The new draft layer remains a typed JSON seam in `wf_artifacts`, but delegates graph construction to `wf_authoring`. Draft parsing owns keyed presentation, validation, and stable patch paths; `WorkflowBuilder` owns graph construction. The MCP workflow surface keeps the same tool family while accepting the new draft shape.
**Tech Stack:** Python 3.14, Pydantic v2, `wf_authoring`, `wf_core`, `jsonpatch`, pytest, basedpyright, Ruff.
---
## File Structure
### Create
- `src/wf_artifacts/drafts/models.py`
- concrete Pydantic draft document models
- `src/wf_artifacts/drafts/adapter.py`
- thin JSON-draft-to-`WorkflowBuilder` adapter
- `src/wf_artifacts/drafts/api.py`
- public compile/validate/patch functions and diagnostics
- `tests/artifacts/test_draft_models.py`
- draft document validation
- `tests/artifacts/test_draft_adapter.py`
- keyed step/routes lowering through `WorkflowBuilder`
- `tests/artifacts/test_draft_api.py`
- public compile/validate/patch behavior
### Modify
- `src/wf_artifacts/drafts.py`
- replace module body with compatibility re-exports or remove once imports are updated
- `src/wf_artifacts/__init__.py`
- export public draft API/models from the package
- `src/wf_authoring/builder/core.py`
- add `use_ref(...)` for named external capabilities without local `NodeSpec`s
- `src/wf_authoring/ops/*`
- only if route helpers need a reusable public lowering entrypoint
- `src/wf_mcp/workflow_surface/handlers.py`
- keep using public draft API; no semantic duplication
- `tests/wf_mcp/test_workflow_surface.py`
- update draft fixtures to the new keyed document shape
- `tests/wf_mcp/test_server.py`
- confirm MCP schemas remain plain-object friendly
- `docs/workflow_drafts.md`
- replace prototype examples with the first real draft surface
- `docs/wf_mcp_end_to_end_runbook.md`
- update draft example
- `docs/wf_mcp_operator_manual.md`
- keep draft-first guidance accurate
- `docs/wf_mcp_troubleshooting.md`
- update patch-path examples
## Task 1: Split Draft Code Into Focused Modules
**Files:**
- Create: `src/wf_artifacts/drafts/models.py`
- Create: `src/wf_artifacts/drafts/api.py`
- Create: `src/wf_artifacts/drafts/adapter.py`
- Modify: `src/wf_artifacts/drafts.py`
- Modify: `src/wf_artifacts/__init__.py`
- Test: `tests/artifacts/test_draft_models.py`
- [ ] **Step 1: Write the failing model tests**
```python
from wf_artifacts.drafts import DraftUseStep, WorkflowDraft
def test_workflow_draft_uses_keyed_steps() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {
"echo": {
"use": "demo.echo",
"in": {"input.text": "text"},
"out": {"echoed": "state.echoed"},
}
},
"routes": {"echo": {"ok": "__end__"}},
}
)
assert isinstance(draft.steps["echo"], DraftUseStep)
assert draft.steps["echo"].use == "demo.echo"
def test_draft_step_requires_exactly_one_kind_key() -> None:
result = WorkflowDraft.model_validate(
{
"name": "bad",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "bad",
"steps": {
"bad": {
"use": "demo.echo",
"join": {},
}
},
"routes": {},
}
)
```
The second test should be written with `pytest.raises(ValidationError)` and assert the authoring path identifies `steps.bad`.
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_models.py -q
```
Expected: import errors or validation failures because keyed draft models do not exist yet.
- [ ] **Step 3: Implement minimal concrete draft models**
Create:
```python
# src/wf_artifacts/drafts/models.py
from __future__ import annotations
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, model_validator
JsonObject = dict[str, Any]
STEP_KIND_KEYS = frozenset({"use", "foreach", "interrupt", "join"})
class DraftUseStep(BaseModel):
use: str
in_: dict[str, str] = Field(default_factory=dict, alias="in")
out: dict[str, str] = Field(default_factory=dict)
desc: str | None = None
retry: int | None = Field(default=None, ge=0)
timeout_seconds: int | None = Field(default=None, gt=0)
class DraftForeachPayload(BaseModel):
over: str
as_: str = Field(alias="as")
mode: Literal["serial", "parallel"] = "serial"
on_item_error: Literal["fail", "collect", "skip"] = "fail"
class DraftForeachStep(BaseModel):
foreach: DraftForeachPayload
class DraftInterruptPayload(BaseModel):
kind: str
request: dict[str, str] = Field(default_factory=dict)
resume: dict[str, str] = Field(default_factory=dict)
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
class DraftInterruptStep(BaseModel):
interrupt: DraftInterruptPayload
class DraftJoinStep(BaseModel):
join: JsonObject = Field(default_factory=dict)
DraftStep = Annotated[
DraftUseStep
| DraftForeachStep
| DraftInterruptStep
| DraftJoinStep,
Field(discriminator=None),
]
class WorkflowDraft(BaseModel):
name: str
input_schema: JsonObject
state_schema: JsonObject
output_schema: JsonObject
start: str
steps: dict[str, DraftStep]
routes: dict[str, dict[str, str]] = Field(default_factory=dict)
@model_validator(mode="before")
@classmethod
def _validate_step_kinds(cls, value: object) -> object:
if not isinstance(value, dict):
return value
steps = value.get("steps")
if not isinstance(steps, dict):
return value
for step_id, payload in steps.items():
if not isinstance(payload, dict):
continue
present = STEP_KIND_KEYS.intersection(payload)
if len(present) != 1:
raise ValueError(
f"steps.{step_id} must contain exactly one step kind key"
)
return value
```
Keep the public import path stable by re-exporting through `src/wf_artifacts/drafts.py` during the transition.
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_models.py -q
```
Expected: pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_artifacts tests/artifacts/test_draft_models.py
git commit -m "refactor: add keyed workflow draft models"
```
## Task 2: Add `use_ref` And Thin Adapter Over `WorkflowBuilder`
**Files:**
- Create: `src/wf_artifacts/drafts/adapter.py`
- Modify only if needed: `src/wf_authoring/builder/core.py`
- Modify only if needed: `src/wf_authoring/ops/*`
- Test: `tests/artifacts/test_draft_adapter.py`
- [ ] **Step 1: Write failing adapter tests**
```python
from wf_artifacts.drafts import WorkflowDraft
from wf_artifacts.drafts.adapter import build_workflow_from_draft
def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "echo",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "echo",
"steps": {"echo": {"use": "demo.echo"}},
"routes": {"echo": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
assert workflow.nodes[0].id == "echo"
assert workflow.nodes[0].node == "demo.echo"
assert workflow.edges[0].from_ == "echo"
assert workflow.edges[0].outcome == "ok"
assert workflow.edges[0].to == "__end__"
def test_builder_use_ref_creates_external_node_use_without_node_def() -> None:
builder = WorkflowBuilder(
"echo",
input_schema={},
state_schema={"fields": {}},
output_schema={},
)
step = builder.use_ref("demo.echo", id="echo")
builder.set_entry_point(step)
builder.connect(step, "ok", "__end__")
workflow = builder.compile()
assert step.node == "demo.echo"
assert workflow.node_defs == []
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py -q
```
Expected: import error because `build_workflow_from_draft` does not exist.
- [ ] **Step 3: Implement the thin adapter**
First add:
```python
def use_ref(
self,
name: str,
*,
id: str | None = None,
in_map: MapArg | None = None,
out_map: MapArg | None = None,
desc: str | None = None,
) -> NodeUse:
...
```
`use_ref` creates a `NodeUse` for an already named external capability and does
not add a local `NodeDef`.
Then implement `build_workflow_from_draft(draft: WorkflowDraft) -> Workflow` so
it:
1. constructs a `WorkflowBuilder`
2. registers each draft step by stable id
3. uses existing `WorkflowBuilder` public methods for:
- `use_ref`
- `foreach`
- `interrupt`
- `join`
4. applies `routes`
5. calls explicit `start(...)`
6. returns `builder.build(...)`
Do **not** invent draft route sugar in this pass.
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_adapter.py -q
```
Expected: pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_artifacts src/wf_authoring tests/artifacts/test_draft_adapter.py
git commit -m "feat: adapt workflow drafts through workflow builder"
```
## Task 3: Replace Prototype Public API
**Files:**
- Create: `src/wf_artifacts/drafts/api.py`
- Modify: `src/wf_artifacts/drafts.py`
- Modify: `src/wf_artifacts/__init__.py`
- Test: `tests/artifacts/test_draft_api.py`
- [ ] **Step 1: Write failing API tests**
```python
from wf_artifacts.drafts import compile_workflow_draft, patch_workflow_draft
def test_compile_workflow_draft_returns_raw_core_shape() -> None:
plan = compile_workflow_draft(_keyed_echo_draft())
assert plan["nodes"][0]["id"] == "echo"
assert plan["nodes"][0]["node"] == "demo.echo"
assert plan["edges"][0]["outcome"] == "ok"
def test_patch_workflow_draft_uses_stable_step_paths() -> None:
result = patch_workflow_draft(
_keyed_echo_draft(),
[
{
"op": "replace",
"path": "/steps/echo/in/input.text",
"value": "message",
}
],
)
assert result["status"] == "valid"
assert result["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_api.py -q
```
Expected: failures because the old prototype API still expects array `steps`.
- [ ] **Step 3: Implement the API**
Create:
```python
# src/wf_artifacts/drafts/api.py
def compile_workflow_draft(draft: JsonObject) -> JsonObject:
parsed = WorkflowDraft.model_validate(draft)
workflow = build_workflow_from_draft(parsed)
return workflow.model_dump(mode="json", by_alias=True, exclude={"node_defs"})
```
Keep:
- `validate_workflow_draft`
- `patch_workflow_draft`
- structured `DraftDiagnostic`
Update diagnostics to use keyed paths such as:
```text
steps.echo.in
routes.echo.error
```
Delete the old array-step prototype code after public tests are green.
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
uv run --with pytest pytest tests/artifacts/test_draft_api.py -q
```
Expected: pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_artifacts tests/artifacts/test_draft_api.py
git commit -m "feat: replace draft prototype with keyed public api"
```
## Task 4: Update MCP Workflow Surface
**Files:**
- Modify: `tests/wf_mcp/test_workflow_surface.py`
- Modify: `tests/wf_mcp/test_server.py`
- Modify only if needed: `src/wf_mcp/workflow_surface/handlers.py`
- [ ] **Step 1: Update the failing MCP tests**
Replace old fixtures like:
```python
"steps": [{"id": "echo", "kind": "use", ...}]
```
with:
```python
"steps": {"echo": {"use": "demo.echo", ...}},
"routes": {"echo": {"ok": "__end__"}},
```
Keep assertions that:
- draft tools still expose plain object schemas to MCP clients
- `create_artifact_from_draft` still saves artifacts
- source binding normalization still works
- missing `wf.std` self-binding diagnostics still work
- [ ] **Step 2: Run tests to verify failures**
Run:
```bash
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
```
Expected: failures wherever MCP handlers still assume the old prototype shape.
- [ ] **Step 3: Make minimal MCP adjustments**
Keep handlers thin:
```python
plan = compile_workflow_draft(draft)
```
No duplicate draft interpretation should appear in `wf_mcp`.
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
```
Expected: pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_mcp tests/wf_mcp
git commit -m "feat: accept keyed workflow drafts over mcp"
```
## Task 5: Add Outcome Validation When Capability Contracts Are Available
**Files:**
- Modify: `src/wf_artifacts/drafts/api.py`
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
- Test: `tests/wf_mcp/test_workflow_surface.py`
- [ ] **Step 1: Write failing outcome validation test**
```python
def test_draft_validation_rejects_unknown_capability_outcome_when_spec_is_known() -> None:
handlers = _handlers_with_demo_echo_spec()
draft = _keyed_echo_draft()
draft["routes"]["echo"] = {"typo": "__end__"}
result = asyncio.run(handlers.validate_draft(draft=draft))
assert result["status"] == "invalid"
assert result["diagnostics"][0]["path"] == "routes.echo.typo"
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py -q
```
Expected: validation currently accepts the typo.
- [ ] **Step 3: Implement capability-aware outcome validation**
Pass an optional capability lookup into draft validation from MCP handlers.
Rules:
- validate outcome keys for `use` steps only when the capability is resolvable
- if the capability is unknown/unavailable, leave dependency validation to the
later artifact/deployment stages
- diagnostic path must identify the keyed route entry
Do not make `wf_artifacts` depend on `wf_mcp`; define a tiny callable/protocol
interface for lookup instead.
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py -q
```
Expected: pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_artifacts src/wf_mcp tests/wf_mcp/test_workflow_surface.py
git commit -m "feat: validate draft routes against known outcomes"
```
## Task 6: Update Documentation
**Files:**
- Modify: `docs/workflow_drafts.md`
- Modify: `docs/wf_mcp_end_to_end_runbook.md`
- Modify: `docs/wf_mcp_operator_manual.md`
- Modify: `docs/wf_mcp_troubleshooting.md`
- [ ] **Step 1: Update docs to the real draft surface**
Replace prototype array examples with keyed examples:
```json
"steps": {
"echo": {
"use": "demo.echo_tool",
"in": {"input.text": "text"},
"out": {"echoed": "state.echoed"}
}
},
"routes": {
"echo": {
"ok": "__end__"
}
}
```
Document:
- exactly-one-kind-key rule
- stable keyed patch paths
- `route` as repeated condition-chain sugar
- `WorkflowBuilder` as the semantic owner beneath the JSON adapter
- outcome strings validated against capability contracts when available
- [ ] **Step 2: Run a targeted docs scan**
Run:
```bash
rg -n '\"steps\": \\[|\"kind\": \"use\"|/steps/0|create_artifact_from_draft' docs
```
Expected:
- no stale prototype examples in current docs
- `create_artifact_from_draft` still documented as the preferred path
- [ ] **Step 3: Commit**
```bash
git add docs
git commit -m "docs: describe keyed workflow draft surface"
```
## Task 7: Full Verification
**Files:**
- No new files
- [ ] **Step 1: Run focused verification**
```bash
uv run --with pytest pytest tests/artifacts tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_server.py -q
```
Expected: pass.
- [ ] **Step 2: Run full project tests**
```bash
uv run --with pytest pytest -q
```
Expected: pass.
- [ ] **Step 3: Run type checking**
```bash
uv run basedpyright --level error
```
Expected: `0 errors`.
- [ ] **Step 4: Run lint**
```bash
uvx ruff check src tests
```
Expected: pass.
- [ ] **Step 5: Commit final cleanup**
```bash
git add .
git commit -m "feat: ship keyed workflow draft authoring surface"
```
## Self-Review
### Spec Coverage
- keyed `steps`: Tasks 1-4
- compact `routes`: Tasks 1-4
- verb-keyed explicit step kinds: Task 1
- saved capability/workflow refs in `use`: Task 2, existing capability refs pass through unchanged
- stable patch paths: Tasks 3 and 6
- `wf_authoring` as semantic owner: Tasks 2 and 6
- outcome validation against declared contracts: Task 5
- prototype replacement rather than migration: Tasks 3, 4, 6
### Placeholder Scan
- no `TBD`
- no unspecified "add validation" placeholders
- every task has exact files, tests, commands, and expected behavior
### Type Consistency
- `WorkflowDraft`, `DraftUseStep`, `build_workflow_from_draft`, and public API
names stay consistent across all tasks
- patch examples use keyed paths consistently
- `routes` stays the only authored outcome-routing section
@@ -22,7 +22,7 @@ This pass covers:
1. keyed steps
2. compact outcome routes
3. verb-keyed step shapes such as `use` and `route`
3. verb-keyed step shapes such as `use`
4. saved capability/workflow references in `use`
5. stable JSON Patch paths
6. parity documentation against current `wf_authoring`
@@ -30,6 +30,7 @@ This pass covers:
This pass does **not** cover:
- reverse-branch / shared outcome handlers
- draft `route` sugar
- a new `wf_authoring` fluent API
- true subgraph support
- new core graph semantics
@@ -85,7 +86,7 @@ The draft layer owns:
It should not own:
- route expansion
- graph construction rules
- graph construction rules
- duplicate edge-building machinery
- alternate workflow semantics
@@ -109,7 +110,6 @@ but no step kind is inferred.
Exactly one step-kind key must be present. Allowed step-kind keys are:
- `use`
- `route`
- `foreach`
- `interrupt`
- `join`
@@ -145,22 +145,8 @@ Zero kind keys or multiple kind keys are validation errors.
}
}
},
"start": "has_text",
"start": "echo",
"steps": {
"has_text": {
"route": [
{
"when": {
"op": "exists",
"path": "input.text"
},
"to": "echo"
},
{
"otherwise": "missing_text"
}
]
},
"echo": {
"use": "demo.personal.echo_tool",
"in": {
@@ -217,49 +203,9 @@ Call one workflow capability.
- saved wrapper capability
- saved workflow capability once graph-as-node is available
The compiler lowers this to a core `NodeUse`.
### `route`
Route based on input/state conditions.
```json
{
"route": [
{
"when": {
"op": "exists",
"path": "state.hit"
},
"to": "found"
},
{
"otherwise": "missing"
}
]
}
```
The draft adapter asks `wf_authoring` to lower a route step using the same
repeated condition-chain sugar that current Python authoring uses:
```text
if cond_1 -> target_1 else
if cond_2 -> target_2 else
...
otherwise -> fallback
```
That means traces show the expanded condition steps. No new core runtime
semantics are required.
Rules:
- clauses are ordered
- zero or more `when` clauses may appear
- at most one `otherwise` clause may appear
- `otherwise` must be last
- every clause must declare a target
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`
@@ -323,7 +269,7 @@ Most ordinary edges should be authored through `routes`:
}
```
The adapter passes these through the same authoring route/connection path that
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
@@ -334,7 +280,7 @@ declared outcomes.
`routes` is intended for:
- outcome routing from `use`
- outgoing edges from lowered route/condition machinery
- outgoing edges from authored graph steps
- ordinary terminal edges
The raw edge list remains compiler output, not the normal authoring surface.
@@ -367,7 +313,7 @@ This is a core reason for keyed `steps`.
| `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 | `steps[id].route` | state/input routing |
| `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 |
@@ -403,7 +349,7 @@ Possible later surfaces:
The MCP draft surface exposes friction in current Python authoring too:
- no fluent shared-handler helper
- no full parity with route sugar
- no draft parity for route sugar yet
- likely room for better grouped declarations
- outcome names still travel as bare strings even though `NodeSpec` already
declares them
@@ -423,6 +369,13 @@ Potential later core work:
- meaningful join semantics
- future START-edge support if `Workflow.start` changes
### 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.
@@ -441,8 +394,7 @@ Tests should cover:
1. verb-key validation: exactly one step-kind key
2. keyed-step compilation into core nodes
3. `routes` compilation into core edges
4. `route` lowering into current condition nodes/edges
5. saved capability refs passing through `use`
6. patching by stable ids
7. artifact creation from the new draft surface
8. diagnostics with stable draft paths
4. saved capability refs passing through `use`
5. patching by stable ids
6. artifact creation from the new draft surface
7. diagnostics with stable draft paths
+8 -12
View File
@@ -257,11 +257,9 @@ arguments:
"required": ["echoed"]
},
"start": "echo",
"steps": [
{
"id": "echo",
"kind": "use",
"capability": "demo.personal.echo_tool",
"steps": {
"echo": {
"use": "demo.personal.echo_tool",
"in": {
"input.text": "text"
},
@@ -269,14 +267,12 @@ arguments:
"echoed": "state.echoed"
}
}
],
"edges": [
{
"from": "echo",
"outcome": "ok",
"to": "__end__"
},
"routes": {
"echo": {
"ok": "__end__"
}
]
}
}
}
```
+32 -61
View File
@@ -64,11 +64,9 @@ A minimal draft looks like this:
"required": ["echoed"]
},
"start": "echo",
"steps": [
{
"id": "echo",
"kind": "use",
"capability": "demo.personal.echo_tool",
"steps": {
"echo": {
"use": "demo.personal.echo_tool",
"in": {
"input.text": "text"
},
@@ -76,22 +74,20 @@ A minimal draft looks like this:
"echoed": "state.echoed"
}
}
],
"edges": [
{
"from": "echo",
"outcome": "ok",
"to": "__end__"
},
"routes": {
"echo": {
"ok": "__end__"
}
]
}
}
```
Important details:
- `steps[].id` is required because JSON has no Python object identity.
- `start` names a step id.
- `edges[].to` can name another step id or `__end__`.
- `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__`.
- `capability` may be concrete during exploration, such as
`demo.personal.echo_tool`.
- When saved with source bindings, concrete refs can be normalized to logical
@@ -105,9 +101,7 @@ Calls a workflow capability.
```json
{
"id": "echo",
"kind": "use",
"capability": "demo.personal.echo_tool",
"use": "demo.personal.echo_tool",
"in": {
"input.text": "text"
},
@@ -120,36 +114,18 @@ Calls a workflow capability.
Use this for normal node calls, including generated workflow wrappers around
MCP tools and local `wf.std` capabilities.
### `condition`
Evaluates a condition and routes by outcome.
```json
{
"id": "has_text",
"kind": "condition",
"check": {
"op": "exists",
"args": ["input.text"]
}
}
```
Condition nodes compile to graph nodes with condition semantics. Their outgoing
edges should use condition outcomes such as `true` and `false`.
### `foreach`
Runs a child body over items.
```json
{
"id": "each_item",
"kind": "foreach",
"over": "state.items",
"as": "item",
"mode": "serial",
"on_item_error": "fail"
"foreach": {
"over": "state.items",
"as": "item",
"mode": "serial",
"on_item_error": "fail"
}
}
```
@@ -162,16 +138,16 @@ Declares an interrupting step.
```json
{
"id": "ask_user",
"kind": "interrupt",
"interrupt_kind": "input",
"request": {
"state.question": "question"
},
"resume": {
"answer": "state.answer"
},
"outcomes": ["resumed", "cancelled"]
"interrupt": {
"kind": "input",
"request": {
"state.question": "question"
},
"resume": {
"answer": "state.answer"
},
"outcomes": ["resumed", "cancelled"]
}
}
```
@@ -185,8 +161,7 @@ Joins control flow.
```json
{
"id": "join_results",
"kind": "join"
"join": {}
}
```
@@ -214,17 +189,13 @@ Example:
[
{
"op": "replace",
"path": "/steps/0/in/input.text",
"path": "/steps/echo/in/input.text",
"value": "message"
},
{
"op": "add",
"path": "/edges/-",
"value": {
"from": "echo",
"outcome": "error",
"to": "__end__"
}
"path": "/routes/echo/error",
"value": "__end__"
}
]
```