Workflow API Tutorial¶
This guide shows you how to drive the workflow engine programmatically. It walks the full lifecycle -- create a workflow definition, save a version, activate it into an execution, track per-node status, and cancel when needed -- with curl examples you can copy-paste.
The full schema for every endpoint is in the OpenAPI reference. This page is the narrative counterpart: when to call which endpoint, and how the pieces fit together.
Prerequisites¶
All examples assume:
- SynthOrg backend reachable at
http://localhost:3001 - A valid session cookie (
session=...) from/api/v1/auth/login jqinstalled for JSON pretty-printing
Workflow Lifecycle¶
Endpoint paths in the diagram below omit the /api/v1 prefix for readability; every curl example later in this guide uses the full /api/v1/... path.
flowchart LR
Create[POST /workflows] --> Draft[WorkflowDefinition v1.0.0]
Draft --> Validate[POST /workflows/:id/validate]
Draft --> Update[PUT /workflows/:id]
Update --> Version[new revision saved]
Draft --> Activate[POST /workflow-executions/activate/:id]
Activate --> Running[WorkflowExecution RUNNING]
Running --> Cancel[POST /workflow-executions/:id/cancel]
Running --> Done[COMPLETED / FAILED]
A workflow is a reusable DAG template. Activating one instantiates concrete tasks via TaskEngine that execute according to the graph's dependencies.
1. Create a workflow definition¶
curl -X POST http://localhost:3001/api/v1/workflows \
-H "Content-Type: application/json" \
-H "Cookie: ${SESSION}" \
-d '{
"name": "Weekly Status Report",
"description": "Research + draft + review + publish",
"workflow_type": "sequential",
"version": "1.0.0",
"nodes": [
{"id": "start", "type": "start", "position": {"x": 0, "y": 0}},
{"id": "research", "type": "task", "config": {"title": "Gather metrics", "assigned_to": "data_analyst"}, "position": {"x": 100, "y": 0}},
{"id": "draft", "type": "task", "config": {"title": "Draft report", "assigned_to": "content_writer"}, "position": {"x": 200, "y": 0}},
{"id": "review", "type": "task", "config": {"title": "Review", "assigned_to": "editor"}, "position": {"x": 300, "y": 0}},
{"id": "end", "type": "end", "position": {"x": 400, "y": 0}}
],
"edges": [
{"source": "start", "target": "research", "type": "sequential"},
{"source": "research", "target": "draft", "type": "sequential"},
{"source": "draft", "target": "review", "type": "sequential"},
{"source": "review", "target": "end", "type": "sequential"}
]
}' | jq
Response payload:
{
"success": true,
"data": {
"id": "wf_01j...",
"name": "Weekly Status Report",
"version": "1.0.0",
"revision": 1,
"created_at": "2026-04-21T10:00:00Z",
"nodes": [...],
"edges": [...]
}
}
Capture the ID:
2. Validate before activating¶
curl -X POST "http://localhost:3001/api/v1/workflows/${WF_ID}/validate" \
-H "Cookie: ${SESSION}" | jq
A valid workflow returns {"success": true, "data": {"is_valid": true}}. Invalid workflows return is_valid: false with a list of errors (unreachable nodes, missing TRUE/FALSE edges on conditionals, etc.). Activation will reject invalid workflows, so validate first.
3. List versions¶
Every create / update / rollback persists a content-addressable version snapshot. List them:
curl "http://localhost:3001/api/v1/workflows/${WF_ID}/versions" \
-H "Cookie: ${SESSION}" | jq '.data[] | {version, content_hash, saved_at, saved_by}'
4. Diff two versions¶
curl "http://localhost:3001/api/v1/workflows/${WF_ID}/versions/diff?from_version=1&to_version=2" \
-H "Cookie: ${SESSION}" | jq
5. Rollback to a previous version¶
curl -X POST "http://localhost:3001/api/v1/workflows/${WF_ID}/versions/rollback" \
-H "Content-Type: application/json" \
-H "Cookie: ${SESSION}" \
-d '{"target_version": 1, "reason": "v2 introduced a routing bug"}' | jq
Rollback writes a new revision whose content hash equals the restored version -- history is never mutated, just extended.
6. Activate into an execution¶
curl -X POST "http://localhost:3001/api/v1/workflow-executions/activate/${WF_ID}" \
-H "Content-Type: application/json" \
-H "Cookie: ${SESSION}" \
-d '{"context": {"quarter": "Q2"}}' | jq
Response:
{
"success": true,
"data": {
"id": "wfe_01j...",
"workflow_id": "wf_01j...",
"status": "running",
"node_executions": [
{"node_id": "start", "status": "completed"},
{"node_id": "research", "status": "task_created", "task_id": "task_01j..."},
{"node_id": "draft", "status": "pending"},
{"node_id": "review", "status": "pending"},
{"node_id": "end", "status": "pending"}
],
"created_at": "..."
}
}
Eager instantiation: every reachable task node gets a concrete Task created upfront with dependencies wired from the graph. TaskEngine handles execution ordering.
7. Track progress¶
curl "http://localhost:3001/api/v1/workflow-executions/${WFE_ID}" \
-H "Cookie: ${SESSION}" | jq '.data.node_executions[] | {node_id, status, task_id}'
Node statuses progress through: pending -> task_created -> task_completed / task_failed / skipped (for conditional branches not taken).
Real-time updates are also available via the WebSocket tasks channel -- see Notifications & Events.
8. List all executions for a definition¶
curl "http://localhost:3001/api/v1/workflow-executions/by-definition/${WF_ID}" \
-H "Cookie: ${SESSION}" | jq '.data[] | {id, status, created_at}'
9. Cancel a running execution¶
curl -X POST "http://localhost:3001/api/v1/workflow-executions/${WFE_ID}/cancel" \
-H "Cookie: ${SESSION}" | jq
Cancels every in-flight task and transitions the execution to CANCELLED. Already-COMPLETED nodes are preserved; only task_created and pending nodes move to task_failed.
Subworkflows¶
To compose reusable fragments (e.g. a "review" step shared across workflows), publish as a subworkflow and invoke via SUBWORKFLOW nodes:
# 1. Mark a workflow as publishable
curl -X PUT "http://localhost:3001/api/v1/workflows/${WF_ID}" \
-H "Content-Type: application/json" \
-H "Cookie: ${SESSION}" \
-d '{"is_subworkflow": true, "inputs": [{"name": "doc_id", "type": "string", "required": true}], "outputs": [{"name": "verdict", "type": "string", "required": true}]}'
# 2. Reference from a parent
# In the parent's nodes array:
# {"id": "review_step", "type": "subworkflow", "config": {
# "subworkflow_id": "wf_01j...",
# "version": "1.0.0",
# "input_bindings": {"doc_id": "@parent.draft_id"},
# "output_bindings": {"verdict": "@child.verdict"}
# }}
See the engine design for the full subworkflow contract (typed I/O, static cycle detection, runtime depth limit).
See Also¶
- OpenAPI Reference -- full schema for every endpoint
- Design: Task & Workflow Engine -- workflow types, node types, edge types, validation
- Notifications & Events -- subscribe to task lifecycle events in real time