The Task Runner
Your Makefile shells out to the wrong node. Your npm scripts can’t depend on
a Go build step. just is nice but it’s a fifth tool to install. mise already
manages your toolchain and your env — so it runs your chores too, with the right
tools on PATH, real dependencies, parallelism, and incremental builds.
mise run build # run a task (recommended spelling)mise r build # short aliasmise run # runs the task named "default"mise tasks # list every task it can seeTOML tasks
Section titled “TOML tasks”Define tasks inline in mise.toml. A one-liner is just run:
[tasks.build]run = "go build -o bin/app ./..."description = "Compile the server"
[tasks.lint]run = ["ruff check .", "ruff format --check ."] # array = sequential stepsThe full set of keys (use these verbatim — don’t invent others):
[tasks.deploy]description = "Ship to staging"run = "./scripts/deploy.sh"run_windows = "pwsh ./scripts/deploy.ps1" # platform overridealias = "d" # SINGULAR (string or array)depends = ["build", "test"] # run first, in paralleldepends_post = ["notify"] # run after this task succeedswait_for = ["migrate"] # soft: run after IF also scheduleddir = "{{cwd}}" # default = config rootenv = { TARGET = "staging" }tools = { node = "22" } # per-task tool installconfirm = "Deploy to staging?" # prompt before running| Key | What it does |
|---|---|
run / run_windows | command string, or array = sequential steps |
depends | hard deps — run (in parallel) before this task |
depends_post | run after this task finishes successfully |
wait_for | soft ordering — only waits if the dep is also being run; never triggers it |
dir | working dir (default: the config’s root; "{{cwd}}" = where you invoked from) |
env | extra env vars for this task |
alias | short name(s) — singular key, string or array |
description | shown in mise tasks and completions |
confirm | y/n prompt string before running (great for destructive tasks) |
tools | tools to install/expose just for this task |
File tasks
Section titled “File tasks”For anything longer than a line, write a real script. Drop executables (chmod +x)
into mise-tasks/ (or .mise-tasks/, mise/tasks/, .config/mise/tasks/).
Subdirectories namespace with : — mise-tasks/test/units becomes the task test:units.
Directorymise-tasks/
- build
Directorytest/
- units
- e2e
Metadata lives in #MISE directive comments; arguments in #USAGE:
#!/usr/bin/env bash#MISE description="Build the release binary"#MISE depends=["lint"]#MISE sources=["src/**/*.go", "go.mod"]#MISE outputs=["bin/app"]#USAGE flag "-r --release" help="Optimized build"#USAGE arg "<target>" default="./..."
set -euo pipefailgo build -o bin/app "$usage_target" # see "Arguments" belowA few language portability notes:
# [MISE] description="…" # formatter-safe form — survives shfmt/black reformatting//MISE description="…" # for languages whose comments aren't '#' (JS, Go, Rust…)//USAGE arg "<file>" # likewise for usage directives in non-bash scripts#!/usr/bin/env python#MISE description="Seed the dev database"import osprint("seeding", os.environ["DATABASE_URL"])#!/usr/bin/env node//MISE description="Generate API client"//USAGE flag "--watch" help="Regenerate on change"console.log("generating client…");Dependencies & the DAG
Section titled “Dependencies & the DAG”depends builds a directed acyclic graph. mise runs independent branches in
parallel and only serializes where a real dependency exists.
[tasks.build] # leaf — runs firstrun = "go build ./..."
[tasks.lint]run = "golangci-lint run"depends = ["build"]
[tasks.test]run = "go test ./..."depends = ["build"]
[tasks.ci]run = "echo all green"depends = ["lint", "test"] # lint + test run in parallel; both wait on buildflowchart TD build["build"] lint["lint"] test["test"] ci["ci"] build --> lint build --> test lint --> ci test --> ci classDef parallel fill:#0d3b24,stroke:#2ecc71,color:#d6ffe6; class lint,test parallel
mise run ci runs build once, then lint and test together (highlighted),
then ci. Inspect the tree without running anything:
mise tasks deps # show the dependency treemise tasks deps ci # just the subgraph for one taskRunning tasks
Section titled “Running tasks”mise run build # one taskmise run build ::: test # several tasks; ::: separates, each gets own argsmise run test -- --verbose -k auth # everything after -- is passed throughmise run "test:*" # glob over task namesmise run "{build,lint}" # brace expansionParallelism and control:
mise run ci --jobs 8 # up to 8 tasks at once (default 4; or MISE_JOBS)mise run ci --jobs 1 # serialize — interleaves output deterministicallymise run ci --interleave # stream child output as it arrivesmise run deploy --skip-deps # run just this task, skip its dependsmise tasks # list (add --hidden to include hide=true tasks)Incremental builds with sources / outputs
Section titled “Incremental builds with sources / outputs”Declare a task’s inputs and outputs and mise skips it when the outputs are already
newer than every source — same idea as make, but cross-language and lockstep with
your toolchain.
[tasks.build]run = "esbuild src/index.ts --bundle --outfile=dist/app.js"sources = ["src/**/*.ts", "package.json"]outputs = ["dist/app.js"]
[tasks.bundle-css]run = "tailwindcss -i src/app.css -o dist/app.css"sources = ["src/**/*.css", "tailwind.config.*", "!src/_scratch/**"] # ! excludesoutputs = { auto = true } # checksum every output dir automaticallyThe rule: skip when the oldest output is newer than the newest source. Use
outputs = { auto = true } to track outputs by checksum instead of naming files —
ideal when a build emits a whole directory.
Arguments: the usage spec
Section titled “Arguments: the usage spec”Tasks take typed args via the usage spec. In TOML, set
usage; in file tasks, use #USAGE directives. Parsed values land in
$usage_<name> environment variables.
[tasks.greet]usage = '''arg "<name>" help="Who to greet"flag "-l --loud" help="Shout it"'''run = '''msg="hello $usage_name"[ "$usage_loud" = "true" ] && msg=$(echo "$msg" | tr a-z A-Z)echo "$msg"'''mise run greet world --loud # → HELLO WORLDSplitting up large task files
Section titled “Splitting up large task files”When mise.toml gets crowded, move tasks into dedicated files and pull them in with
[task_config].includes:
[task_config]includes = ["tasks.toml", "mytasks/"] # extra TOML files and/or task dirsWorking in a monorepo with tasks spread across packages? mise has first-class
//path:task addressing for that — covered in
Designing Your Workflow.
mise vs Make / npm scripts / just
Section titled “mise vs Make / npm scripts / just”| Make | npm scripts | just | mise | |
|---|---|---|---|---|
| Polyglot (not JS-only) | yes | no | yes | yes |
Tools on PATH automatically | no | partial | no | yes (your pinned versions) |
| Real dependency DAG | yes | no | partial | yes (depends, parallel) |
| Incremental (skip-if-fresh) | yes (mtime) | no | no | yes (sources/outputs, checksums) |
| Typed CLI args | no | no | partial | yes (usage spec) |
| Built-in parallelism | -j | no | no | yes (--jobs) |
| Cross-platform (Windows) | painful | yes | yes | yes (run_windows) |
The pitch: mise tasks see the exact toolchain mise pinned for the project, so
mise run build behaves the same on your laptop, a teammate’s, and CI.
Next: re-run tasks automatically on file changes and wire them into git hooks in Watch, Hooks & Automation, or design a team/monorepo task layout in Designing Your Workflow.