P1 → Open Workflow DSL, v1

Normative. Derived from RUNBOOK.md §5. Every rule here has a worked example in spec/v1/examples/, and every example is validated against the published schema — vendored at spec/vendor/open-workflow-1.0.3.yaml — by packages/convert/src/ows.test.ts.

Without this mapping the catalog is a format island (§5). The mapping is what makes "implementable on any engine" checkable rather than rhetorical.

Which DSL, exactly

The specification RUNBOOK.md §5 calls "Open/Serverless Workflow DSL 1.0" is published today as Open Workflow DSL, version 1.0.3, under the Open Workflow Specification. This document targets 1.0.3 and every example declares dsl: 1.0.3, because a mapping that names a version nobody publishes is not checkable.

The mapping

P1 kind DSL Example
check run (or call) then switch on the result check.workflow.yaml
action run, preceded by listen when approval is required action.workflow.yaml
decision switch decision.workflow.yaml
wait wait wait.workflow.yaml
escalate emit then raise escalate.workflow.yaml

check

expect becomes the when of the passing branch; on_fail becomes the branch taken when it does not hold. Invariant 4 guarantees both exist, so the switch always has somewhere to go and never falls through.

Where a step carries a machine-checkable assert (Q12), that is what the condition is built from. A step with only prose expect cannot be converted faithfully: the mapping emits the switch with the prose in a comment and the condition left for a human, and the conversion report says so rather than inventing a predicate.

action

The approval gate is a task, not a flag. requires_approval: true becomes a listen immediately before the action, waiting for an approval event that names the step.

That shape is chosen deliberately: a gate expressed as a property of the action is invisible in a trace, while a gate expressed as a task means a runner that skipped it has visibly skipped a task. The forced-approval rule of §12 applies before conversion — a destructive or irreversible action is converted with the gate whatever the source document said.

decision

Branches come from branches. The default branch is the one with no when, which the DSL takes as the fallthrough. Invariant 3 requires it to exist.

wait

A P1 wait is always bounded — a duration, or a condition with a timeout — so the DSL form carries a bound too. Neither format can express an unbounded wait, which is the point: an unbounded wait is a hang with better manners.

escalate

emit the escalation event, then raise a fault. Escalation is an exit from the workflow rather than a step inside it, so the workflow ends faulted: something outside now owns the problem, and a run that ended in escalation must not be reported as success.

What is dropped

Conversion loses everything the catalog adds and the runner does not need: provenance, trust, licence, publisher, content_hash, x-layout, facets. The conversion report lists what went, because a silent drop is how someone later discovers their attestation did not travel with the workflow.

The reverse direction does not exist

Not "is not implemented" — must not exist. The DSL is more expressive than P1, so an arbitrary workflow does not fold back into it, and a converter that appeared to do so would silently discard the parts it could not represent. If one is written, people will rely on it.