Taskfile: The Modern Alternative to Makefile That Will Change Your Workflow
On most of our projects we use Makefile. It allows us to automate certain repetitive tasks and also serves as our first entry point into a project. A well-written Makefile enables quickly discovering a project. However, after years of working with Makefiles, I’ve accumulated a list of frustrations that slow me down every day.
Here’s what typically happens: I’m joining an existing project with a makefile that has already been initialized. I see a command called “run_specific_task” I don’t know what this command does because it is not documented and Makefile does not offer help by default. I run it, but damn, it launches an apt_get. I’m on a Mac and already I have a problem because I can’t run this command.
So I need to make a distinction between Mac and Linux. How do I do that? I have two choices: either I duplicate my command “run_specific_task_OSX” or I modify my first command by adding OS detection with cryptic shell syntax?
The worst part? Six months later, when a new developer joins the team, and he works on Windows !!
I know that with scripting and a good knowledge of Make, these problems can be overcome without difficulty. But what if there was a turnkey solution? And then I discovered Taskfile.
Taskfile: What is it?
Taskfile is a tool inspired by Makefile written in Go. It was designed to address common problems encountered with Makefile, while offering a simpler and more readable syntax. Taskfile uses a YAML configuration file, which makes it more accessible for those who are not familiar with Makefile syntax.
To install it, simply follow the instructions on the official website: Taskfile Installation. This tool is cross-platform!
A Real-World Comparison
Let me show you the kind of Makefile we typically write for a React project with a Node.js backend:
.PHONY: help install dev build test clean deploy
# Detect OS for cross-platform commandsifeq ($(OS),Windows_NT) RM = cmd /C del /Q /F RMD = cmd /C rmdir /S /Qelse RM = rm -f RMD = rm -rfendif
help: ## Show this help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}'
install: ## Install dependencies npm ci cd api && npm ci
dev: ## Start development servers @echo "Starting frontend and backend..." npm run dev & cd api && npm run dev
build: install ## Build for production npm run build cd api && npm run build
test: ## Run all tests npm test cd api && npm test
clean: ## Clean build artifacts $(RMD) dist $(RMD) api/dist $(RMD) node_modules $(RMD) api/node_modules
deploy: test build ## Deploy to production @if [ -z "$(ENV)" ]; then \ echo "Error: ENV variable not set"; \ exit 1; \ fi ./scripts/deploy.sh $(ENV)This Makefile works, but it has several problems:
The help target - That grep | awk is an obscure line that has been passed down from makefile to makefile for 40 years.
OS detection is fragile - The ifeq ($(OS),Windows_NT) block handles Windows but what about WSL? Or specific Linux distributions? It quickly becomes a mess of conditionals.
Sequential execution is unclear - The dev target uses & to run commands in parallel, but this only works in bash. The build target runs install first, but only because it’s listed as a dependency - it’s not obvious from the command list.
Error handling is a nightmare - The deploy target checks if ENV is set, but the syntax is bash-specific and verbose. Every time you want to add validation, you need more shell scripting.
Here’s the equivalent Taskfile:
version: '3'
dotenv: ['.env.{{.ENV}}', '.env']
tasks: install: desc: Install dependencies cmds: - npm ci - npm ci dir: api sources: - package-lock.json - api/package-lock.json generates: - node_modules/**/* - api/node_modules/**/*
dev: desc: Start development servers cmds: - task: frontend-dev - task: api-dev
frontend-dev: cmds: - npm run dev
api-dev: dir: api cmds: - npm run dev
build: desc: Build for production deps: [install] cmds: - npm run build - npm run build dir: api
test: desc: Run all tests cmds: - npm test - npm test dir: api
clean: desc: Clean build artifacts cmds: - cmd: rm -rf dist api/dist node_modules api/node_modules platforms: [linux, darwin] - cmd: rmdir /s /q dist api\dist node_modules api\node_modules platforms: [windows]
deploy: desc: Deploy to production deps: [test, build] prompt: This will deploy to {{.ENV}}. Continue? preconditions: - sh: '[ ! -z "$ENV" ]' msg: 'ENV variable must be set' cmds: - ./scripts/deploy.sh {{.ENV}}Look at the difference:
- Self-documenting: No magic
grep | awk. Thedescfield is built-in and appears withtask --list. - Cross-platform by design: The
platformsfield handles OS differences explicitly. No more fragile shell conditionals. - Smart rebuilds: The
installtask only runs whenpackage-lock.jsonchanges, thanks tosourcesandgenerates. - Clear dependencies:
deps: [test, build]is immediately obvious. No hidden behavior. - Built-in validation:
preconditionsandpromptprovide safety nets without bash scripting.
The Taskfile is longer and more verbose yes, but every line is self explaining. When a new developer joins the project, they can read it top to bottom without arcane Make knowledge.
What Makes Taskfile Different
Let me be clear: Makefiles can technically do most of what Taskfile does. And with enough shell scripting, you can work around the limitations. And that’s why Taskfile is here, to provide a better experience without writing shell scripts.
Self documenting by Design
When using Makefile for the first time, I was surprised not to be able to easily add a description to my tasks. When I talk about description, I’m talking more about self-documentation. There are solutions, as explained a previous blog post (see Self-Documented Makefile), but they are not very intuitive. With Taskfile, you just need to add a desc key to the task to give it a description. It’s a real plus for the readability of the configuration file.
Then at the command line level, you just need to run the task --list command to see the list of all available tasks with their description. It’s a real time saver to quickly discover the available tasks in a project.
Furthermore, since we are writing YAML, we must adhere to a certain nomenclature, which forces us to have clearer code.
Truly Cross-Platform Without Hacks
In Make, cross-platform support means detecting the OS with shell commands and branching logic:
ifeq ($(OS),Windows_NT) RM = cmd /C del /Q /F RMD = cmd /C rmdir /S /Qelse RM = rm -f RMD = rm -rfendifThis breaks down quickly. What if someone uses Git Bash on Windows? What about different PowerShell versions? You end up with a maze of conditionals. Taskfile lui se base sur sh, un shell universel présent sur tous les systèmes. Vous écrivez une commande, et Taskfile s’assure qu’elle fonctionne partout. Pas de détection d’OS, pas de syntaxe spécifique à un shell. Mais si toutefois vous avez besoin d’une commande spécifique à une plateforme, Taskfile gère ça aussi.
Taskfile’s platforms field is native and works at the command level:
clean: cmds: - cmd: rm -rf dist platforms: [linux, darwin] - cmd: Remove-Item -Recurse -Force dist platforms: [windows]It’s not just “easier” - it’s architecturally different. Taskfile was designed with cross-platform development in mind from day one, while Make was designed for Unix-like systems.
File Watching and Incremental Builds
Make can track file changes with pattern rules, but it’s verbose and limited:
# VariablesSRC_DIR = srcDIST_DIR = distSRC_FILES = $(shell find $(SRC_DIR) -name "*.js")DIST_FILES = $(patsubst $(SRC_DIR)/%.js,$(DIST_DIR)/%.js,$(SRC_FILES))
all: build-js
build-js: $(DIST_FILES)
$(DIST_DIR)/%.js: $(SRC_DIR)/%.js @mkdir -p $(@D) @babel $< --out-file $@
watch: fswatch -o $(SRC_DIR) | while read f; do make build-js; done
clean: rm -rf $(DIST_DIR)
.PHONY: all build-js watch cleanThis doesn’t scale. What if you have nested directories? What if you want to watch for changes and rebuild automatically? You need to add inotifywait or fswatch, write shell scripts, handle edge cases…
Taskfile has this built-in:
build-js: sources: - src/**/*.js generates: - dist/**/*.js cmds: - babel src --out-dir dist
watch: cmds: - task build-js --watchThe --watch flag is native. Run task watch and it monitors file changes automatically. On every platform.
Interactive Prompts and Validation
Try adding user confirmation in a Makefile:
deploy: @echo "Deploy to production? (y/n)" @read -p "" -n 1 -r; \ if [[ ! $$REPLY =~ ^[Yy]$$ ]]; then \ echo "Aborted"; \ exit 1; \ fi ./deploy.shThis is bash-specific. It won’t work in sh, fish, or Windows CMD. You need to detect the shell and write different versions.
Taskfile’s prompt is built-in and cross-platform:
deploy: prompt: Deploy to production? cmds: - ./deploy.shOne line. Works everywhere. And you can add preconditions for validation:
deploy: prompt: Deploy to {{.ENV}}? preconditions: - sh: '[ ! -z "$ENV" ]' msg: 'ENV variable must be set' - sh: 'git diff-index --quiet HEAD' msg: 'Working directory must be clean' cmds: - ./deploy.sh {{.ENV}}Try implementing this in Make without shell-specific code and you will end up with something unreadable. And it’s a small exemple. In a real project, you might have multiple prompts, complex validation logic, or dynamic behavior based on user input.
deploy: @if not "$(ENV)"=="" ( \ if not errorlevel 1 ( \ git diff-index --quiet HEAD || ( \ echo Error: Working directory must be clean && \ exit /b 1 \ ) && \ set /p REPLY="Deploy to $(ENV)? [y/N] " && \ if "$$REPLY"=="y" ( \ ./deploy.sh $(ENV) \ ) else ( \ echo Deploy cancelled. \ ) \ ) else ( \ echo ENV variable must be set && \ exit /b 1 \ ) )Dynamic Dependencies and Parallel Execution
Make handles dependencies, but they’re static:
test: build npm test
build: install npm run build
install: npm ciThis runs sequentially: install → build → test. What if you want to run frontend and backend tests in parallel, but both depend on their respective builds?
test: frontend-test backend-test
frontend-test: frontend-build npm test
backend-test: backend-build cd api && npm testThis still runs sequentially because Make doesn’t parallelize by default. You need make -j, but then you lose control - everything runs in parallel, including things that shouldn’t.
Taskfile lets you control parallelism explicitly:
test: desc: Run all tests deps: - task: frontend-test - task: backend-test
frontend-test: deps: [frontend-build] cmds: - npm test
backend-test: deps: [backend-build] dir: api cmds: - npm testThe deps at the test level run in parallel automatically. The nested deps run sequentially. You get fine-grained control without flags or guesswork.
Other Features Worth Mentioning
Beyond the core differences, Taskfile has several features that improve day-to-day workflows:
Task namespacing: Organize tasks into groups:
tasks: docker:build: cmds: - docker build -t myapp .
docker:push: cmds: - docker push myappRun them with task docker:build. Try organizing tasks in Make without prefixing everything manually.
Including other Taskfiles: Split your configuration across files:
includes: docker: ./docker/Taskfile.yml aws: ./aws/Taskfile.ymlRun tasks with task docker:build or task aws:deploy. Make has include, but it just concatenates files - there’s no namespacing or organization.
Silent mode and output control: Control what gets printed:
tasks: build: silent: true cmds: - npm run build - echo "Build complete"In Make, you prefix every command with @ to suppress output. In Taskfile, it’s a flag on the task.
Remote Taskfiles (Experimental): Run tasks defined in remote repositories:
version: '3'
includes: my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.ymlWith this, you can reuse common tasks across projects without copying files. Make has no equivalent.
Conclusion
I loved discovering Taskfile. It’s because it solves real problems that Make wasn’t designed for at the beginning.
Yes, Make is powerful. Yes, you can technically implement most of these features with enough shell scripting. But that’s exactly the problem - you end up maintaining shell scripts instead of build configurations. Your Makefile becomes a compatibility layer instead of a task runner.
Taskfile was designed in 2017, decades after Make. It learned from Make’s strengths and addressed its weaknesses.
I don’t say that you should delete your Makefile and switch to Taskfile tomorrow. This is a very personal opinion; some will say that YAML syntax is too verbose or have other concerns, and I respect that. In any case, for me, it’s adopted. If you start a new project, give Taskfile a try. Your future self will thank you.
For more information, check out the official documentation.
Authors
Full-stack web developer at marmelab, Anthony seeks to improve and learn every day. He likes basketball, motorsports and is a big Harry Potter fan