mq-task is a task runner that executes code blocks in Markdown files based on section titles.
It is implemented using mq, a jq-like command-line tool for Markdown processing, to parse and extract sections from Markdown documents.
Warning
mq-task is currently under active development.
- Execute code blocks from specific sections in Markdown files
- Task dependencies with automatic execution ordering, including calls that pass arguments
- Parallel execution of independent dependencies, joined before the dependent task runs
- Loops (
forover a list, a glob, a matrix or another task's output) defercleanup tasks that run even after a failure- Conditions (
if), retries and timeouts (plusignore_error), confirmation prompts, and passing a task's output to its dependents - Importing tasks from other Markdown files
- Skip up-to-date tasks (
sources/outputsor customstatuscommands) and--watchmode - Dependency graph output (
graph) and shell completions - Finds
tasks.md(orREADME.md) from any subdirectory, completes task names in your shell, and catches typos inmetaand task names with a suggestion mq-task checkfinds unknown keys, missing dependencies, cycles and missing runtimes without running anything- CLI options for scripts and CI:
--summary,list --json,--output group,--yes - Interactive TUI with search, details, argument prompts, per-task progress and a log pane
- Named task parameters with defaults, plus positional args
- Per-run
--env/--diroverrides, plusmeta-declared defaults for environment variables,.envfiles and working directory (details) - Shared variables, fixed or computed by a command (
vars = { VERSION = { sh = "git describe" } }) - Default task, aliases, and private tasks
- Configurable runtimes for different programming languages, with real exit codes and interactive stdin
- Support for custom heading levels
- TOML-based configuration
- Built on top of the mq query language
See the documentation for the full guide and reference, or how the concepts map if you come from just or Task.
curl -sSL https://task.mqlang.org/install.sh | bashThe installer will:
- Download the latest mq-task binary for your platform and verify its checksum
- Install it to
~/.local/bin/ - Update your shell profile to add
~/.local/binto your PATH
$ cargo install --git https://github.com/harehare/mq-task.git# Run from tasks.md, else README.md (searched up to the repository root)
mq-task "Task Name"
# Run from a specific file
mq-task -f tasks.md "Task Name"mq-task run "Task Name"
mq-task run --file tasks.md "Task Name"You can pass arguments to your task using -- separator:
# Pass arguments to a task
mq-task "Task Name" -- arg1 arg2 arg3
# With explicit run command
mq-task run "Task Name" -- arg1 arg2 arg3
# From a specific file
mq-task -f tasks.md "Task Name" -- arg1 arg2Arguments are accessible via environment variables:
MX_ARGS: All arguments joined by space (e.g., "arg1 arg2 arg3")MX_ARG_0,MX_ARG_1, ...: Individual arguments
Example in a Markdown task:
## My Task
```bash
echo "All args: $MX_ARGS"
echo "First arg: $MX_ARG_0"
echo "Second arg: $MX_ARG_1"
```Like just, you can set environment variables and a working directory for a task at invocation time, with no declaration needed in the Markdown file:
# Set one or more environment variables (repeatable)
mq-task deploy --env REGION=eu --env DEBUG=1
# Run the task's commands in a specific working directory
mq-task deploy --dir ../other-project
# Combine both
mq-task run deploy --env REGION=eu --dir ../other-project--env KEY=VALUE variables are available in the task's shell alongside MX_ARGS/MX_PARAM_*. --dir applies to every command in the task (and its dependencies) for that run. Both flags are available on the shorthand form, run, and tui.
You can also declare default environment variables directly in a task's meta block:
## deploy
```meta
env = ["REGION=staging", "LOG_LEVEL=info"]
```
```bash
echo "deploying to $REGION ($LOG_LEVEL)"
```A CLI --env flag of the same name overrides the meta default for that run:
mq-task deploy # REGION=staging
mq-task deploy --env REGION=prod # REGION=prodA working directory can be declared the same way with dir (relative paths resolve against the directory mq-task was invoked from):
## deploy
```meta
dir = "services/api"
```
```bash
pwd # services/api
```The CLI --dir flag overrides a meta-declared dir for that run:
mq-task deploy # runs in services/api
mq-task deploy --dir services/worker # runs in services/worker insteadA meta block placed before the first heading in the file applies to every task, not just one. It's useful for defaults you'd otherwise have to repeat in each task's own meta block:
```meta
env = ["REGION=staging", "LOG_LEVEL=info"]
dir = "services/api"
```
## deploy
```bash
echo "deploying to $REGION ($LOG_LEVEL) from $(pwd)"
```
## build
```bash
echo "building in $(pwd)"
```Both tasks inherit REGION, LOG_LEVEL, and dir from the document-wide block. Precedence, from lowest to highest, is: document-wide meta → task's own meta → CLI --env/--dir. A task's meta only needs to declare the keys it wants to change:
```meta
env = ["REGION=staging"]
```
## deploy
```meta
env = ["REGION=prod"]
```
```bash
echo "$REGION" # prod (the task's own meta wins)
```
## build
```bash
echo "$REGION" # staging (falls back to the document-wide default)
```You can declare dependencies between tasks using a meta code block (TOML format). When a task is run, its dependencies are automatically executed first in the correct order.
## format
```bash
cargo fmt
```
## lint
```meta
depends = ["format"]
```
```bash
cargo clippy
```
## test
```meta
depends = ["lint"]
```
```bash
cargo test
```Running mq-task test will automatically execute format → lint → test in order:
▶ test
↳ format (dependency)
...
↳ lint (dependency)
...
(test output)
- Multiple dependencies are supported:
depends = ["format", "lint"] - Shared dependencies are executed only once even if multiple tasks depend on them
- Circular dependencies are detected and reported as an error
By default a task's dependencies run one after another in the order they are declared. Set parallel = true to run them concurrently; the task itself starts only after all of them have finished (a fan-out / fan-in):
## ci
```meta
depends = ["lint", "test", "build"]
parallel = true
```
```bash
echo "all checks passed"
```- Only the real dependency edges constrain the order, so a task starts as soon as its own dependencies are done. Tasks that depend on each other (
testdepends onbuild) still run in order - Output lines are prefixed with the task name (
lint │ ...) and each task reports✔/✘with its duration - By default the first failure aborts the run: running tasks are stopped (including processes they started) and nothing new begins. Use
--keep-going/-kto let independent tasks finish; tasks that depend on a failed task are not run --parallelruns every dependency graph concurrently regardless ofparallelin the file, and-j N/--jobs Nlimits how many tasks run at once (-j 1forces sequential execution). The default limit is the number of CPUs- In parallel runs stdin is closed for tasks; interactive tasks should run sequentially. Dry-run is always sequential
To run several tasks at once, define an aggregate task that depends on them (as ci above).
if skips a task when its condition is false. A skipped task does not block the tasks that depend on it.
## deploy
```meta
if = 'flag("CI") && os() == "linux"'
```
```bash
./deploy.sh
```| Expression | True when |
|---|---|
flag("NAME") |
the variable is set and not empty, 0 or false |
getenv("NAME") == "x", getenv("NAME") != "x" |
the variable equals / differs from x |
os() == "linux" |
the OS is linux, macos or windows |
exists("path") |
the path exists (relative to the task's working directory) |
which("cmd") |
cmd is on PATH |
!expr, a && b, a || b |
boolean combinations; parentheses group |
Variables are looked up in --env, the task's meta/params, then the process environment.
A condition is an mq expression that must evaluate to true or false, so anything else mq offers (string functions, contains, ...) works too. Use getenv/flag rather than mq's built-in env(), which only sees the process environment.
## flaky-integration
```meta
retries = 3 # up to 4 attempts in total
retry_delay = "2s" # default: 1s
timeout = "5m" # per attempt; exit code 124 when exceeded
```
```bash
./run-integration-tests.sh
```Durations accept ms, s, m and h (a bare number is seconds). Only runtime failures are retried; missing runtimes or parameters fail immediately. A task with a timeout runs in its own process group so the whole process tree is stopped; for that reason it cannot read from the terminal.
Set capture = true to expose a task's stdout (trailing newline removed) to the tasks that depend on it as MX_OUTPUT_<TASK> (the title upper-cased, other characters replaced with _). The output is still printed.
## version
```meta
capture = true
```
```bash
git describe --tags
```
## release
```meta
depends = ["version"]
```
```bash
echo "releasing $MX_OUTPUT_VERSION"
```Captured output is available to the task's direct and transitive dependents. When a task captures its output, stdout is piped, so programs may stop colouring it.
List files in the document-wide meta block (before the first heading). Their tasks become available as <file name>:<task>:
```meta
imports = ["ci/checks.md", "deploy.md"]
```
## release
```meta
depends = ["checks:lint", "deploy:staging"]
```- Paths are relative to the importing file; imports can be nested (
a:b:task) and circular imports are rejected - Inside an imported file,
dependsrefer to that file's own tasks by their short names - The imported file's document-wide
env/dirdefaults apply to its tasks. Relativedirvalues resolve against the directorymq-taskwas invoked from, as usual
Declare the files a task reads with sources (globs). The task is skipped when its sources, code, arguments and environment are unchanged since its last successful run. With outputs, the task also runs if any listed output is missing.
## build
```meta
sources = ["src/**/*.rs", "Cargo.toml"]
outputs = ["target/release/app"]
```
```bash
cargo build --release
```Fingerprints are stored in .mq-task/cache.json in the directory mq-task runs from (consider git-ignoring .mq-task/). --no-cache ignores them.
--watch/-w re-runs the task whenever any sources of the task or its dependencies, or the task file itself, change:
mq-task test --watchmq-task graph ci # tree
mq-task graph --format mermaid # Mermaid diagram of all public tasksci [parallel]
├── lint
└── test
└── build
mq-task tui shows a searchable task list with a details pane (description, dependencies, params, options, code).
| Key | Action |
|---|---|
↑/↓, j/k |
move |
/ |
search (matches titles, aliases, descriptions) |
Enter |
run inside the TUI (prompts for arguments when a param is required) |
a |
run with arguments (name=value, positional; quote values containing spaces) |
x |
leave the TUI and run in the terminal (interactive stdin works) |
q/Esc |
quit |
While a task runs, the top pane draws the dependency graph as a left-to-right workflow (one stage per column, each task showing its state ○ pending, spinner running, ✔, ✘, ⊘, ↻ retry and its elapsed time) and the bottom pane the log. Arrow keys (or hjkl) move the selection, v switches to a flat task list, Tab switches between the combined log and the selected task's log, PgUp/PgDn scroll, q/Ctrl+C aborts, and r re-runs. The execution flags (--parallel, -j, -k, --no-cache) apply to the TUI too.
mq-task completions zsh > ~/.zfunc/_mq-task
mq-task completions bash > /etc/bash_completion.d/mq-task # also completes task namesDeclare parameters in the meta block. A bare name is required; name=value sets a default.
## deploy
```meta
params = ["env=staging", "region"]
```
```bash
echo "deploying to $MX_PARAM_ENV / $MX_PARAM_REGION"
```mq-task deploy -- region=eu # env falls back to "staging"
mq-task deploy -- prod eu # positional, filled in declaration order
mq-task deploy -- env=prod region=eu # namedParameters are exposed as MX_PARAM_<NAME> environment variables. A required parameter with no value bound is an error.
Give a task alternate names with alias in its meta block:
## build
```meta
alias = ["b"]
```
```bash
cargo build
```mq-task b now runs build. Set default_task in mq-task.toml to run a task when mq-task is invoked with no task name:
default_task = "build"A task whose title starts with _, or whose meta block has private = true, is hidden from list/tui output but can still be run directly or used as a dependency:
## _cleanup
```bash
rm -rf tmp/
```# List tasks from tasks.md, else README.md
mq-task
# List tasks from a specific file
mq-task -f tasks.md
mq-task list --file tasks.md
# Include private tasks
mq-task list --allmq-task init # writes a starter tasks.md
mq-task init --config # writes mq-task.toml with the default runtimesmq-task.toml is loaded automatically from the directory of the task file.
Create an mq-task.toml file to customize runtime behavior:
# Runtimes configuration
# Simple format: language = "command", execution mode defaults to "file"
[runtimes]
bash = "bash"
sh = "sh"
python = "python3"
ruby = "ruby"
node = "node"
javascript = "node"
js = "node"
php = "php"
perl = "perl"
# Detailed format with execution mode
# Execution modes: "file" (default), "stdin", or "arg"
# - file: write code to a temp file and run it as an argument (keeps stdin interactive)
# - stdin: pipe code into the command's standard input (stdin unavailable to the script itself)
# - arg: pass code as a command-line argument
[runtimes.go]
command = "go run"
execution_mode = "file"
[runtimes.jq]
command = "jq"
execution_mode = "arg" # jq's filter is a CLI argument, not read from stdin
[runtimes.mq]
command = "mq"
execution_mode = "arg" # mq uses query as argumentUse execution_mode = "stdin" only when a command needs the code piped in literally (e.g. psql, redis-cli). The task script then can't also read from the terminal.
You can also mix both formats:
[runtimes]
python = "python3" # Simple format, uses default file mode
[runtimes.go] # Detailed format with custom execution mode
command = "go run"
execution_mode = "file"# Using shorthand (from tasks.md by default)
mq-task Build
# From a specific file
mq-task -f tasks.md Build
# Using explicit run command
mq-task run Build
mq-task run --file tasks.md Builddefault_task (top-level, outside [runtimes]) sets which task runs when mq-task is invoked with no task name. See Aliases and the default task.
mq-task exits with the same code the task's process exited with, so it composes correctly with &&, CI, and git hooks. A task killed by its timeout exits with 124. When several tasks fail under --keep-going, the first failure's code is used.
MIT
