How to Use just and Task Runners for Project Scripts
Compare the task runners just and Taskfile for project scripts: recipe syntax, arguments, dependencies, environment loading, and when make is still enough.

Make solves a build-system problem, and most projects use it for something else: a list of commands with names. Two tools exist for that. They fix the pain points listed in Makefile automation: tabs, one shell per line and argument handling. The examples assume just 1.36 and Task 3.38.
A justfile
set dotenv-load := true
# list recipes
default:
@just --list
# run the tests
test *args:
python -m pytest {{args}}
# build and push an image for an environment
deploy env="staging":
docker build -t myapp:{{env}} .
docker push registry.example.test/myapp:{{env}}
just
just test -k login
just deploy productionAvailable recipes:
default # list recipes
deploy env="staging" # build and push an image for an environment
test *args # run the tests
Recipes take arguments with defaults, and *args collects any extra. Comments above a recipe become its description. Indentation can be spaces. Each recipe runs as one script in a single shell unless you change that, so cd persists. dotenv-load reads a .env file into the recipe environment.
A Taskfile
version: "3"
vars:
IMAGE: myapp
tasks:
default:
cmds:
- task --list
silent: true
test:
desc: Run the tests
cmds:
- python -m pytest {{.CLI_ARGS}}
build:
desc: Build the image
deps: [test]
sources:
- Dockerfile
- src/**/*
cmds:
- docker build -t {{.IMAGE}} .Run with task build, and pass arguments after --, as in task test -- -k login. deps run in parallel before the task. With sources and generates, Task skips a task when files have not changed, which gives make-like incremental behaviour with checksums rather than timestamps. The YAML is more verbose than a justfile and easier to generate from other tools.
Choosing
A short comparison of the features that decide in practice:
Need make just task
Present on a fresh Linux box yes no no
Recipe arguments meh yes yes (CLI_ARGS)
File-change skipping yes no yes
Loads .env no yes yes
Tabs required yes no no
If contributors run the commands on their own machines and each can install one binary, just gives the shortest recipes. If your pipeline already uses YAML and wants change detection, pick Task. If the repository must build on a bare CI image or a customer's server, keep make. Whichever you choose, make CI call the same targets developers use, as described in CI scripts in the repository.
Why a named command list beats a pile of scripts
A repository with ten shell scripts in a scripts/ directory has the same commands as one with a task runner, but the runner adds three things that scripts do not: a single index of what exists, a place for defaults, and one entry point for both people and CI. The index matters most. A new contributor who types just or task --list sees every operation with its one-line description, and nobody has to open files to find out whether the test command wants a flag or an environment variable.
Defaults are the second gain. A recipe that takes env="staging" as a parameter documents its own default in the same line that defines it. Compare that with a script that reads $1, falls back to a hard-coded value halfway down, and prints a usage message nobody has updated since the second argument was added. The runner also forces the habit of naming arguments, which keeps call sites readable in CI logs.
Migrating from a Makefile without a big bang
Most projects arrive at a runner from a Makefile that grew phony targets. The safest route is to move the targets one at a time and keep the old file as a thin forwarder until the last caller changes. Each Make target such as test becomes a recipe of the same name, and the Makefile keeps a line that calls the new tool. CI and developers keep typing make test, and the change lands in small reviewable steps.
Three behaviours differ and cause most of the surprises. Make runs every line of a recipe in a separate shell, so a cd on one line does not carry to the next; just runs the whole recipe in one shell by default, and Task runs each command separately like Make does. Make expands $VAR itself and needs $$VAR to reach the shell; just and Task do not apply the same rule, so a copied recipe may need its doubled dollar signs undone. And Make's timestamp-based skipping has no equivalent in just at all: a recipe always runs, so any step that was cheap only because Make skipped it needs an explicit check or a move to Task's checksum-based sources.
Running the same targets in CI
The value of a runner collapses if CI uses a different set of commands from the developers. The practical rule is that the pipeline calls the recipe, and the recipe holds the logic. A CI job then reads just test, and a failure on the server reproduces on a laptop by typing the same words. This is the structure described in keeping CI logic in scripts in the repository.
The cost is installing the runner on the CI image. Both tools ship as a single static binary, so a download step with a pinned version and a checksum is enough, and the pin should live in the repository so a runner upgrade is a reviewed change. A hosted action or package-manager install is fine as long as the version is explicit; an unpinned install means the pipeline can break on a day when nothing in the repository changed.
Finally, decide who owns the recipe list. Recipes that only one person understands rot quickly. A short comment above each one, shown by the list command, is the cheapest documentation a project can keep accurate, because it sits next to the code it describes and shows up in the output people actually read.
Mistakes teams make with task runners
The first mistake is putting real logic in the runner file. A recipe that grows to thirty lines of conditionals and string handling has recreated the unreadable Makefile it replaced, in a syntax that fewer editors highlight. Keep recipes to one or two commands and move anything longer into a script that the recipe calls. The script can be linted with ShellCheck and exercised by itself, which a recipe body cannot.
The second is hiding what runs. Both tools print each command before executing it by default, and the leading @ in just or the silent setting in Task turns that off. Silence suits a helper like the list recipe, but a deploy that prints nothing leaves a failing build with no clue about which step broke. Prefer visible commands everywhere except trivial ones.
The third is treating the tool as a dependency manager. Neither just nor Task installs compilers, interpreters or system packages. A recipe named setup that runs apt-get works on exactly one operating system, so state the supported platforms in the README, or give the project a container image that holds the toolchain.
Finally, pin the runner version where the team can see it, whether in a tool-versions file, a CI variable or a devcontainer. A teammate on an older release will otherwise hit a syntax error that the file's author never sees.
Where this does not apply
For a project with three commands, a scripts/ directory with a short README is enough, and a new tool is a new dependency to document. Packaging also differs: both are single binaries available through Homebrew, Cargo or a release download, but not in every distribution. See the just manual and the Taskfile usage guide. The pattern of replacing make is covered in the tooling cluster.


