Skip to content

Repository files navigation

RunFlow

RunFlow is a local workflow runner written in Rust. The CLI binary is named flow.

It currently supports:

  • validating YAML workflows;
  • registering and running jobs;
  • creating an isolated workspace per run;
  • capturing command stdout/stderr without shell redirection;
  • writing workflow and step log metadata;
  • writing run events as JSONL;
  • writing completion manifests for finished runs;
  • listing runs and reading run logs;
  • recording manual step actions;
  • pruning old run data with retention policies;
  • initializing and validating plugin manifests;
  • building and installing structured .flowpkg packages with workflow checksums.

Requirements

  • Windows PowerShell;
  • Git;
  • stable Rust with Cargo.

Check Rust:

rustc --version
cargo --version

Install From GitHub

git clone https://github.com/dandyArise/runflow.git
Set-Location .\runflow
cargo build --release

The binary is generated here:

.\target\release\flow.exe

Optional: add the release directory to your user PATH.

$runflowBin = (Resolve-Path .\target\release).Path
[Environment]::SetEnvironmentVariable(
  "Path",
  [Environment]::GetEnvironmentVariable("Path", "User") + ";$runflowBin",
  "User"
)

Open a new terminal, then verify the install:

flow version

Use Without Global Install

From the repository directory:

cargo run -- version

Pass options to the binary after --:

cargo run -- --root . version

Quick Start

Create workflow.yml:

name: demo
version: 1
schema_version: 1
steps:
  - name: hello
    type: command
    run:
      command: echo
      args: ["hello"]

Validate it:

flow validate .\workflow.yml

Register the job:

flow job add .\workflow.yml

List jobs:

flow job list

Run the job:

flow job run demo

The command prints a run_id.

List runs:

flow run list

Read JSONL logs for a run:

flow run logs <run_id>

Each completed run also writes:

.flow/runs/<run_id>/manifest.json
logs/<run_id>/workflow.metadata.json
logs/<run_id>/<step_name>/step.metadata.json
logs/<run_id>/<step_name>/stdout.log
logs/<run_id>/<step_name>/stderr.log

RunFlow captures stdout and stderr itself with Rust. Recommended workflows do not need cmd, powershell, bash, >, 2>&1, or pipes just to produce logs.

Workspace Root

By default, RunFlow uses the current directory as its root.

Use --root to store runtime data somewhere else:

flow --root C:\Temp\runflow-demo job add .\workflow.yml
flow --root C:\Temp\runflow-demo job run demo
flow --root C:\Temp\runflow-demo run list

RunFlow stores internal data in:

.flow/
  jobs/
  runs/
    <run_id>/manifest.json
  packages/
  plugins/
logs/
  <run_id>/workflow.metadata.json
  <run_id>/<step_name>/stdout.log
  <run_id>/<step_name>/stderr.log
  <run_id>/<step_name>/step.metadata.json

The .flow/ directory is ignored by Git except .flow/registry/plugins.json, which is generated and versioned as the local plugin contract.

CLI Commands

Version

flow version

Workflow

flow validate .\workflow.yml
flow migrate .\workflow.yml

validate also checks the optional workflow schedule cron expression. migrate currently validates the workflow and reports that no migration is required.

Schedules

RunFlow accepts cron expressions through the cron crate format:

sec min hour day-of-month month day-of-week year

The workflow schedule block is the real automation config used by the daemon. The flow schedule commands are preview helpers only; they do not run or enqueue anything.

Preview the next executions for an expression:

flow schedule next "0 */5 * * * * *"
flow schedule next "0 */5 * * * * *" --count 3
flow schedule next "0 */5 * * * * *" --from 2026-05-31T14:00:00Z

Preview the next executions from a workflow file containing schedule::

flow schedule workflow .\workflow.yml

Recommended workflow format for automatic daemon execution:

name: scheduled-job
schedule:
  cron: "0 */5 * * * * *"
  timezone: Europe/Paris
  enabled: true
steps:
  - name: hello
    type: command
    run:
      command: echo
      args: ["hello"]

The short format remains supported and is equivalent to UTC + enabled:

schedule: "0 */5 * * * * *"

No schedule means manual/direct execution only. You can also disable scheduling explicitly while keeping manual flow job run available:

schedule: false

or:

schedule:
  enabled: false

When flow daemon start is running, scheduled jobs are automatically enqueued when their next cron occurrence is due. The daemon stores schedule cursors in .flow/daemon/schedules/ so a job is not enqueued twice for the same occurrence.

Examples:

"0 0 * * * * *"      # every hour
"0 */10 * * * * *"   # every 10 minutes
"0 30 9 * * Mon *"   # every Monday at 09:30 UTC

Jobs

flow job add .\workflow.yml
flow job update .\workflow.yml
flow job delete <job_name>
flow job clear
flow job list
flow job show <job_name>
flow job run <job_name>

Runs

flow run list
flow run show <run_id>
flow run logs <run_id>
flow run summary <run_id>
flow run output <run_id> <step_name>
flow run output <run_id> <step_name> --stderr
flow run cancel <run_id>

run logs reads JSONL events. run summary reads logs/<run_id>/workflow.metadata.json. run output prints stdout.log or stderr.log for one step.

Steps

These commands record a manual action in the run event log.

flow step retry <run_id> <step_name>
flow step restart <run_id> <step_name>
flow step reset <run_id> <step_name>
flow step skip <run_id> <step_name>
flow step rerun-from <run_id> <step_name>

Job Test

flow test <job_name>
flow test <job_name> --verbose

Plugins

Initialize a plugin skeleton:

flow plugin init rust .\my-plugin
flow plugin init java .\my-plugin
flow plugin init python .\my-plugin
flow plugin init node .\my-plugin

Validate a plugin manifest:

flow plugin validate .\my-plugin\plugin\manifest.json

Inspect a manifest:

flow plugin inspect .\my-plugin\plugin\manifest.json

Validate plugin output JSON, or execute a plugin command with a PluginInput sample. Real execution uses an isolated .flow/plugin-tests/<uuid>/workspace directory and validates the command stdout as plugin output JSON.

flow plugin test "unused-command" .\plugin-output.json
flow plugin test "powershell -NoProfile -File .\plugin.ps1" .\plugin-input.json

Registry

The registry is the generated local plugin contract shared with validation, UI, MCP and runflow-agent.

flow registry scan
flow registry scan --check
flow registry list plugins
flow registry inspect <plugin_name>
flow registry export --for-agent

V1 scans plugins/*/plugin.yml and writes .flow/registry/plugins.json. The file is generated, committed, and checked in CI with flow registry scan --check. Empty registries are valid and contain plugins: [].

Plugin workflows keep the explicit syntax:

steps:
  - name: check
    type: plugin
    run:
      command: python
      args: ["plugins/my_plugin/run.py"]
    plugin_id: my_plugin
    input: {}

flow validate rejects plugin steps whose plugin_id is registered but whose command does not reference the registered entrypoint.

See docs/registry.md for the development workflow and contract.

Packages

Build a package from a workflow:

flow package build .\workflow.yml

Install a package:

flow package install .\.flow\packages\demo.flowpkg

.flowpkg files are JSON packages containing the package format version, job name, workflow versions, the workflow YAML, and a deterministic workflow checksum. Installing still accepts legacy .flowpkg files that contain only workflow YAML.

Daemon

flow daemon start
flow daemon status
flow daemon stop
flow daemon restart

When the daemon is running, flow job run <job_name> enqueues the run instead of executing it in the foreground. The daemon also scans registered jobs with schedule.enabled: true, enqueues them at the next due cron occurrence, exposes JSON status with heartbeat, active run and queue size, and supports clean stop/restart requests.

flow run cancel <run_id> cancels queued runs directly. For an active daemon run, it records a cancel marker, kills the tracked process tree, writes PROCESS_KILLED / PROCESS_TREE_KILLED, and the daemon finishes the run as CANCELLED.

Retention

flow retention run
flow retention run --dry-run --keep-runs 20
flow retention run --keep-runs 20 --older-than-days 30

Retention scans .flow/runs, keeps the newest runs according to --keep-runs, optionally limits deletion to runs older than --older-than-days, and removes matching run snapshots too. With no policy options, it only scans and reports 0 removals.

Minimal Workflow Format

The smallest valid draft workflow only needs name:

name: draft-job

RunFlow defaults missing fields to:

version: 1
schema_version: 1
steps: []

Draft workflows can be validated, added, listed and edited. They cannot be run, tested or packaged until they contain at least one step.

Runnable workflows need steps:

name: backup-db
version: 1
schema_version: 1
steps:
  - name: dump
    type: command
    run:
      command: echo
      args: ["backup"]

Preferred command steps use run.command plus a separate string per argument:

name: network-check
version: 1
schema_version: 1
steps:
  - name: ping_google
    type: command
    run:
      command: ping
      args: ["-n", "4", "google.com"]

Shells remain supported for advanced, platform-specific steps, but RunFlow still captures its own logs:

steps:
  - name: list_directory_windows
    type: command
    run:
      command: cmd
      args: ["/C", "dir"]

Shell steps are marked with is_shell: true and a portability warning in step.metadata.json.

Avoid this in recommended examples:

run:
  command: cmd
  args: ["/C", "ping -n 4 google.com > ping.log 2>&1"]

Step By Step: Ping With Logs

Create workflow.yml:

name: ping-cloudflare
version: 1
schema_version: 1

steps:
  - name: ping_1111
    type: command
    run:
      command: ping
      args: ["-n", "4", "1.1.1.1"]

Run it:

flow job add .\workflow.yml
$runId = flow job run ping-cloudflare

Read the generated logs through RunFlow:

flow run summary $runId
flow run output $runId ping_1111
flow run output $runId ping_1111 --stderr

Or read the files directly:

Get-Content ".\logs\$runId\workflow.metadata.json"
Get-Content ".\logs\$runId\ping_1111\step.metadata.json"
Get-Content ".\logs\$runId\ping_1111\stdout.log"
Get-Content ".\logs\$runId\ping_1111\stderr.log"

On Linux/macOS, use args: ["-c", "4", "1.1.1.1"] for ping.

Release

GitHub Releases are built from tags:

git tag v0.1.1
git push origin v0.1.1

The release workflow builds flow.exe on Windows and publishes:

flow-windows-x64.exe
flow-windows-x64.exe.sha256

Supported step types in the current engine:

  • command
  • sleep
  • wait_until
  • plugin

Example sleep step:

name: wait-demo
version: 1
schema_version: 1
steps:
  - name: pause
    type: sleep
    duration: 1s

Development

Format:

cargo fmt --all

Check formatting:

cargo fmt --all --check

Strict lint:

cargo clippy --all-targets --all-features -- -D warnings

Tests:

cargo test --all

Build:

cargo build --all

Release build:

cargo build --release

Current Status

RunFlow is under active development.

Working now:

  • complete S6 CLI;
  • schema validation;
  • JSONL event store;
  • run completion manifests;
  • workflow DAG;
  • local execution engine;
  • isolated workspaces;
  • snapshots;
  • SQLite projections;
  • retention cleanup for runs and snapshots;
  • structured .flowpkg packages;
  • daemon queue with real status/stop/restart;
  • process-tree cancellation for daemon runs;
  • plugin runtime with isolated command test.

Known limitations:

  • no distributed worker protocol yet;
  • plugin execution is local-command based;

About

RunFlow is a local workflow runner written in Rust.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages