Tasks¶
You run mise run deploy — one task, no ::: in sight — and the terminal shows lint and
unit-test output interleaved before test even starts. Nothing raced by accident: mise built the
full dependency graph before running anything, saw two branches with no path between them, and ran
both at once on its own. course.md §9 shows how to define a task (inline in mise.toml, or as a
file under mise-tasks/) and run it; this page goes one level deeper — how mise turns depends into
an actual dependency graph, what runs in parallel versus in sequence, and the task-level knobs
(caching, visibility, safety) that don't fit in a quick intro.
TL;DR — in 30 seconds:
- mise builds the full dependency graph from every task's
dependsbefore running anything — any two independent branches run concurrently on their own;:::is one way to ask for parallelism, not the only way it happens. depends(before),depends_post(after, always — even on failure), andwait_for(soft — only rides along if something else already scheduled the task) are three distinct relationships, not one.- A task with both
sourcesandoutputsdeclared skips its run when nothing's changed; a dependency shared by several tasks still runs exactly once — mise dedupes by task name across the whole graph.
1. A task is a graph node, not a line in a list¶
Every task can declare who has to run first, and — less obviously — who should run after it, and
who should run only if already scheduled. mise reads every task's depends (and the two variants
below) across the whole config and builds a directed acyclic graph before it runs anything:
| Field | Meaning | Effect on scheduling |
|---|---|---|
depends |
"Run these first" | Hard prerequisite — always runs, before this task starts |
depends_post |
"Run these after, once I'm done" | Hard postrequisite — always runs, after this task finishes |
wait_for |
"If this is already going to run, run it first — but don't trigger it" | Soft ordering — only affects tasks already in the graph for another reason |
depends is what course.md §9's test → lint example uses — the same mechanism that built the
lint/unit-test/deploy graph from the top of this page. depends_post and wait_for matter once
a task list grows past two or three entries and you need to say "cleanup always runs last" or "if
someone else already asked for the DB migration, run it before me — but I'm not the one who should
trigger it."
2. The graph, not the task list, decides what's parallel¶
mise run lint ::: test (course.md §9) asks for two tasks side by side. But even a single
mise run deploy can fan out internally: once mise has the full graph, any two nodes with no path
between them are independent, and mise runs independent branches concurrently on its own — you don't
have to spell out ::: between every pair.
flowchart LR
L["lint"] --> T["test"]
U["unit-test"] --> T
T --> D["deploy"]
D --> C["cleanup<br/>(depends_post)"]
M["migrate<br/>(wait_for: test)"] -.soft order.-> T
Reading the diagram: lint and unit-test share no dependency on each other, so mise runs them
together; both must finish before test starts; deploy waits on test; cleanup is wired as
deploy's depends_post, so it always runs once deploy finishes, success or fail; migrate only
takes the dotted "run before test" ordering if something else already put migrate in the graph —
naming it in a depends elsewhere is what actually triggers it.
A dependency that appears more than once in the graph — two tasks both depending on lint — still
runs exactly once. mise dedupes by task name across the whole resolved graph.
3. Skip work that's already done: sources and outputs¶
A task can declare what it reads and what it produces. If the declared sources haven't changed
since the declared outputs were last written, mise skips the run — the same idea as a Makefile
target with dependencies:
[tasks.build]
description = "Compile the service binary"
run = "go build -o dist/server ./cmd/server"
sources = ["**/*.go", "go.mod", "go.sum"]
outputs = ["dist/server"]
Re-running mise run build with no source changes is a no-op; touch any matched file and it
rebuilds. This is what makes it safe to put a build task as a depends of several other tasks — the
first caller pays the cost, everyone after it gets the cached result for that invocation.
4. Two more knobs worth knowing before you scale up a task list¶
hide = truekeeps a task out ofmise tasksandmise runautocomplete — for tasks meant to be called only as someone else's dependency, not run directly by a person.confirmadds an interactive "are you sure?" prompt before the task body runs — the guard rail for anything destructive (a prod deploy, a DB drop) that you still want reachable by name.
Both are metadata on the task, alongside description, depends, and run — no separate
configuration surface to learn.
5. Where tasks live in a monorepo¶
course.md's per-directory [env] layering (§8) applies to tasks too: every mise.toml mise merges
on the walk-up can contribute its own [tasks], and file tasks are discovered from mise-tasks/
relative to whichever config defined them. Running mise run <name> from a subdirectory resolves
against the merged task set for that directory — a repo-root ci task and a service-level test
task coexist without either file needing to know about the other.
Common gotchas
- Assuming
:::is the only way to get parallel tasks. Fix: it isn't — once mise has built the full dependency graph, it runs any two nodes with no path between them concurrently on its own;:::just asks for two tasks side by side explicitly. - Reading
depends_postas "runs before." Fix: it's the opposite — a hard postrequisite that always runs after this task finishes, success or fail. Don't confuse it withdepends(before). - Expecting
wait_forto trigger a task. Fix: it's soft ordering only — it affects a task that's already scheduled for another reason, but naming something inwait_foralone never puts it in the graph; only adependsedge does that. - Expecting
sources/outputsskip-if-unchanged behavior without declaring both. Fix: only tasks that declaresourcesandoutputsget the no-op-when-nothing-changed treatment; a task without them re-runs its full body every invocation. - Worrying that two tasks depending on the same task will run it twice. Fix: mise dedupes by task name across the whole resolved graph — a shared dependency still runs exactly once.
Check yourself — you run mise run deploy and the terminal shows lint and unit-test interleaving before test even starts, even though you never typed :::. Why?
Because mise reads every task's depends across the config and builds the full dependency graph
before running anything. lint and unit-test have no path between them, so mise runs independent
branches concurrently on its own — ::: is one way to request parallelism, not the only way it happens.
Check yourself — cleanup is wired as deploy's depends_post. deploy's script exits non-zero. Does cleanup still run?
Yes. depends_post is a hard postrequisite: it always runs after the task it's attached to finishes,
regardless of whether that task succeeded or failed — unlike depends, which is a prerequisite that
runs before.
You can defend this when you can read a depends / depends_post / wait_for mix and say, in
order, what runs before what, what runs after regardless of outcome, and what only rides along if
something else already scheduled it; and you can explain why adding sources/outputs to a build
task makes it safe to depend on from several places without repeating the work.
Go deeper: the full task system — file tasks,
usage-based CLI argument parsing, remote/git tasks, and the completedepends/depends_post/wait_forreference — is documented in the mise tasks docs; this page covers the 80/20 you need for the Do exercise.
6. How this connects¶
- §5's monorepo merge is the same directory walk-up that Config hierarchy & precedence (this course's
first page, §2) already names reciprocally — that page's rule ("every
mise.tomlmise merges on the walk-up can contribute") isn't specific to[env]; this page is where it plays out for[tasks]. - This course's next page, Environments & backends (§2), shows a
mise.production.tomlfile's[tasks]table following the identical override pattern as its[env]table — a named profile can expose a different task set entirely, layered on top of (not instead of) the graph this page describes; switchingMISE_ENVnever changes the merge rule, only which tasks are in scope for it. - Terraform's dependency graph (module 07, Terraform, "Providers &
lifecycle" §5) and Helmfile's release DAG (module 06, Helmfile, "The
release DAG" §1) both build a DAG before running anything, but neither has this page's
three-way split: Terraform's edges are mostly implicit (inferred from an attribute reference) and
Helmfile's are 100% explicit
needs:lines, while mise addsdepends_post(a hard postrequisite) andwait_for(soft ordering that only applies if something else already triggered the task) — two relationships neither of those tools' dependency systems distinguishes.