Stakeholder
-Architect
-flowr v1.0.0
+Non-deterministic state machine specification for defining workflows in YAML.
+Engineer
-Introduction
++ In software systems, workflows are state machines. Development cycles, review processes, deployment pipelines, content moderation. All are sequences of states with transitions governed by conditions. Yet no declarative, validatable format exists for defining these state machines independently of their execution. +
++ This specification proposes a YAML format for non-deterministic state machine workflows. A flow definition declares states, transitions, and exit contracts. Transitions may be gated by evidence-based conditions. States may invoke subflows with call-stack semantics. Within-flow cycles are permitted for iterative workflows. The validator checks structural integrity. Tools query, track, and visualise. No execution engine. No side effects. Pure structure. +
++ This is not a new or revolutionary idea. State machines are well-understood. The gap is not in the concept but in the format: no declarative, validatable YAML standard exists for non-deterministic state machine workflows that treats validation as a first-class concern independent of execution. By giving a precise definition to this format, it becomes possible to build shared tooling (validators, editors, visualisers, session trackers) that work across any project that adopts the specification. The format is the foundation. +
+Overview
+A complete flow definition with all structural elements highlighted using the colour guide above.
+flow: deploy # unique name +version: 1.0.0 # semver +params: # optional parameters + - environment +exits: [deployed, failed] # declared outcomes +attrs: # opaque metadata + owner: SE # extension field + git: feature # extension field + +states: + - id: build # unique within flow + next: + ok: test + fail: failed + + - id: test + attrs: + timeout: 300 + conditions: # named condition groups + quality: + coverage: ">=80" + next: + pass: + to: staging + when: quality # named condition ref + fail: failed + + - id: staging + flow: smoke-test # subflow invocation + flow-version: "^1" + next: + pass: deployed + fail: review + + - id: review + next: + retry: staging # cycle back + abort: failed+
Specification
+The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
+ +Top-Level Structure
+-
+
-
+
A flow definition MUST contain
+flow(unique name string),version(semver),exits(non-empty list of exit names), andstates(ordered list of state objects).flow: deploy +version: 1.0.0 +exits: [deployed, failed] +states: [...]
+
+ -
+
The first state in
+statesis the initial state. A flow MUST contain at least one state.
+ -
+
+exitsdeclares the outcomes this flow offers to callers. Every exit name MUST be referenced by at least one state'snextmapping.exitsMUST be a non-empty list.
+
States
+-
+
-
+
Each state MUST have a unique
+idwithin the flow. State ids MUST NOT match any exit name (ambiguous reference).
+ -
+
Each state MUST have a
+nextmapping (trigger → target), unless the state only references exits as terminal targets.
+ -
+
A state MAY declare
+attrs: an opaque dict. State-levelattrsreplace (not merge) flow-levelattrs. The specification does not interpretattrs.states: + - id: build + attrs: + timeout: 600 + docker: true + next: + ok: test
+
+
Transitions
+-
+
-
+
Every
+nexttarget MUST resolve to either a state id or an exit name. A target that matches both is a validation error. A target that matches neither is a validation error.
+ -
+
A transition MAY include
+when: a dict of guard conditions. Evidence keys MUST matchwhenkeys exactly (closed schema). No extra keys, no missing keys.next: + approve: + to: deployed + when: { score: ">=80" }
+
+ -
+
Guard conditions use expression operators:
+==(equality),!=(inequality),>=<=><(numeric comparison). A plain value without an operator prefix is an implicit==(e.g.,status: approvedis equivalent tostatus: ==approved). Numeric extraction is applied to both sides:>=80%vs evidence75%compares 80 vs 75. Multiple conditions in awhendict are AND-combined. No inheritance. Every condition is explicit on the transition where it applies.++Rule 9. Guarded transition+ +
+
Named Condition Groups
+-
+
-
+
A state MAY declare
+conditions: a mapping of named condition groups. Each group is a condition-map (key-value pairs of expressions). Named groups are referenced by transitions viawhen.states: + - id: review + conditions: + quality_gate: + score: ">=80" + coverage: ">=90" + next: + approve: + to: published + when: quality_gate # named ref
+
+ -
+
The
+whenfield on a transition accepts three forms: a dict (inline condition-map), a string (reference to a named group), or a list (mix of named refs and inline dicts). All conditions are AND-combined. A named ref that does not match a group defined on the same state MUST cause a validation error.next: + deploy: + to: production + when: + - quality_gate # named ref + - { override: "==yes" } # inline + reject: failed
+
+
Subflows
+-
+
-
+
A state with a
+flowfield becomes a subflow invocation. Theflowvalue is a relative file path. Parentnextkeys MUST match childexitsexactly.++Rule 12. Subflow nesting+ +
+ -
+
Subflows use a call-stack: push on entry, pop on exit. Context is isolated to the current flow. A
+flow-versionfield MAY constrain compatible child versions using semver ranges.
+
Validation
+-
+
-
+
A conforming validator MUST check all of the following at load time:
+-
+
- Every
nexttarget resolves to a state id or exit name
+ - No
nexttarget is ambiguous (matches both state id and exit name)
+ - Parent
nextkeys match childexitsexactly
+ - No cross-flow cycles (detected via DFS) +
- Exit names in
exitsare referenced by at least one state
+ - Named condition references in
whenresolve to a group defined on the same state
+ - Params without defaults are provided at flow invocation time +
+ - Every
Sessions
+-
+
-
+
A session tracks
+flow,state,name,created_at,updated_at,stack(subflow call stack), andparams. Session writes MUST be atomic (temp-file-then-rename). Filesystem is the source of truth.flow: deploy +state: review +name: default +created_at: "2026-05-01T10:00:00" +updated_at: "2026-05-01T14:25:00" +stack: [] +params: {}
+
+
Cycles and Versioning
+-
+
-
+
Within-flow cycles are allowed (a state MAY transition to an earlier state in the same flow). Cross-flow cycles are forbidden.
+++Rule 16. Within-flow cycle+ +
+ -
+
Flows SHOULD use semver for versioning. Adding a new exit is a minor bump. Adding states is a patch. Removing or renaming exits is a major (breaking) change.
+
+
Extension Fields
+-
+
-
+
A flow definition MAY contain fields not specified in this document. Such extension fields are not interpreted by a conforming validator. The keys defined in this specification (
+flow,version,params,exits,attrs,states,id,next,to,when,conditions,flow,flow-version) are reserved. Implementations MUST NOT assign semantics to reserved keys beyond what this specification defines.
+ -
+
The
+attrsfield is the designated extension point. Implementations SHOULD place implementation-specific data (agent assignments, tool configuration, environment variables) insideattrsrather than as top-level keys. This keeps the structural contract separate from tool-specific configuration.
+
Formal Syntax
+The following grammar defines valid flow definition structure:
+Visual Reference
+The following diagrams illustrate valid structural patterns. They are normative: they depict what the specification permits.
+ +Minimal Flow
+The smallest valid flow: one state, one exit, one transition.
+Non-Deterministic Branching
+A state with multiple outgoing transitions. The actor chooses which path to take, not the machine. This is what makes flowr non-deterministic.
+Guarded Transition
+A transition that requires evidence. The actor sends a trigger with evidence, and the condition engine validates it. The guarded path is visually distinct.
+Subflow Invocation
+A parent state invokes a child flow. The call-stack pushes on entry, pops on exit. Parent next keys match child exits.
Within-Flow Cycle
+A state transitions back to an earlier state in the same flow. Permitted. Cross-flow cycles are forbidden.
+Normative Examples
+Complete, valid flow definitions that serve as reference implementations.
+ +1. Deploy Pipeline
+A linear flow with exits. The first state is initial. States reference exits in next.
flow: deploy +version: 1.0.0 +exits: [deployed, failed] + +states: + - id: prepare + next: + ready: execute + + - id: execute + next: + success: deployed + error: failed+
2. Review with Guard Conditions
+Guarded transitions requiring evidence. Both approve and reject are gated. The actor provides evidence and the engine validates it.
+flow: review +version: 1.0.0 +exits: [approved, rejected] + +states: + - id: pending + next: + submit: under-review + + - id: under-review + next: + approve: + to: approved + when: { score: ">=80" } + reject: + to: rejected + when: { score: "<40" }+
3. TDD Cycle (Within-Flow Cycle)
+States loop back to earlier states. The red state is revisited for each new test example.
flow: tdd-cycle +version: 1.0.0 +exits: [all_green, blocked] + +states: + - id: red + next: + test_written: green + blocked: blocked + + - id: green + next: + test_passes: refactor + + - id: refactor + next: + next_example: red + all_pass: all_green+
4. Feature Flow (Subflow Invocation)
+A parent flow invokes a child flow via flow:. Parent next keys match child exits exactly.
# Parent +flow: feature-flow +version: 1.0.0 +exits: [completed, cancelled] + +states: + - id: scope + flow: scope-cycle + next: + complete: build + blocked: cancelled + + - id: build + next: + done: completed+
Conformance
+A conforming implementation MUST satisfy all rules marked MUST in this specification. A conforming implementation SHOULD satisfy all rules marked SHOULD unless there is a documented reason not to.
+Conformance levels:
+| Level | Meaning | Requirement |
|---|---|---|
| MUST | Required for all conforming implementations | Immutable loaded flows, closed evidence schema, validation rules |
| SHOULD | Recommended but optional | Filesystem wins over session cache on conflict, semver for flows |
| MAY | Optional extension | Per-state attrs, flow params, Mermaid export |
FAQ
+ +Why not BPMN, SCXML, Serverless Workflow, XState, or Temporal?
+Existing solutions target execution engines or are framework-specific. They define what the workflow does: side effects, retries, timeouts, error handling. flowr defines what the workflow is: its structure, states, transitions, and guard conditions. By staying agnostic to execution, any tool (editors, visualizers, CI systems, AI agents) can build on the same structural foundation without coupling to a runtime.
+Why YAML and not JSON or XML?
+YAML is the most human-readable format for nested structures with string keys. It supports comments (needed for specification examples), requires less punctuation than JSON, and is more concise than XML. The format prioritises authoring experience.
+Why no execution engine?
+flowr defines what a workflow is, not what it does. Execution engines are opinionated. They prescribe side effects, error handling, retry logic. flowr stays agnostic so any tool (editors, visualizers, CI systems, AI agents) can build on the same structural foundation without coupling to a runtime.
+How do I handle parallel states?
+Parallel (fork-join) states are out of scope for v1. A flow is a sequence of states with transitions. If your workflow needs parallelism, model each branch as a separate subflow and coordinate through a parent flow.
+Can a state have no next?
+ A state MUST have next unless it only references exits as terminal targets. Every state must declare where it can go, even if the only option is an exit.
What happens if evidence doesn't match a condition?
+The transition is blocked. The actor receives a warning indicating which conditions failed and what evidence is required. No state change occurs.
+How do subflow versions work?
+flow-version accepts a semver range (e.g., "^1"). The validator checks that the referenced flow's version satisfies the constraint. Breaking changes (renamed exits) require a major version bump.
Why do attrs replace instead of merge?
+Merge semantics require defining deep-merge rules, conflict resolution, and precedence, adding complexity without universal benefit. Replace is unambiguous: if a state declares attrs, those are its attrs. If it doesn't, it inherits flow-level attrs.
Can I use flowr without the CLI?
+Yes. flowr is a specification first. The CLI is a reference implementation. Any tool can parse the YAML format and implement validation, querying, or visualisation independently. The specification is the contract, not the tool.
+Can I add custom fields to a flow definition?
+Yes. Fields not defined in this specification are extension fields and are not interpreted by a conforming validator. The reserved keys (flow, version, params, exits, attrs, states, id, next, to, when, conditions, flow, flow-version) belong to the spec. Place implementation-specific data inside attrs to keep the structural contract clean.