Cross-platform CLI runner for the duckflux workflow DSL β a minimal, deterministic, runtime-agnostic language for orchestrating workflows through declarative YAML.
Define what happens and in what order. The runner decides how.
participants:
build:
type: exec
run: npm run build
test:
type: exec
run: npm test
flow:
- build
- testduckflux run ci.flow.yaml- Go 1.24+
git clone https://github.com/duckflux/runner.git
cd runner
make buildThe binary is produced at bin/duckflux. Add it to your PATH or run it directly:
./bin/duckflux versionGOOS=linux GOARCH=amd64 go build -o duckflux-linux ./cmd/duckflux
GOOS=darwin GOARCH=arm64 go build -o duckflux-macos ./cmd/duckflux
GOOS=windows GOARCH=amd64 go build -o duckflux.exe ./cmd/duckfluxCreate a file called hello.flow.yaml:
id: hello
name: Hello World
version: "1"
participants:
greet:
type: exec
run: echo "Hello, duckflux!"
flow:
- greetRun it:
duckflux run hello.flow.yamlOutput:
Hello, duckflux!
Parse, validate, and execute a workflow.
duckflux run <file.flow.yaml> [flags]| Flag | Description |
|---|---|
--input key=value |
Pass an input value (repeatable) |
--input-file path.json |
Load inputs from a JSON file |
--cwd path |
Base working directory for exec participants |
--verbose |
Enable debug logging |
--quiet |
Suppress all output except errors |
Input resolution priority (highest wins): --input flags > --input-file > stdin (piped JSON).
# With inline inputs
duckflux run deploy.flow.yaml --input branch=main --input env=staging
# With a JSON file
duckflux run deploy.flow.yaml --input-file inputs.json
# With piped JSON via stdin
echo '{"branch": "main"}' | duckflux run deploy.flow.yamlParse and validate a workflow without executing it. Checks JSON Schema conformance and semantic correctness (cross-references, reserved names, CEL expression syntax).
duckflux lint <file.flow.yaml>Exits 0 and prints OK on success, exits 1 with errors otherwise.
Everything lint does, plus validates provided inputs against the workflow's declared input schema (required fields, types, formats).
duckflux validate <file.flow.yaml> [flags]| Flag | Description |
|---|---|
--input key=value |
Input value to validate (repeatable) |
--input-file path.json |
JSON file with input values to validate |
duckflux validate deploy.flow.yaml --input branch=main --input max_retries=3Print the current version.
duckflux versionFor the full specification, see the duckflux spec.
Participants are named steps that can be referenced in the flow. Each has a type:
| Type | Description | Status |
|---|---|---|
exec |
Shell command execution | β Implemented |
http |
HTTP request | β Implemented |
workflow |
Sub-workflow composition | β Implemented |
mcp |
MCP server delegation (tool field replaces operation) |
π Stub (v2) |
emit |
Publish an event to the event hub (v1: logged/stubbed) | π Stub (v1) |
| Construct | Description |
|---|---|
| Sequential | Steps run top-to-bottom |
loop |
Repeat steps until a condition is met or N times |
parallel |
Run steps concurrently |
if/then/else |
Conditional branching |
when |
Guard condition on a single step |
Note (spec v0.2):
- The
waitconstruct is available to pause execution until an event, a timeout, or a polling condition is met. - Inline participants are supported: a
flowstep can contain an inline participant definition instead of referencing the top-levelparticipants:map. loopsupports theasfield to rename the loop context (for exampleas: attemptexposesattempt.index). The runner rewrites the context for CEL expressions.ifis now an object with aconditionfield:if: { condition: "expr", then: [...], else: [...] }.
Configurable per participant or per flow step invocation (flow overrides participant):
onError |
Behavior |
|---|---|
fail |
Stop the workflow (default) |
skip |
Mark as skipped, continue |
retry |
Re-execute with backoff |
<participant> |
Redirect to a fallback participant |
Resolution chain: flow override > participant > defaults > none.
exec commands run with this precedence:
participant.cwd > defaults.cwd > --cwd > current process cwd.
All conditions and input mappings use Google CEL. Expressions are type-checked at parse time and sandboxed (no I/O, no side effects).
Example workflows are in the examples/ directory.
A single-step workflow (examples/minimal.flow.yaml):
id: minimal
name: Minimal Workflow
version: "1"
participants:
greet:
type: exec
run: echo "Hello, duckflux!"
flow:
- greetduckflux run examples/minimal.flow.yamlFixed iteration loop (examples/loop.flow.yaml):
id: loop-example
name: Loop Workflow
version: "1"
inputs:
max_rounds:
type: integer
default: 3
description: "Maximum number of loop iterations"
participants:
counter:
type: exec
run: echo "running iteration"
timeout: 5s
check:
type: exec
run: echo "checking progress"
timeout: 5s
flow:
- loop:
max: 3
steps:
- counter
- checkduckflux run examples/loop.flow.yamlConcurrent execution with a sequential follow-up (examples/parallel.flow.yaml):
id: parallel-example
name: Parallel Workflow
version: "1"
participants:
lint:
type: exec
run: echo "linting..."
timeout: 30s
test:
type: exec
run: echo "testing..."
timeout: 30s
build:
type: exec
run: echo "building..."
timeout: 30s
report:
type: exec
run: echo "all checks done"
flow:
- parallel:
- lint
- test
- build
- reportduckflux run examples/parallel.flow.yamlInline participants allow defining a participant directly inside the flow without adding it to the top-level participants map:
flow:
- myInlineStep:
type: exec
run: echo "inline participant"
# This runs the inline exec once and doesn't require a named participant entryThe wait step supports simple timeouts, event matching (stubbed in v1), and polling. Examples:
- wait:
timeout: 30s
# event-based (stubbed in v1):
- wait:
event: my-event
match: "payload.type == 'ready'"A full pipeline with loops, conditionals, parallel steps, error handling, and output mapping (examples/code-review.flow.yaml):
id: code-review
name: Code Review Pipeline
version: "1"
defaults:
timeout: 5m
inputs:
branch:
type: string
default: "main"
description: "Branch to review"
max_rounds:
type: integer
default: 3
description: "Maximum review iterations"
participants:
coder:
type: exec
run: echo '{"status":"coded","branch":"'"${BRANCH:-main}"'"}'
timeout: 30s
onError: retry
retry:
max: 2
backoff: 1s
reviewer:
type: exec
run: |
echo '{"approved":true,"score":8,"comments":"Looks good"}'
timeout: 30s
onError: fail
tests:
type: exec
run: echo "tests passed"
timeout: 30s
onError: skip
lint:
type: exec
run: echo "lint passed"
timeout: 30s
onError: skip
notify_success:
type: http
url: http://localhost:0
method: POST
timeout: 10s
onError: skip
notify_failure:
type: http
url: http://localhost:0
method: POST
timeout: 10s
onError: skip
flow:
- coder
- loop:
until: reviewer.output.approved == true
max: 3
steps:
- reviewer
- coder:
when: reviewer.output.approved == false
- parallel:
- tests
- lint
- if: 'tests.status == "success" && lint.status == "success"'
then:
- notify_success
else:
- notify_failure
output:
approved: reviewer.output.approved
score: reviewer.output.score
testResult: tests.status
lintResult: lint.statusduckflux run examples/code-review.flow.yaml --input branch=developAdd the JSON Schema to your VS Code settings for autocomplete and validation in .flow.yaml files:
{
"yaml.schemas": {
"./schema/duckflux.schema.json": "*.flow.yaml"
}
}Requires the YAML extension.
- duckflux spec β Full DSL specification
docs/MOTIVATION.mdβ Why Go was chosen as the runner stackdocs/IMPLEMENTATION_PLAN.mdβ Architecture and implementation phasesdocs/HISTORY.mdβ Past decisions and changelog