← Back to articles

Taskfile: the modern replacement for Makefile

Taskfile (Task) is a task runner written in Go that advantageously replaces Make for modern projects. A look at its history, its advantages, and a comparison with Make, Just and shell scripts.

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.