Skip to content

Composition

FlowState supports composing complex workflows from simpler building blocks through sub-flows, parallel execution, and inheritance.

Sub-flows allow a workflow to call another workflow inline, enabling modular and reusable workflow components.

main-workflow:
tool: sub-flow
arguments:
workflow: helper-workflow
next: continue-main

The sub-flow executes to completion before the parent continues.

Pass variables to customize sub-flow behavior:

process-file:
tool: sub-flow
arguments:
workflow: file-processor
variables:
input_path: "{{ source_file }}"
output_format: json
next: use-result

Passed variables override the sub-flow’s default values.

Variables set by the sub-flow are namespaced:

# Sub-flow (helper-workflow) sets: result = "success"
# Parent accesses as: helper-workflow::result
check-result:
tool: bash
arguments:
command: 'echo "Sub-flow result: {{ helper-workflow::result }}"'

During sub-flow execution, the current state is tracked as parent-state/subflow-state:

main-workflow → process-file/validate-input
process-file/transform-data
process-file/write-output
→ continue-main

This enables accurate pause/resume within nested workflows.

Sub-flows are resolved from:

  1. The same directory as the parent workflow
  2. The project .flowstate/ directory
  3. The user ~/.flowstate/ directory

When creating an instance, FlowState copies referenced sub-flows into the instance’s workflow/ directory.

The parallel tool runs multiple tasks concurrently:

run-tests:
tool: parallel
arguments:
tasks:
- tool: bash
arguments:
command: npm test -- --shard=1/3
output: var(shard1)
- tool: bash
arguments:
command: npm test -- --shard=2/3
output: var(shard2)
- tool: bash
arguments:
command: npm test -- --shard=3/3
output: var(shard3)
next: merge-results

Each task in the tasks array mirrors a regular state:

tasks:
- tool: <tool-name>
arguments:
<arg>: <value>
output: var(<name>) | file(<path>)

Each task can capture output independently:

gather-data:
tool: parallel
arguments:
tasks:
- tool: bash
arguments:
command: curl https://api1.example.com/data
output: var(api1_data)
- tool: bash
arguments:
command: curl https://api2.example.com/data
output: file(./api2_data.json)

If any parallel task fails, the parallel tool returns the first non-zero exit code. Use on-error to handle failures:

run-parallel:
tool: parallel
arguments:
tasks:
- tool: bash
arguments:
command: ./task1.sh
- tool: bash
arguments:
command: ./task2.sh
on-error:
_: handle-parallel-failure
next: all-succeeded

Successful tasks complete even if others fail.

Interactive tools cannot run in parallel:

  • claude-code (requires terminal)
  • ask-user (requires user input)

These must be executed sequentially in regular states.

Workflows can extend other workflows using the extends field:

name: code-review
extends: new-feature
# Override or add states
states:
additional-step:
tool: bash
arguments:
command: echo "Added by code-review"

The child workflow inherits:

  • All states from the parent
  • Variable defaults (child can override)
  • The start-at value (unless overridden)

Child states override parent states with the same name.

Reference parent workflows by:

# Workflow name (discovered automatically)
extends: base-workflow
# Relative path from workflow directory
extends: ./parent.yaml
# Project root path
extends: @/workflows/base/index.yaml

Standardized pipelines: Define a base CI workflow, extend for specific projects.

base-ci.yaml
name: base-ci
start-at: checkout
states:
checkout:
tool: bash
arguments:
command: git checkout {{ branch }}
next: build
build:
tool: bash
arguments:
command: echo "Override me"
node-ci.yaml
name: node-ci
extends: base-ci
states:
build:
tool: bash
arguments:
command: npm install && npm run build
next: test
test:
tool: bash
arguments:
command: npm test

The pause tool halts workflow execution until manually resumed:

await-approval:
tool: pause
arguments:
reason: "Waiting for deployment approval"
next: deploy

Specify a condition that must be met before resuming:

wait-for-build:
tool: pause
arguments:
reason: "Waiting for CI build to complete"
condition:
tool: bash
command: '[ -f ./build-complete.flag ]'
next: deploy

On resume, FlowState checks the condition:

  • If satisfied (exit 0), continues to next
  • If not satisfied, remains paused
Terminal window
fs list --paused

Shows all paused instances with their pause reason and timestamp.

Terminal window
# Resume specific instance
fs resume my-instance
# Resume all paused instances
fs resume --all
# Resume all paused instances for a workflow
fs resume --workflow my-workflow
# Force resume (skip condition check)
fs resume my-instance --force
start-at: build
states:
build:
tool: bash
arguments:
command: make build
next: test
test:
tool: bash
arguments:
command: make test
next: await-approval
await-approval:
tool: pause
arguments:
reason: "Ready for production deployment"
next: deploy
deploy:
tool: bash
arguments:
command: make deploy-prod
start-at: prepare
states:
prepare:
tool: bash
arguments:
command: echo "Starting parallel processing"
next: process-parallel
process-parallel:
tool: parallel
arguments:
tasks:
- tool: sub-flow
arguments:
workflow: process-shard
variables:
shard: "1"
- tool: sub-flow
arguments:
workflow: process-shard
variables:
shard: "2"
- tool: sub-flow
arguments:
workflow: process-shard
variables:
shard: "3"
next: aggregate
aggregate:
tool: bash
arguments:
command: ./merge-shards.sh