Overview
A managed engine where agents conceive and evolve processes, and where several agents cooperate over one shared, auditable, durable substrate. Part of the parklab family (store / mindmap).
State = Process
Entity state machines and process orchestration are the same model (WDL) — two profiles, one grammar.
Evolve by version
A workflow has a stable slug; each publish appends an immutable version. Instances pin to their start version.
Delegated to DBOS
One generic interpreter reads the pinned WDL; steps are journaled, waits are durable. Instances survive crashes.
run_instance(id) bridges them: it reads the pinned definition and evaluates states one at a time. Because the version can't change mid-flight, deterministic replay holds — pure interpretation re-runs in memory while every side effect is a journaled, exactly-once step.Core concepts
Immutable, versioned definitions
“Updating” a workflow means publishing a new version under the same slug. Running instances are pinned to the version they started on and run to completion on it — there is no live migration. This isn't a convenience; it's a correctness requirement: DBOS deterministic replay needs the logic fixed for an instance's lifetime.
Two profiles, one grammar
The same WDL expresses an entity state machine (event-centric, like order-lifecycle) and a process orchestration (activity-centric, like doc-triage). The engine and definition language are one; the difference is only which state types dominate.
Expressions
- Guards & computed values — JSONLogic. Data access via
{"var":"context.path"}. Event transitions also see{"var":"event.field"}. - String templating —
{{ $.context.path }}(JSONPath). A whole-token string resolves to the raw value (dict/number preserved); embedded tokens interpolate to text. - assign — a flat mapping
{"context.k": <rule>}writes computed values back into context.
WDL reference
A WDL 1.0 document has wdl_version, name, initial, and a states map. Every state declares a type.
| type | meaning | key fields |
|---|---|---|
| event | Wait for an external event (entity-state primitive) | on |
| task | Perform an activity (external call) | activity, on_success, on_error, retry, timeout_s |
| choice | Guard-based branching | choices[], default |
| parallel | Fan out into branches, then join | branches[], next |
| map | Iterate over a collection | items, iterator, next |
| wait | Durable timer and/or external event | seconds or on, next |
| pass | Transform context, then move on | assign, next |
| succeed | Terminal success | result |
| fail | Terminal failure | error, cause |
Activities (what a task runs)
| kind | shape |
|---|---|
| http | { method, url, headers?, body? } — templated; non-2xx → on_error |
| agent | { prompt, system?, model?, output_schema? } — Claude; output_schema forces structured JSON |
| mcp | { server, tool, args } — calls an external MCP tool over Streamable HTTP |
| subworkflow | { workflow, version?, input } — starts a child instance and awaits it (durable) |
| noop | pass-through / testing |
Examples
Two definitions, one grammar — an entity machine and a process orchestration.
A · entity state machine
{
"wdl_version": "1.0", "name": "order-lifecycle", "initial": "pending",
"states": {
"pending": { "type": "event", "on": { "PAY": {"target":"paid"}, "CANCEL": {"target":"cancelled"} } },
"paid": { "type": "event", "on": { "SHIP": {"target":"shipped"} } },
"shipped": { "type": "event", "on": { "DELIVER": {"target":"delivered"} } },
"delivered": { "type": "succeed" },
"cancelled": { "type": "fail", "error": "ORDER_CANCELLED" }
}
}B · process orchestration
{
"wdl_version": "1.0", "name": "doc-triage", "initial": "classify",
"states": {
"classify": {
"type": "task",
"activity": { "kind": "agent",
"prompt": "Classify this document: {{ $.context.doc }}",
"output_schema": { "type":"object", "properties":{"category":{"type":"string"}} } },
"on_success": { "target":"route", "assign":{"context.category":{"var":"result.category"}} },
"on_error": { "target":"needs_human" }
},
"route": { "type":"choice",
"choices":[ {"guard":{"==":[{"var":"context.category"},"urgent"]}, "target":"escalate"} ],
"default":"archive" },
"escalate": { "type":"succeed" },
"needs_human": { "type":"wait", "on":{ "RESOLVE":{"target":"archive"} } },
"archive": { "type":"succeed" }
}
}REST API
JSON over HTTPS. Everything under /workflows and /instances requires Authorization: Bearer <ADMIN_TOKEN>. Health & docs are public.
Definitions & versions
| POST/workflows/validate | Validate WDL without persisting (agents call this before publishing) |
| POST/workflows | Create a workflow (optionally with its first version) |
| GET/workflows | List workflows |
| GET/workflows/{slug} | Metadata + version summaries |
| PATCH/workflows/{slug} | Update meta / default_version |
| POST/workflows/{slug}/versions | Publish a new immutable version (= evolve) |
| GET/workflows/{slug}/versions/{v} | The full WDL of a version |
| GET/workflows/{slug}/versions/{v}/mermaid | Render the version as a Mermaid diagram |
Instances
| POST/workflows/{slug}/instances | Start an instance (input context, optional pinned version) |
| GET/instances/{id} | Current state + context + status |
| POST/instances/{id}/events | Send {type, payload} → drives a transition |
| GET/instances/{id}/history | Append-only audit log |
| POST/instances/{id}/cancel | Terminate |
| GET/stats | Workflow / version / instance counts (public) |
Interactive: Swagger UI → · ReDoc → · openapi.json →
MCP server
Streamable HTTP at /mcp (bearer-authed). An agent runs the whole closed loop — validate → publish → instantiate → drive by events → inspect — as MCP tools.
workflow.validate workflow.list workflow.get workflow.publish workflow.start workflow.send_event workflow.get_state workflow.get_history workflow.list_instances workflow.cancel workflow.render_mermaid
{
"mcpServers": {
"workflow": {
"url": "https://workflow.parklab.work/mcp",
"headers": { "Authorization": "Bearer <ADMIN_TOKEN>" }
}
}
}Quickstart
Publish a workflow, start an instance, drive it through events — end to end.
# publish (validation runs automatically) curl -X POST https://workflow.parklab.work/workflows \ -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \ -d '{"slug":"order","name":"Order","definition":{ ...WDL A... }}' # start an instance → returns {id, current_state:"pending", status:"waiting"} curl -X POST https://workflow.parklab.work/workflows/order/instances \ -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"input":{}}' # drive it (each event is durable; queued if the instance isn't parked yet) curl -X POST https://workflow.parklab.work/instances/$ID/events \ -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"type":"PAY"}' # inspect: GET /instances/$ID → completed @ delivered
ADMIN_TOKEN (it is not shown here). Retrieve it on the host with grep ^ADMIN_TOKEN= /opt/workflow/infra/prod/.env.Architecture
Agents speak JSON (REST or MCP). The control plane validates & stores immutable versions and starts instances; the execution plane (DBOS) gives each instance durable, exactly-once execution. One Postgres backs both.
Stack — Python 3.12 · FastAPI · DBOS Transact · PostgreSQL 16 · JSONLogic · MCP (Streamable HTTP) · UUIDv7. Deployed as a Docker container behind the shared Caddy on the parklab Vultr box, alongside store and mindmap.