v0.9.0 — Open Specification · March 2026

Process definitions that are contracts, not programs.

A declarative YAML format for workflow orchestration — 25 directives, 463 validation rules, zero runtime surprises.

25 directives 463 validation rules 27 example flows

No signup. No paywall. No vendor lock-in.

order-fulfillment.flowmarkup.yaml YAML
flowmarkup:
  title: Order Fulfillment
  input:
    order: {$schema: Order}
    priority: {$default: standard}
  throws: [PaymentDeclinedError, OutOfStockError]
  requires: {}
  transaction: true
  do:
  - set:
      total: =order.items.map(i, i.price * i.quantity).sum()
  - call:
      service: payment
      operation: charge
      params: {amount: =total}
      circuitBreaker: 5/payment
      retry: 3/2s/EXPONENTIAL
      rollback:
      - call: {service: payment, operation: refund}

Why existing approaches break down

Business processes are contracts

Payment pipelines, compliance workflows, multi-service orchestration — these are promises to customers and regulators. They must be verified before execution, not just hoped to work.

Code and AI both fall short

Code buries logic under boilerplate — HTTP clients, retry decorators, connection pools. AI orchestration is non-deterministic and unauditable. Neither can be reviewed by non-engineers.

A single, validatable YAML document

FlowMarkup captures the entire process in one file. What you read is what executes. 463 static analysis rules catch errors at authoring time, not in production.

60 lines of Python becomes 19 lines of YAML

Python — manual resilience 60 lines
import httpx, time, random
from tenacity import retry, stop_after_attempt, wait_exponential

class CircuitBreaker:
    def __init__(self, threshold=5, window=60, reset=30):
        self.threshold = threshold
        self.window = window
        self.reset_timeout = reset
        self.failures = []
        self.state = "closed"
        self.opened_at = 0

    def call(self, fn, *args):
        if self.state == "open":
            if time.time() - self.opened_at < self.reset_timeout:
                raise CircuitOpenError()
            self.state = "half-open"
        try:
            result = fn(*args)
            self.state = "closed"
            self.failures = []
            return result
        except Exception as e:
            self.failures.append(time.time())
            recent = [f for f in self.failures
                      if time.time() - f < self.window]
            if len(recent) >= self.threshold:
                self.state = "open"
                self.opened_at = time.time()
            raise

breaker = CircuitBreaker(threshold=5, window=60, reset=30)

@retry(stop=stop_after_attempt(3),
       wait=wait_exponential(multiplier=2))
def charge_payment(order_id, amount, currency):
    def _do_charge():
        with httpx.Client(timeout=30) as client:
            resp = client.post(
                "https://api.payment.com/charge",
                json={
                    "order_id": order_id,
                    "amount": amount,
                    "currency": currency,
                },
                headers={"Authorization": f"Bearer {API_KEY}"},
            )
            resp.raise_for_status()
            return resp.json()["tx_id"]
    return breaker.call(_do_charge)

def refund_payment(tx_id):
    for attempt in range(5):
        try:
            with httpx.Client(timeout=30) as client:
                resp = client.post(
                    "https://api.payment.com/refund",
                    json={"tx_id": tx_id},
                    headers={"Authorization": f"Bearer {API_KEY}"},
                )
                resp.raise_for_status()
                return
        except Exception:
            time.sleep(3 * (2 ** attempt))
            if attempt == 4: raise
FlowMarkup — declarative 17 lines
flowmarkup:
  transaction: true
  do:
  - call:
      service: payment
      operation: charge
      params: {order_id: =order.id, amount: =order_total, currency: USD}
      circuitBreaker: 5/payment
      retry: 3/2s/EXPONENTIAL
      timeout: 30s
      result: {payment_tx_id: =RESULT.tx_id}
      rollback:
      - call:
          service: payment
          operation: refund
          params: {tx_id: =payment_tx_id}
          retry: 5/3s/EXPONENTIAL
Circuit breakers, retry, timeout, and error handling — built in, not bolted on.

Three layers of validation catch errors before execution

Every flow passes through structural, semantic, and behavioral verification at authoring time — not at runtime.

1

JSON Schema

Structural conformance. Validates directive names, required fields, value types, and nesting rules against the Draft 2020-12 schema.

2

Static Analysis

Semantic correctness. 463 rules verify type safety, variable scope, expression validity, security constraints, and contract compliance.

3

Testing Framework

Behavioral verification. Mocks, fixtures, assertions, and fault injection exercise your flows against scenarios — in milliseconds.

broken-flow.flowmarkup.yaml YAML
flowmarkup:
  do:
  - call:
      service: claude
      params:
        prompt: =user_input   # never defined
      result:
        ai_output: =RESULT.text

  - log: "Result: {{ result }}"  # wrong name

Validation Output

CORE-1 [ERROR] Input completeness
Variable user_input referenced in params.prompt is not declared in any reachable scope.
CORE-2 [WARN] Output reachability
Variable result referenced in log step is not defined — did you mean ai_output?

463 rules catch bugs like these at authoring time. Not at 3 AM in production.

Built for processes that cannot afford to fail

Every design decision serves one goal: the process definition is a formal contract that can be validated, tested, and audited before a single step executes.

463 Static Analysis Rules

Variable scope, type safety, expression validity, security constraints — every rule with a documented ID, severity, and fix example.

Non-Turing-Complete (CEL)

CEL expressions cannot loop, perform I/O, or cause side effects. This enables static guarantees no general-purpose language can provide.

Opaque Secrets

SECRET values cannot be logged, interpolated, or returned. Resolved only at action boundaries. Enforced structurally, not by policy.

Native Streaming

Built-in yield/onYield for real-time data. Stream LLM tokens, progress updates, or partial results — no library add-on.

Saga Transactions + Rollback

Declarative transaction: true with per-step rollback: handlers. Automatic compensating actions on failure.

Circuit Breakers

One declarative block — threshold, window, reset timeout, scope. Not 30 lines of state management in a custom class.

Native Testing Framework

Mocks, fixtures, assertions, fault injection, snapshot testing — all in YAML. Test in milliseconds without external services.

Capability Security

Deny-by-default for exec, mail, network. Sub-flows never gain privileges their parent doesn't hold. Monotonic capability decrease.

Designed for both humans and AI

For Humans

  • 20 lines of YAML replaces 200+ lines of code
  • Git-friendly diffs — review process changes like code
  • Natural keywords: forEach, catch, timeout
  • Business logic visible without infrastructure noise

For AI

  • 6,700+ line formal specification as training context
  • JSON Schema validates structure automatically
  • 463 SA rules catch AI-generated mistakes
  • Deterministic verification — no hallucinated behavior

AI models are services inside your flow — not the orchestrator

Your flow controls sequencing, branching, retry, and data flow — deterministically. AI models are called as services, just like any API, with typed inputs, outputs, timeouts, and error handling.

The model generates text, classifies documents, or scores results. The flow decides what to do with those results. No prompt-driven routing. No hallucinated tool calls.

AI-assisted authoring

The Claude AI skill bundle provides FlowMarkup-aware authoring — and the same 463 validation rules that catch human errors catch AI errors too.

AI service call YAML
# AI is a service, called like any other
- call:
    service: claude
    operation: classify
    timeout: 60s
    retry: 3/2s/EXPONENTIAL
    params: {text: =customer_message, categories: ["billing", "technical", "general"]}
    result: {category: =RESULT.classification}

# The flow decides what to do
- switch:
    value: =category
    match:
      billing:
        run: {flow: billing-support.flowmarkup.yaml}
      technical:
        run: {flow: tech-support.flowmarkup.yaml}

Entire classes of bugs, eliminated by design

FlowMarkup doesn't just catch bugs — it makes them structurally impossible. Every row below is a class of error your team stops worrying about.

Error Class In Typical Code FlowMarkup Prevention
Null reference Unchecked null access crashes at runtime $nullable annotations + SA type checks
Data race Two threads write same variable SA-CONC detects concurrent writes
Deadlock Two locks acquired in wrong order Runtime DeadlockError via wait-for graph
Secret leakage API key logged or returned in response SECRET.* is opaque — resolved at action boundaries only
Floating-point error 0.1 + 0.2 ≠ 0.3 All numbers are BigDecimal by default
Unhandled exception Crash on error, no cleanup try/catch with typed error hierarchy
Injection User input in query string params: separates data from operations
TOCTOU race Check-then-act with stale data set: has OCC with transparent retry
Missing timeout Service call hangs indefinitely SA rules flag missing timeout: on every action
Cascading failure One service down takes system down circuitBreaker: with threshold and scope
Partial transaction Payment charged, inventory not reserved transaction: true with automatic rollback
Resource leak File handle or connection not closed No manual resource management
Off-by-one Wrong loop bounds forEach: over collections, not indices
Type coercion "5" + 3 = "53" in JavaScript CEL is strictly typed — no implicit coercion
Invalid input Bad URL or path traversal accepted Built-in semantic types validated at boundaries

How FlowMarkup compares

FlowMarkup is not a framework or a library. It is a specification — and that distinction changes everything.

Capability FlowMarkup Step Functions Temporal Airflow BPMN
Format YAML specification JSON (ASL) Host language code Python code XML
Static validation 463 rules Schema only Compiler checks Linters Schema only
Circuit breaker Built-in Library
Native streaming yield/onYield Signals
Opaque secrets Structural IAM reference Application-level Connections
Native testing YAML-first Local emulator Unit test SDK pytest
Saga / rollback Declarative Catch + compensate Manual Compensation events
Version migration onVersionChange New execution Patching DAG versioning

Example flows for every pattern

Every example passes all three validation layers. Browse the showcase or explore all 27 flows.

Run branches concurrently. Use failPolicy: COMPLETE to wait for all results, or let the first finisher win with a RACE pattern.

ai-comparison.flowmarkup.yamlYAML
# Two AI models run in parallel — pick the best result
- parallel:
    failPolicy: COMPLETE
    claude_branch:
    - call:
        service: claude
        operation: generate
        timeout: 60s
        retry: 3/2s/EXPONENTIAL
        result: {claude_result: =RESULT.text}
    gemini_branch:
    - call:
        service: gemini
        operation: generate
        timeout: 60s
        result: {gemini_result: =RESULT.text}
- set: {best: =claude_result.size() > gemini_result.size() ? claude_result : gemini_result}

Open in Inspector →

Stream data mid-execution with yield. Callers subscribe with onYield or collect results via $yields.

streaming-llm.flowmarkup.yamlYAML
flowmarkup:
  title: Streaming LLM Response
  yields: {$kind: TEXT}
  do:
  - call:
      service: claude
      operation: generate
      params: {prompt: =user_prompt, stream: true}
      onYield:
        as: token
        do:
          yield: =token
      result: {full_response: =RESULT.text}

Open in Inspector →

Typed error handling with try/catch. Map specific error types to specific recovery strategies.

Error handling patternYAML
- try:
    do:
      call:
        service: payment
        operation: charge
        params: {amount: =total, currency: USD}
        timeout: 30s
        retry: 3/2s/EXPONENTIAL
    catch:
      PaymentDeclinedError:
      - log: "Declined: {{ERROR.MESSAGE}}"
      - throw: {error: PaymentDeclinedError, message: =ERROR.MESSAGE}
      TimeoutError:
      - set: {status: pending_retry}
      - emit: {event: payment_timeout, data: {order_id: =order.id}}

Open in Inspector →

Declarative sagas with transaction: true. Root-level shorthand wraps the entire flow in one implicit transaction. Each step defines its own rollback: — automatic compensating actions on failure.

idempotency-key.flowmarkup.yamlYAML
flowmarkup:
  transaction: true
  onRollbackError: CONTINUE
  do:
  - call:
      service: payments
      operation: charge
      params: {order_id: =order_id, amount: =amount}
      result: {charge_id: =RESULT.charge_id}
      rollback:
      - call:
          service: payments
          operation: refund
          params: {charge_id: =charge_id}
          retry: 3/2s/EXPONENTIAL
  - call:
      service: ledger
      operation: record
      params: {charge_id: =charge_id, order_id: =order_id, amount: =amount}

Open in Inspector →

Cross-workflow coordination with emit/waitFor. Filter events by condition, set timeouts, capture event data.

event-signaling.flowmarkup.yamlYAML
# Wait for human approval with timeout
- waitFor:
    event: order_approval
    scope: GLOBAL
    condition: =EVENT.DATA.order_id == order.id
    timeout: 24h
    capture:
      decision: =EVENT.DATA.decision
      approver: =EVENT.DATA.approver

# Notify downstream systems
- emit:
    event: order_fulfilled
    data: {order_id: =order.id, tracking: =tracking_id}

Open in Inspector →

Execute commands on remote servers via ssh:. Parse stdout as JSON, inject secrets as env vars, orchestrate multi-server deployments.

ssh-commands.flowmarkup.yamlYAML
# Check disk space on remote server
- ssh:
    service: prod_server
    command: df
    args: [-h, /opt/app]
    timeout: 30s
    result: {disk_info: =RESULT.stdout}

# Deploy with JSON auto-parsing
- ssh:
    service: prod_server
    command: deploy.sh
    args: [status, --format, json]
    parseAs: JSON
    result: {deploy_status: =RESULT.stdout}

# Secrets as environment variables
- ssh:
    service: staging
    command: deploy.sh
    args: [deploy, =ENV.DEPLOY_VERSION]
    env: {DEPLOY_TOKEN: =SECRET.deploy_token, DEPLOY_ENV: staging}
    timeout: 10m
    retry: 3/30s/EXPONENTIAL

Open in Inspector →

Unified storage: directive for S3 and SFTP. Get, put, list, check existence, and stream cross-storage transfers — all with parseAs auto-decoding.

storage-operations.flowmarkup.yamlYAML
# Download JSON from S3 and auto-decode
- storage:
    service: s3_data
    operation: get
    path: "config/settings.json"
    parseAs: JSON
    timeout: 30s
    result: {settings: =RESULT.data}

# Cross-storage transfer: S3 → SFTP (streamed)
- storage:
    operation: transfer
    source: {service: s3_data, path: ="reports/" + report_date + ".csv"}
    target: {service: backup_sftp, path: ="/backups/" + report_date + ".csv"}
    overwrite: true
    timeout: 30m
    result: {backup_path: =RESULT.targetPath}

# Upload processed data to SFTP
- storage:
    service: sftp_dest
    operation: put
    path: =dest_dir + "/" + file_name
    data: =active_records
    overwrite: true
    timeout: 5m

Open in Inspector →

Parse raw JSON strings with parse(JSON), auto-decode from storage/SSH with parseAs: JSON, filter and transform structured data in CEL.

JSON parsing patternsYAML
# Parse raw JSON string with CEL
- set: {parsed: =api_response.parse(JSON)}
- assert: =parsed.items != null

# Filter and transform in CEL
- set:
    active: =parsed.items.filter(e, e.status == "active")
    total: =active.map(e, e.amount).sum()

# Auto-decode JSON from S3 with parseAs
- storage:
    service: s3_data
    operation: get
    path: "config/settings.json"
    parseAs: JSON
    result: {settings: =RESULT.data}

# SSH with JSON auto-parsing
- ssh:
    service: prod_server
    command: deploy.sh
    args: [status, --format, json]
    parseAs: JSON
    result: {deploy_status: =RESULT.stdout}

Open in Inspector →

Get started in 3 steps

1

Write

Define your flow in YAML — input contract, steps, error handling.

flowmarkup:
  title: Hello World
  input:
    name: STRING
  output:
    greeting: TEXT
  requires: {}
  do:
    return:
      greeting: "Hello, {{name}}!"
2

Validate

Open the Inspector, paste your flow, see results instantly. JSON Schema + 463 SA rules + testing.

Open Inspector →
3

Run

Implement an engine in your language. The cross-language guide covers Java, Go, Python, TypeScript, Rust, and C#.

Cross-Language Guide

Specification documents

Frequently asked questions

What is FlowMarkup?

FlowMarkup is a declarative, non-Turing-complete YAML format for defining process flows. It provides exactly 25 directives for sequencing, branching, error handling, parallel execution, and typed data flow. Every flow is validated by JSON Schema, 463 static analysis rules, and a native testing framework before execution.

How is FlowMarkup different from Temporal, Airflow, or Step Functions?

Workflow frameworks are imperative programs in a host language — the process definition is scattered across code, configuration, and framework conventions. FlowMarkup is a single YAML document that serves as specification, documentation, and executable artifact simultaneously. It separates orchestration from execution, enabling validation without running external services and testing with mocks in milliseconds.

Is FlowMarkup Turing-complete?

No, by design. FlowMarkup uses CEL (Common Expression Language) for expressions, which cannot perform unbounded loops, I/O, or side effects. This enables static analysis for termination guarantees, type correctness, and security auditing.

How mature is the specification?

Version 0.9.0 includes a 6,700+ line formal specification, 463 static analysis rules, a native testing framework, 27 validated example flows, and a JSON Schema (Draft 2020-12). The spec is licensed under OWFa 1.0 and designed for production use in business-critical workflows.

Can I migrate from Temporal or Step Functions?

FlowMarkup covers the same orchestration patterns — sequencing, branching, parallel execution, error handling, retry, saga transactions — but as a declarative YAML specification rather than imperative code. Most workflow patterns translate directly. The cross-language implementation guide covers Java, Go, Python, TypeScript, Rust, and C#.

Is FlowMarkup open source?

The specification is licensed under the Open Web Foundation Agreement 1.0 (OWFa 1.0), which grants a royalty-free, perpetual license to implement the specification. The complete spec, JSON Schema, and all 27 example flows are freely available for download.

Ready to define your first flow?

No signup. No dependencies. Open specification.