Skip to main content

How fyrer Executes Tasks: Scheduling and Concurrency

fyrer is a task runner, not a build system. It orchestrates commands you define, in the order their dependencies require, running as much as possible in parallel. It doesn't know anything about compilers or frameworks — it just reads your fyrer.yml, figures out the correct order, and runs each command with full log streaming.

Execution lifecycle

1
Parse config

fyrer reads fyrer.yml from the current directory (or the path given to --config) and validates it: version check, unique package and task names, valid paths, valid glob patterns, and mutually exclusive flags like cache + persistent.

2
Resolve graph

Every depends_on declaration is resolved into edges of a directed acyclic graph (DAG). Each node in the graph is a fully-qualified package:task identifier. fyrer validates that no cycles exist and reports an error at startup if one is found.

3
Schedule

Tasks are scheduled in streaming DAG order: a task becomes runnable as soon as all of its dependencies have succeeded (or were cached). The engine keeps a ready queue and spawns tasks up to the concurrency limit (default: CPU count). There are no level barriers — unrelated branches run concurrently even while other branches are still busy.

4
Cache check

For tasks with cache: true, fyrer computes a blake3 hash of the task's ID, command, working directory, resolved environment, and matched input file contents. Outputs are also digested (blake3 of outputs) to decide if hydration is needed. If a usable cache entry exists, the task is skipped and reported as ⚡ Cached; declared outputs are restored from the tar.zst archive under .fyrer/cache/ before the skip.

5
Output streaming

For tasks that are not cached, their stdout and stderr are streamed in real time as the command runs. In plain mode (-n), every line is prefixed with the package:task label and colorized so interleaved output from concurrent tasks remains easy to read. The default TUI mode routes logs into a per-task pane you can browse interactively.

6
Completion

After all tasks have run, a summary is shown with counts for successful, failed, cached, and skipped tasks, plus the total wall-clock duration.

Concurrency model

fyrer runs a streaming DAG: each task starts as soon as its own dependencies have succeeded or were cached, up to the concurrency limit (set concurrency in fyrer.yml or defaults to available parallelism). There are no level barriers — if ui:build finishes before api:build, then web:build (which depends only on ui:build) starts immediately even while api:build is still running.

fyrer plan still prints tasks grouped into levels for readability, but execution does not wait for levels to drain.

You can cap parallelism with the top-level concurrency field in fyrer.yml:

concurrency: 4

Failure propagation

If a task exits with a non-zero status, it is marked as failed. Any task that declares a (transitive) dependency on the failed task is skipped — not failed itself. This distinction matters in the summary: failed tasks are those that actually crashed; skipped tasks are those that couldn't run because something upstream went wrong.

Persistent tasks

Tasks marked persistent: true are intended for long-running processes like development servers. They never exit on their own, so any task that transitively depends on a persistent task will never start — it stays Skipped with UpstreamFailed. Unrelated tasks (those not dependent on the persistent task) still run concurrently.

Keep persistent tasks on the leaves of your graph (no other tasks should depend on them). A typical pattern is to build everything first, then start dev servers that depend only on their own build outputs.

Watch mode

Tasks with watch: true are polled every 300ms for changes to their inputs globs (debounced 300ms). When inputs change, the task is restarted automatically; its already-succeeded dependents are marked Stale ( in the TUI) but not re-run. In the TUI you can also restart the selected task manually with r or kill it with K.

watch cannot be combined with cache.

Plain mode

Pass -n / --no-tui to fyrer run to disable the interactive TUI and get prefixed, colorized log output instead:

fyrer run build -n

Plain mode is ideal for CI environments and log aggregators, where a full-screen TUI would produce garbled output. Each log line is prefixed with the originating package:task so the source is always clear even when multiple tasks are running at the same time.