# `avril:task` `avril:task` runs a task graph from guest code, with the same semantics as [`avril task`](config.md#tasks): dependencies first (concurrently where a task sets `parallel`), each task at most once per run, and result caching for tasks that declare `files`. Unlike `avril task`, the graph is a TypeScript value, so it can be computed — enumerated from the filesystem, derived from content, built by loops — and tasks can run functions in-process instead of spawning a program per task. ```ts import { runTasks } from "avril:task"; const report = await runTasks({ page: { files: ["content/page.md", "render.ts"], output: ["dist/page.html"], run: async (ctx) => { /* render the page */ }, }, assets: { command: ["avril", "bundle", "web/main.tsx", "--outdir", "dist"], files: ["web/**"], output: ["dist/main.js"], }, all: { dependencies: ["page", "assets"], parallel: true }, }, { target: "all" }); report.throwIfFailed(); ``` ## Tasks A task has exactly one action, or none (a pure grouping of dependencies): - `command: string` — run through the platform shell (`sh -c` / `cmd /C`), like an `avril.conf` task string. - `command: string[]` — spawn directly, without a shell. Arguments keep their boundaries; shell metacharacters are literal. - `run: (ctx) => void | number | Promise` — call a function in this process. It receives `{ name, root, args, env, signal }`; a returned number is its exit code, a throw is a failure with code 1. `dependencies` names other tasks in the graph, which run first; a failed dependency skips the dependent's action. `files`, `output`, `env`, and `envFile` behave as in `avril.conf`: declaring `files` opts into caching, keyed on the matched files, the named env values, the action, and every dependency's fingerprint. A task is skipped while its inputs are unchanged and its outputs are present and untouched. Cache entries live in avril's shared task cache, so the same graph hits the same entries whether it was run through `avril:task` or expressed in `avril.conf`. One caveat for function tasks: the fingerprint includes the function's source text, so editing its body invalidates it — but code it *imports* is not traced. List shared modules in `files` (as `render.ts` is above), or their edits will not invalidate cached outputs. The same is true of shell tasks, whose command string is fingerprinted in place of the program it runs. ## Running `runTasks(graph | (() => graph), options?)` accepts a graph or a thunk returning one (possibly asynchronously), and resolves with a report — `{ code, ok, completed, failed, cached, errors }` — even when tasks fail. Only graph errors (unknown tasks, cycles, unmatched `files` patterns) throw. `report.throwIfFailed()` throws a `TaskRunError` carrying the report, which is how an entry point turns a failed build into a nonzero exit. Options: `target` (default `"all"`), `root` (default the working directory), `args` (forwarded to the target only), `force` (ignore cache hits), `cacheDir`, `onEvent` (`started` / `cached` / `finished` / `error`), and `signal`. With `watch: true`, the run repeats when task inputs change: events are debounced over 50ms, the graph thunk is re-evaluated each cycle (so files created since the last cycle can appear as new tasks), and a changed graph definition bypasses the cache for that cycle, mirroring how `avril task --watch` treats config edits. A failed cycle is reported like any other and watching continues; aborting `signal` ends the watch and resolves with the last report. Editing the module that *defines* the graph is not picked up — its imports stay cached in this process — so restart the program after editing the build definition itself.