Process definitions that are contracts, not programs.
A declarative YAML format for workflow orchestration — 25 directives, 463 validation rules, zero runtime surprises.
No signup. No paywall. No vendor lock-in.
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
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:
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
Three layers of validation catch errors before execution
Every flow passes through structural, semantic, and behavioral verification at authoring time — not at runtime.
JSON Schema
Structural conformance. Validates directive names, required fields, value types, and nesting rules against the Draft 2020-12 schema.
Static Analysis
Semantic correctness. 463 rules verify type safety, variable scope, expression validity, security constraints, and contract compliance.
Testing Framework
Behavioral verification. Mocks, fixtures, assertions, and fault injection exercise your flows against scenarios — in milliseconds.
flowmarkup:
do:
- call:
service: claude
params:
prompt: =user_input # never defined
result:
ai_output: =RESULT.text
- log: "Result: {{ result }}" # wrong name
Validation Output
Variable
user_input referenced in params.prompt is not declared in any reachable scope.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 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.
# 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}
Stream data mid-execution with yield. Callers subscribe with onYield or collect results via $yields.
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}
Typed error handling with try/catch. Map specific error types to specific recovery strategies.
- 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}}
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.
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}
Cross-workflow coordination with emit/waitFor. Filter events by condition, set timeouts, capture event data.
# 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}
Execute commands on remote servers via ssh:. Parse stdout as JSON, inject secrets as env vars, orchestrate multi-server deployments.
# 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
Unified storage: directive for S3 and SFTP. Get, put, list, check existence, and stream cross-storage transfers — all with parseAs auto-decoding.
# 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
Parse raw JSON strings with parse(JSON), auto-decode from storage/SSH with parseAs: JSON, filter and transform structured data in CEL.
# 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}
All 27 example flows
Getting Started 2
Actions 7
Error Handling 1
Parallelism and DAGs 2
Streaming 1
Resilience 3
Security and Sub-flows 4
Events and Triggers 3
Data and Functions 2
Advanced 4
Get started in 3 steps
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}}!"
Validate
Open the Inspector, paste your flow, see results instantly. JSON Schema + 463 SA rules + testing.
Open Inspector →Run
Implement an engine in your language. The cross-language guide covers Java, Go, Python, TypeScript, Rust, and C#.
Cross-Language GuideSpecification documents
Language Specification
Complete format definition: directives, actions, types, expressions, contracts
244 KBValidation Reference
All 463 static analysis rules with severity and examples
213 KBTesting Framework
Mocks, fixtures, assertions, fault injection, snapshot testing
138 KBEngine Implementation
Building a FlowMarkup engine
Cross-Language Guide
Java, Go, Python, TS, Rust, C#
Secrets Management
Opaque secrets, pluggable backends
Error Catalog
50+ standard error types
Examples Guide
Patterns and quick-reference
Changelog
Version history
JSON Schema
Draft 2020-12
Claude AI Skill
FlowMarkup-aware authoring
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.