Skip to content

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 depends before 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), and wait_for (soft — only rides along if something else already scheduled the task) are three distinct relationships, not one.
  • A task with both sources and outputs declared 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 testlint 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 = true keeps a task out of mise tasks and mise run autocomplete — for tasks meant to be called only as someone else's dependency, not run directly by a person.
  • confirm adds 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_post as "runs before." Fix: it's the opposite — a hard postrequisite that always runs after this task finishes, success or fail. Don't confuse it with depends (before).
  • Expecting wait_for to trigger a task. Fix: it's soft ordering only — it affects a task that's already scheduled for another reason, but naming something in wait_for alone never puts it in the graph; only a depends edge does that.
  • Expecting sources/outputs skip-if-unchanged behavior without declaring both. Fix: only tasks that declare sources and outputs get 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 complete depends/depends_post/wait_for reference — 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.toml mise 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.toml file'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; switching MISE_ENV never 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 adds depends_post (a hard postrequisite) and wait_for (soft ordering that only applies if something else already triggered the task) — two relationships neither of those tools' dependency systems distinguishes.