CLI · commit 98956bf

taskforge

Local DAG task runner with content-hash caching

Download taskforge.zip
Quickstart
unzip taskforge.zip && cd taskforge
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

taskforge

A local DAG task runner with content-hash caching: a Make/Airflow hybrid for data and build pipelines on one machine. Declare tasks with inputs and outputs in YAML; taskforge runs the graph in parallel, skips work whose inputs have not changed (by content hash, never by mtime), restores outputs from a cache when you flip back to a known state, retries flaky steps, and keeps a queryable run history in SQLite.

Features

Install

Requires Python 3.12+.

cd taskforge
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate        # or call .venv/bin/taskforge directly

Quickstart

cd examples/pipeline
taskforge run                    # first run: everything executes
taskforge run                    # second run: 13 cache hits
taskforge why report             # explain what would re-run, and why

A minimal taskforge.yaml:

tasks:
  build:
    cmd: gcc -o app main.c
    inputs: [main.c]
    outputs: [app]
  test:
    cmd: ./app --self-test
    deps: [build]
    retries: 2
    retry_delay: 1s

Usage

All commands accept -f FILE (default taskforge.yaml) and -p KEY=VALUE to override top-level params. The project root is the directory of the YAML file; all input/output paths are relative to it and state lives in .taskforge/.

taskforge run [TARGETS] [-j N] [--dry-run] [--force TASK] [--keep-going]
taskforge graph [TARGETS] [--format dot|mermaid|ascii]
taskforge list
taskforge clean [--cache]
taskforge logs TASK [--run ID]
taskforge history [--run ID] [--task NAME] [-n N]
taskforge stats
taskforge why TASK
taskforge watch [TARGETS] [--interval SECONDS]

TARGETS are task names or matrix base names (transform selects every instance); their dependencies are included automatically. Exit codes: 0 success, 1 task failure, 2 configuration error, 130 interrupted.

Walkthrough on the example pipeline

examples/pipeline simulates a download (one CSV per region), cleans it, converts per-region revenue with a Python task, aggregates, draws an SVG chart and renders an HTML report. fx_rates is deliberately flaky: it fails on its first attempt and is retried. Output below is captured from real runs (the live status board is replaced by plain lines when output is piped).

1. First run - every task executes; fx_rates is retried once:

$ taskforge run
taskforge run #1: 13 task(s)
success          fetch[region=eu]  21ms
success          fetch[region=in]  22ms
success          fetch[region=us]  22ms
retry            fx_rates: attempt 1 failed (exit code 3), retrying in 500ms
success          clean[region=eu]  19ms
...
success          fx_rates  566ms after 2 attempts
success          transform[region=us]  25ms
success          aggregate  18ms
success          chart  17ms
success          report  19ms
OK 13 success in 690ms

2. No-op re-run - everything is a cache hit:

$ taskforge run
taskforge run #2: 13 task(s)
cached (hit)     fx_rates  1ms
cached (hit)     fetch[region=us]  1ms
...
cached (hit)     report  0ms
OK 13 cached in 28ms

Touching a file without changing its content is also a hit: keys depend on content, not mtime.

3. Edit one input - only the dependent task runs:

$ sed -i 's|<h1>Revenue report</h1>|<h1>Revenue report v2</h1>|' templates/report.html
$ taskforge why report
report: would re-run, 1 component(s) changed since run #2
  changed  file:templates/report.html: 763cfbf2dc -> 6fb0558593
$ taskforge run
...
cached (hit)     chart  0ms
success          report  17ms
OK 1 success, 12 cached in 41ms

4. Partial re-run after a parameter change - why walks the upstream chain down to the root cause, --dry-run previews the plan:

$ taskforge why aggregate -p currency=EUR
aggregate: would re-run, 3 component(s) changed since run #3
  changed  upstream:transform[region=eu]: 5ee636db09 -> dfdeeb61ec
    transform[region=eu]: would re-run, 1 component(s) changed since run #3
      changed  param:currency: "USD" -> "EUR"
  ...

$ taskforge run --dry-run -p currency=EUR
Dry run (nothing executed)
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Task                 ┃ Action ┃ Why                         ┃ Key          ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ fetch[region=in]     │ cached │ up to date                  │ 9fd7f76e1c89 │
│ clean[region=us]     │ cached │ up to date                  │ d26e26247efe │
│ transform[region=in] │ run    │ inputs changed or never run │ a6c1c59b44f0 │
│ aggregate            │ run    │ inputs changed or never run │ d740fcc97911 │
│ report               │ run    │ inputs changed or never run │ b62b1376e8b3 │
└──────────────────────┴────────┴─────────────────────────────┴──────────────┘
6 to run, 7 cached

(rows abbreviated). Reverting to currency=USD afterwards costs nothing: the old keys are still in the cache, so outputs are restored without executing anything.

History and stats

$ taskforge history
Runs
┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ Run ┃ Started             ┃ Status  ┃ Duration ┃ Tasks              ┃
┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ 4   │ 2026-10-16 21:15:22 │ success │ 136ms    │ 7 cached 6 success │
│ 3   │ 2026-10-16 21:15:21 │ success │ 32ms     │ 13 cached          │
│ 1   │ 2026-10-16 21:15:20 │ success │ 694ms    │ 13 success         │
└─────┴─────────────────────┴─────────┴──────────┴────────────────────┘

$ taskforge stats          # (abbreviated)
│ Task      │ Executed │ Cache hits │ Hit rate │ p50   │ p95   │ Failures │ Flaky        │
│ aggregate │ 1        │ 2          │ 67%      │ 17ms  │ 17ms  │ 0        │ -            │
│ fx_rates  │ 1        │ 2          │ 67%      │ 550ms │ 550ms │ 0        │ yes (100%)   │
overall cache hit rate: 64% (25/39 task runs)
flaky tasks: fx_rates

taskforge logs fx_rates --run 1 prints the captured log, including the --- attempt 1/3 --- headers. When a task fails, its last 15 log lines are printed in the summary.

Graphs

taskforge graph --format mermaid prints Mermaid source (paste into any Markdown renderer), dot prints Graphviz, ascii prints dependency levels:

[0]
  fetch[region=in]
  fx_rates
[1]
  clean[region=in]      <- fetch[region=in]
[2]
  transform[region=in]  <- clean[region=in], fx_rates
...

Configuration

Summary below; the full reference is in docs/config-reference.md.

Key Meaning
params Values for {{ params.x }}, {param: x} inputs and when. Override with -p.
pools Capacity of resource pools. cpu defaults to the CPU count; sizes like 8G are parsed.
tasks.<name>.cmd / python Shell command, or module:function run in a subprocess (gets a ctx dict).
inputs Path/glob strings, {param: n}, {env: NAME}, {task: name}. Paths another task outputs create dependencies automatically.
outputs Files or directories, relative to the project root.
deps Extra dependencies; a matrix base name means all its instances.
retries, retry_delay, backoff Attempt n waits retry_delay * backoff^(n-1) before the next one.
timeout Per attempt; process group is killed (SIGTERM, then SIGKILL).
resources e.g. {cpu: 2, mem: 4G}, drawn from pools while the task runs.
matrix, when, always_run, priority, env, cwd See the reference.

Tasks see TASKFORGE_TASK, TASKFORGE_ATTEMPT (1-based) and TASKFORGE_ROOT; the interpreter taskforge runs under is first on PATH, so python in a cmd resolves to the same environment.

Architecture

src/taskforge/
  config.py    YAML loading, schema validation, matrix expansion, templating, `when`
  expr.py      safe AST evaluator for `when`
  graph.py     DAG construction, implicit edges from outputs, cycle/producer/duplicate checks
  hashing.py   sha256 file hashing with persistent stat cache
  plan.py      cache-key components, dry-run plan, `why` explanations
  cache.py     key-addressed output store, verify and restore
  engine.py    scheduler, worker pool, pools, retries, timeouts, process-group handling
  history.py   SQLite run history, stats queries, stat cache and result tables
  ui.py        rich live display and summary
  render.py    dot / mermaid / ascii graph output
  watch.py     polling watcher
  pyrunner.py  entry point for `python:` tasks
  cli.py       click commands

Design notes with worked examples: docs/caching-model.md, docs/scheduler-design.md, docs/config-reference.md.

Tests

.venv/bin/pytest -q

90 tests cover schema validation, matrix expansion, cycle/producer/duplicate detection, scheduling (dependency order, job limit, resource pools, critical-path and explicit priority using timestamps from instrumented tasks), cache-key stability, content-vs-mtime invalidation, output restore, retries and backoff, timeouts and process-group kill, keep-going vs fail-fast, SIGINT handling, history queries, why diffs, watch, and the CLI end to end against the example pipeline.

Limitations

License

MIT, see LICENSE.