Taskfile: the modern replacement for Makefile
Table of Contents
For decades, Make has been the reference tool for automating build and development tasks. But it has to be said that its cryptic syntax and its limitations make it a sometimes painful tool day to day. Taskfile (also called Task) offers a modern, readable and cross-platform alternative. Here’s why it deserves your attention.
A bit of history #
Make, the timeless classic #
Make was born in 1977 at Bell Labs, created by Stuart Feldman to replace the shell scripts used to compile Unix. Its fundamental principle: a Makefile defines rules based on targets, prerequisites, and commands to run. Make compares file modification dates to only rebuild what has changed.
.PHONY: build
build:
go build -o bin/myapp ./cmd/myapp
For nearly 50 years, Make has been the ubiquitous tool for C/C++ projects, then for any project requiring automation. It’s installed by default on almost all Unix/Linux/macOS distributions.
Make’s limits #
Make was designed for a world different from ours:
- Opaque syntax: tabs vs spaces, variables with
$()or${},%pattern rules — everything is a source of confusion. - No native JSON/YAML support: Makefiles are raw text with no data structure.
- Limited ecosystem: no task registry, no automatically resolved dependencies between tasks.
- Shell dependency: commands run in a subshell, which can vary by OS.
- Generated Makefiles: many projects end up generating their Makefile via tools (cmake, automake, etc.), adding a layer of indirection.
The emergence of alternatives #
The community gradually developed alternatives:
- Ant (2000) — XML, verbose, popularized by Java
- Rake (2004) — Ruby DSL, elegant but Ruby-required
- Gradle (2008) — Groovy/Kotlin, the Java/Kotlin standard
- Jake (2010) — JavaScript, gone
- just (2016) — Simplicity, recipes inspired by Make but modernized
- Task (2017) — YAML, cross-platform, inspired by Go
What is Taskfile? #
Task (github.com/go-task/task) is a task runner written in Go, available under the MIT license. It uses Taskfile.yml files in YAML to define tasks, which makes it accessible to anyone who already knows Kubernetes, GitHub Actions, or Ansible.
Installation #
# macOS
brew install go-task/tap/go-task
# Linux (official script)
sh -c "$(curl -sL https://taskfile.dev/install.sh)"
# Windows (Scoop, Chocolatey, or via GitHub releases)
scoop install task
# Via Go
go install github.com/go-task/task/v3/cmd/task@latest
A first example #
version: '3'
tasks:
default:
deps: [build]
cmds:
- ./bin/myapp --help
build:
desc: Build the application
dir: ./cmd/myapp
cmds:
- go build -o ../../bin/myapp .
test:
desc: Run tests
cmds:
- go test -v ./...
lint:
desc: Lint code
cmds:
- golangci-lint run
clean:
desc: Clean build artifacts
cmds:
- rm -rf bin/
# List available tasks
task --list
# Run the default task
task
# Run a specific task
task build
# Run with a variable
task deploy ENV=production
The features that make the difference #
Dependencies between tasks #
tasks:
build:
deps: [deps, lint, test]
cmds:
- go build ./...
deps:
cmds:
- go mod download
lint:
cmds:
- golangci-lint run
test:
cmds:
- go test ./...
Dependencies run in parallel by default. For sequential execution, use depends:
tasks:
setup:
depends: [create-db, migrate-db]
cmds:
- echo "Setup complete"
Environment variables and interpolation #
version: '3'
env:
APP_NAME: myapp
VERSION: '1.0.0'
vars:
BINARY_NAME: "{{.APP_NAME}}-{{.OS}}-{{.ARCH}}"
tasks:
build:
vars:
OUTPUT: "./dist/{{.BINARY_NAME}}"
cmds:
- go build -ldflags="-X main.version={{.VERSION}}" -o {{.OUTPUT}} ./cmd/myapp
Task exposes variables:
.OS— operating system (linux, darwin, windows).ARCH— architecture (amd64, arm64, etc.).TASK— name of the current task.CLI_ARGS— arguments passed on the command line
Watching and builds #
version: '3'
tasks:
dev:
desc: Run development server with hot reload
cmds:
- task: watch-src
watch: true
- go run ./cmd/server
watch-src:
desc: Watch source files and rebuild
cmds:
- while true; do
inotifywait -q -e modify src/*.go;
go build ./cmd/server;
done
status:
- test -f ./server
Or using the native watch directive (v3.26+):
tasks:
build:
cmds:
- go build ./...
watch: true
sources:
- src/**/*.go
generate:
task: build
Templates and includes #
# Main Taskfile.yml
version: '3'
includes:
docker:
taskfile: ./Taskfile.docker.yml
vars:
TAG: latest
.shared: ./Taskfile.shared.yml
# Taskfile.shared.yml
version: '3'
tasks:
log:
cmds:
- echo "Version: {{.VERSION}}"
Interactive prompts #
tasks:
deploy:
prompt: "Are you sure you want to deploy to {{.ENV}}? [y/N]"
confirm: true
cmd: echo "Deploying..."
Makefile vs Taskfile: the comparison #
| Makefile | Taskfile | |
|---|---|---|
| Syntax | Make DSL | YAML |
| Readable | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| Cross-platform | ⚠️ (msys, gmake) | ✅ Native |
| Variables | ${VAR} or $(shell ...) | {{.VAR}} |
| Dependencies | Manual | Automatic with deps |
| Parallel execution | Global -j flag | By default in deps |
| Config files | No | Yes (JSON/YAML includes) |
| Interactive prompts | No | Yes |
| Watch mode | Via external tools | Native |
| Task registry | No | No (DIY) |
| Installability | Make installed | binary or brew |
Make vs Task example #
Makefile:
.PHONY: build test lint clean
APP_NAME := myapp
VERSION := $(shell git describe --tags --always)
BIN := bin/$(APP_NAME)
SRC := $(wildcard cmd/**/*.go)
build: $(BIN)
$(BIN): $(SRC)
go build -ldflags="-X main.version=$(VERSION)" -o $(BIN) ./cmd/myapp
test:
go test -v ./...
lint:
golangci-lint run
clean:
rm -rf bin/
install: build
install -Dm755 $(BIN) /usr/local/bin/$(APP_NAME)
Taskfile.yml:
version: '3'
vars:
APP_NAME: myapp
VERSION:
sh: git describe --tags --always
BIN: bin/{{.APP_NAME}}
tasks:
default:
deps: [lint, test, build]
build:
desc: Build the application
cmds:
- go build -ldflags="-X main.version={{.VERSION}}" -o {{.BIN}} ./cmd/myapp
test:
desc: Run tests
cmds:
- go test -v ./...
lint:
desc: Lint code
cmds:
- golangci-lint run
clean:
desc: Clean artifacts
cmds:
- rm -rf bin/
install:
desc: Install binary
deps: [build]
cmds:
- install -Dm755 {{.BIN}} /usr/local/bin/{{.APP_NAME}}
The Taskfile is more explicit (desc, vars, structure), the Makefile more concise but cryptic.
Just vs Taskfile #
Just (github.com/casey/just) is another serious competitor, written in Rust. Its recipe syntax looks like Make but better:
# .justfile
APP_NAME := "myapp"
VERSION := `git describe --tags --always`
build:
go build -ldflags="-X main.version={{VERSION}}" -o bin/{{APP_NAME}} ./cmd/myapp
test:
go test -v ./...
default: build test
| Just | Task | |
|---|---|---|
| Syntax | Custom DSL (recipes) | YAML |
| Files | .justfile | Taskfile.yml |
| Readable | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Learning curve | Low (Make-like syntax) | Low (familiar YAML) |
| Variables | Make-like | Template {{.VAR}} |
| Includes | Yes | Yes |
| Watch mode | Plugin | Native |
| Parallel execution | Via recipe dependencies | Automatic in deps |
| Prompts | No | Yes |
Just is excellent if you prefer a recipe-style syntax. Task excels in projects where YAML is already the standard (Kubernetes, CI/CD).
Use cases #
Go project #
version: '3'
tasks:
default:
deps: [test, build]
dev:
desc: Run with hot reload
cmds:
- air
test:
cmds:
- go test -race -v ./...
lint:
cmds:
- golangci-lint run
build:
cmds:
- go build -ldflags="-s -w" -o bin/myapp ./cmd/myapp
container:
deps: [build]
cmds:
- docker build -t myapp:{{.VERSION}} .
release:
deps: [test, lint]
cmds:
- goreleaser release --clean
Node.js project #
version: '3'
tasks:
default:
deps: [lint, test]
install:
cmds:
- npm install
test:
deps: [install]
cmds:
- npm run test
lint:
deps: [install]
cmds:
- npm run lint
build:
deps: [install]
cmds:
- npm run build
docker:build:
cmds:
- docker build -t myapp:{{.TAG}} .
docker:push:
deps: [docker:build]
cmds:
- docker push myapp:{{.TAG}}
Multi-service Docker Compose #
version: '3'
tasks:
up:
cmds:
- docker compose up -d
down:
cmds:
- docker compose down
logs:
cmds:
- docker compose logs -f {{.SERVICE}}
restart:
deps: [down, up]
db:migrate:
cmds:
- docker compose exec api npm run db:migrate
db:seed:
cmds:
- docker compose exec api npm run db:seed
The state of the ecosystem in 2026 #
Task is a mature project:
- 15,000+ GitHub stars
- 1 million+ downloads per month
- Native support in several IDEs (VS Code extension)
- Integration in CI/CD tools
- An active team with regular releases (v3.49.x in March 2026)
The project remains community-maintained, without major corporate backing, which can be an advantage (independence) or a risk (dependence on contributors).
Conclusion #
Taskfile fills Make’s gaps while keeping its philosophy: a simple tool to run common tasks. YAML makes it immediately accessible, the automatic deps simplify the dependency graph, and the cross-platform support eliminates surprises.
Personally, I mainly use it to bootstrap the base components in a Kubernetes cluster, defining tasks to apply the essential manifests (Namespace, ServiceAccount, RBAC, CNI, etc.) and orchestrate the deployment of Operators.
One last piece of advice: whatever tool you choose, document your tasks with desc or comments. A project without documentation of its tasks is a project where every developer reinvents the wheel at deployment time.