Skip to content
◢ DISPATCH / MISE

Writing Tasks with mise

Posted on September 11, 2026·Updated on September 14, 2026·8 MIN READ· VIEWS

◢ This post is also available in: 日本語

Writing Tasks with mise

What a task is

[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.

Writing one

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.

mise.tomltoml
[tasks.lint]
run = "biome check --write"

That makes mise run lint available. With one unformatted file in the directory, it runs like this.

terminal
❯ mise run lint
[lint] $ biome check --write
Checked 1 file in 125ms. Fixed 1 file.
1 commands3 lines

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.

mise.tomltoml
[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.

terminal
❯ 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 tests
1 commands7 lines

They 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.

Running several commands in order

Give run an array and the entries run from the top.

mise.tomltoml
[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.

It stops where it fails

An array is abandoned at the point something fails. Here is that being checked.

mise.tomltoml
[tasks.steps]
run = ["echo step1", "false", "echo step3"]
terminal
❯ mise run steps
[steps] $ echo step1
step1
[steps] $ false
[steps] ERROR task failed
1 commands5 lines

false on the second line failed, so the run stopped there. step3 never ran.

the run arrayinteractive

Move false all the way down and see what happens to the others.

run = [ ... ]

  1. echo step1
    ran
  2. false
    cut short here
    a command that exits 1
  3. echo step3
    never reached

Lines reached

2

Reached line 2 of 3

The array stops at the first failure, which is why the order matters
mise.tomltoml
[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.

Declaring dependencies

Things that must happen first go in depends.

mise.tomltoml
[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.

Passing arguments

Anything after the task name is appended to the end of the command.

mise.tomltoml
[tasks.greet]
run = "echo hello"
terminal
❯ mise run greet world
[greet] $ echo hello world
hello world
1 commands3 lines

mise.tomltoml
[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.

On Windows it goes through cmd

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.

mise.tomltoml
[tasks.probe]
run = "echo cmd-style=%OS% bash-style=$OSTYPE"
terminal
❯ mise run probe
cmd-style=Windows_NT bash-style=$OSTYPE
1 commands2 lines

%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.

File tasks let a shebang decide

Instead of writing run, you can put a script in mise-tasks/. It is the equivalent of a shebang recipe in just.

mise-tasks/filetaskbash
#!/usr/bin/env bash
#MISE description="Check how a file task behaves"
echo "shell=$OSTYPE args=$*"
terminal
❯ mise run filetask hello
shell=cygwin args=hello
1 commands2 lines

It 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.

text
filetask  Check how a file task behaves

Wanting 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.

text
mise-tasks/db/start   →  mise run db:start
mise-tasks/thumbnail  →  mise run thumbnail

The 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.

In closing

  • [tasks] gives commands a single entry point, and mise tasks is the list of it
  • an array in run executes from the top and stops where it fails; put the tasks that rewrite first and the ones that report last
  • depends declares what has to have finished already. It is not the same job as an array in run
  • arguments land at the end of the command. Write the usage into description
  • on Windows the string goes to cmd. Once you want a second line, move it into a file task under mise-tasks/ and let a shebang pick the interpreter

Tasks 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.

◢ RELATED DISPATCHES — Continue Exploring