◢ This post is also available in: 日本語
[tasks] in mise is a place to give the commands you type often a name and keep them in
mise.toml. Think of it as a language-agnostic task runner, the same idea as
just or make. It looks like scripts
in package.json too, but what the two share stops at "give a command a name" — declaring
dependencies and listing what exists are closer to what just does.
Getting started with mise mentioned the problem of commands
having scattered entry points. The dev server is bun dev, migrations are ./scripts/migrate.sh,
releases are make release. None of that is wrong, but knowing what lives where means opening three
files. Tasks collapse that into one entry point.
As an example, take a command that formats your code. Biome is a linter and
formatter for JavaScript and TypeScript; biome check --write inspects and applies whatever fixes
it can in one pass.
Rather than typing that string every time, give it a name in mise.toml.
[tasks.lint]
run = "biome check --write"That makes mise run lint available. With one unformatted file in the directory, it runs like
this.
❯ mise run lint
[lint] $ biome check --write
Checked 1 file in 125ms. Fixed 1 file.The first line is the command the task actually ran; below it is that command's own output.
Here are a few more. description is optional — it changes nothing about how the task runs.
[tasks.dev]
description = "Start the dev server"
run = "bun dev"
[tasks.build]
description = "Build for production"
run = "bun run build"
[tasks.test]
description = "Run the unit tests"
run = "bun test"
[tasks.lint]
run = "biome check --write"
[tasks."db:image"]
description = "Pull the database container image"
run = "docker pull amazon/dynamodb-local"
[tasks."db:start"]
description = "Start the local database"
run = "bash scripts/db-start.sh"mise tasks lists them.
❯ mise tasks
build Build for production
db:image Pull the database container image
db:start Start the local database
dev Start the dev server
lint
test Run the unit testsThey come out in alphabetical order by name, not in the order you wrote them. Only the ones
given a description have anything beside them; lint is a bare name. Think of that column as
something you write for yourself in six months rather than for other people.
Separating names with a colon, as in db:, puts related tasks next to each other in the listing.
There is no namespace feature behind it — they are simply sorted by name — but that is enough to be
useful.
Give run an array and the entries run from the top.
[tasks.check]
run = [
"biome check --write",
"mise run test",
]A task can call another task, as mise run test does here. That makes it easy to define small
pieces and keep one task that pulls them together.
An array is abandoned at the point something fails. Here is that being checked.
[tasks.steps]
run = ["echo step1", "false", "echo step3"]❯ mise run steps
[steps] $ echo step1
step1
[steps] ERROR task failedstep3 never ran.
[tasks.check]
run = [
"biome check --write", # rewrites
"mise run test", # only reports
]Reverse them and a failing test cuts the run short, so the formatting never happens. Locally that costs you a "fix it and go again". In CI it is a different matter.
If you use something like autofix.ci, where CI commits the fixes your linter produced, whatever was rewritten before the failure gets committed anyway. Put a report-only task in the middle and the run stops there, the formatting behind it never lands, and what gets committed is a half-finished state.
Deciding the order is easiest if you sort by whether the task rewrites files.
Things that must happen first go in depends.
[tasks."db:setup"]
depends = ["db:image", "db:start"]
run = ["mise run db:health", "mise run db:push"]mise run db:setup pulls the image and starts the database, and only then moves on to the health
check and creating the tables.
depends and an array in run do different jobs. depends declares "this has to have finished",
while run is what this task itself does. The former is reusable from other tasks.
Anything after the task name is appended to the end of the command.
[tasks.greet]
run = "echo hello"❯ mise run greet world
[greet] $ echo hello world
hello world[tasks.thumbnail]
description = "Convert a thumbnail to AVIF: mise run thumbnail <png> <slug>"
run = "bash scripts/thumbnail.sh"The order of the arguments is something you will forget if you leave it alone for a while. Putting
it where mise tasks will remind you is the cheapest place to keep it.
If you write tasks on Windows, not knowing this will trip you up. What is in run is handed to
cmd. Here is that being checked.
[tasks.probe]
run = "echo cmd-style=%OS% bash-style=$OSTYPE"❯ mise run probe
cmd-style=Windows_NT bash-style=$OSTYPE%OS% was expanded and $OSTYPE was left as it was. Unix shell notation does not apply. $VAR,
control syntax other than &&, the finer behaviour of pipes and the quoting rules are all
different.
Instead of writing run, you can put a script in mise-tasks/. It is the equivalent of a shebang
recipe in just.
#!/usr/bin/env bash
#MISE description="Check how a file task behaves"
echo "shell=$OSTYPE args=$*"❯ mise run filetask hello
shell=cygwin args=helloIt runs under the interpreter the shebang names rather than under cmd, and the arguments arrive
as they were. Write #MISE description= and it appears in mise tasks with its description
alongside.
filetask Check how a file task behavesWanting to write a second line of shell in a task is a good signal to move it out into a file task. The body is ordinary bash, and the same file runs on macOS, Linux and Windows.
If you already write things like run = "bash scripts/db-start.sh", moving the script into
mise-tasks/ is the whole migration. The directory structure becomes the colon-separated name.
mise-tasks/db/start → mise run db:start
mise-tasks/thumbnail → mise run thumbnailThe wrapper task definitions disappear from mise.toml, and description moves into the script.
Having the explanation next to the thing it explains is worth something too.
[tasks] gives commands a single entry point, and mise tasks is the list of itrun executes from the top and stops where it fails; put the tasks that rewrite
first and the ones that report lastdepends declares what has to have finished already. It is not the same job as an array in rundescriptionmise-tasks/ and let a shebang pick the interpreterTasks are where the steps that used to sit in the README become code. Write them down and the next
person only has to type mise tasks.
Sapling (sl), Meta's Git-compatible VCS — installing it, stacks, one PR per commit with sl pr submit, ReviewStack, and the sl web GUI
Jujutsu (jj), the Git-compatible version control system — installing it, how it differs from Git, and the commands you type every day
Declaring packages, dotfiles, tools and a finishing task in one config file, and bringing a new machine up to it with a single mise bootstrap