flowr v1.0.0

Non-deterministic state machine specification for defining workflows in YAML.

Mandatory Optional Extension

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

  1. A flow definition MUST contain flow (unique name string), version (semver), exits (non-empty list of exit names), and states (ordered list of state objects).

    flow: deploy
    version: 1.0.0
    exits: [deployed, failed]
    states: [...]
  2. The first state in states is the initial state. A flow MUST contain at least one state.

  3. exits declares the outcomes this flow offers to callers. Every exit name MUST be referenced by at least one state's next mapping. exits MUST be a non-empty list.

States

  1. Each state MUST have a unique id within the flow. State ids MUST NOT match any exit name (ambiguous reference).

  2. Each state MUST have a next mapping (trigger → target), unless the state only references exits as terminal targets.

  3. A state MAY declare attrs: an opaque dict. State-level attrs replace (not merge) flow-level attrs. The specification does not interpret attrs.

    states:
      - id: build
        attrs:
          timeout: 600
          docker: true
        next:
          ok: test

Transitions

  1. Every next target 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.

  2. A transition MAY include when: a dict of guard conditions. Evidence keys MUST match when keys exactly (closed schema). No extra keys, no missing keys.

    next:
      approve:
        to: deployed
        when: { score: ">=80" }
  3. Guard conditions use expression operators: == (equality), != (inequality), >= <= > < (numeric comparison). A plain value without an operator prefix is an implicit == (e.g., status: approved is equivalent to status: ==approved). Numeric extraction is applied to both sides: >=80% vs evidence 75% compares 80 vs 75. Multiple conditions in a when dict are AND-combined. No inheritance. Every condition is explicit on the transition where it applies.

    Rule 9. Guarded transition

Named Condition Groups

  1. 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 via when.

    states:
      - id: review
        conditions:
          quality_gate:
            score: ">=80"
            coverage: ">=90"
        next:
          approve:
            to: published
            when: quality_gate        # named ref
  2. The when field 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

  1. A state with a flow field becomes a subflow invocation. The flow value is a relative file path. Parent next keys MUST match child exits exactly.

    Rule 12. Subflow nesting
  2. Subflows use a call-stack: push on entry, pop on exit. Context is isolated to the current flow. A flow-version field MAY constrain compatible child versions using semver ranges.

Validation

  1. A conforming validator MUST check all of the following at load time:

    • Every next target resolves to a state id or exit name
    • No next target is ambiguous (matches both state id and exit name)
    • Parent next keys match child exits exactly
    • No cross-flow cycles (detected via DFS)
    • Exit names in exits are referenced by at least one state
    • Named condition references in when resolve to a group defined on the same state
    • Params without defaults are provided at flow invocation time

Sessions

  1. A session tracks flow, state, name, created_at, updated_at, stack (subflow call stack), and params. 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

  1. 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
  2. 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

  1. 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.

  2. The attrs field is the designated extension point. Implementations SHOULD place implementation-specific data (agent assignments, tool configuration, environment variables) inside attrs rather 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:

<flow-definition> ::= flow: <string> version: <semver> params: <param-list>? exits: [<exit-name>, ...] attrs: <mapping>? states: [<state>, ...] <state> ::= id: <identifier> next: <transition-map> conditions: <condition-groups>? # named condition groups flow: <flow-reference>? # subflow invocation flow-version: <semver-range>? attrs: <mapping>? # replaces flow-level <transition-map> ::= <trigger>: <target> # simple | <trigger>: { to: <target>, when: <when-clause> } # guarded <target> ::= <state-id> | <exit-name> <when-clause> ::= <condition-map> # inline dict | <identifier> # named ref to condition group | [<when-clause>, ...] # list: AND-combined <condition-groups> ::= { <identifier>: <condition-map>, ... } <condition-map> ::= { <key>: <expression>, ... } <expression> ::= <value> # implicit == (exact match) | ==<value> # equality | !=<value> # inequality | >=<numeric> # greater or equal | <=<numeric> # less or equal | ><numeric> # greater than | <<numeric> # less than <param-list> ::= [<string>, ...] # required params | [{ name: <string>, default: <value>? }, ...] # optional with default <extension-fields> ::= <string>: <value> # any non-reserved key # reserved: flow, version, params, # exits, attrs, states, id, next, # to, when, conditions, flow, flow-version

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:

LevelMeaningRequirement
MUSTRequired for all conforming implementationsImmutable loaded flows, closed evidence schema, validation rules
SHOULDRecommended but optionalFilesystem wins over session cache on conflict, semver for flows
MAYOptional extensionPer-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.