Skip to content

Fix: Taskfile Not Working — Variables, Sources/Generates, Includes, Watch, and Run-Once Semantics

FixDevs ·

Quick Answer

How to fix Task (go-task) errors — Taskfile.yml not found, vars interpolation, sources/generates fingerprint, includes scoping, watch mode glob, deps parallel execution, and run: once preventing reruns.

The Error

You run task in your project and it complains there’s no Taskfile:

$ task build
task: No Taskfile found. Use "task --init" to create a new one

Or a variable interpolation produces empty output:

# Taskfile.yml
version: '3'
vars:
  BIN: ./bin/myapp
tasks:
  build:
    cmds:
      - go build -o {{.BIN}} ./cmd/...
$ task build
go build -o  ./cmd/...
# {{.BIN}} expanded to nothing — empty binary path.

Or a task with sources runs every time even when nothing changed:

tasks:
  build:
    sources:
      - "**/*.go"
    generates:
      - ./bin/myapp
    cmds:
      - go build -o ./bin/myapp ./cmd/...
$ task build  # Runs go build
$ task build  # Runs go build AGAIN — should skip

Or task watch doesn’t pick up file changes:

$ task --watch build
# Edit src/main.go → no rebuild.

Why This Happens

Task (the go-task/task project) is a YAML-based task runner. Most issues map to one of:

  • Taskfile.yml location. Task walks up from the working directory looking for Taskfile.yml, taskfile.yml, Taskfile.yaml, or taskfile.yaml. Misnamed files (Taskfile.YML, Taskfile.yaml.bak) are ignored.
  • Variable scope. Vars defined in vars: at the file level are global. Task-local vars: shadow them. Env vars are separate (env:). Mixing them up produces empty interpolations.
  • sources/generates use modification time + checksum. A task --status check compares timestamps and contents. If your “generated” file changes for unrelated reasons (formatting), Task sees it as stale.
  • Watch mode triggers on sources changes, not arbitrary files. Without listing the right sources, watch won’t restart.

Fix 1: Name the File Correctly

Task accepts these names (case-sensitive on Linux):

  • Taskfile.yml
  • Taskfile.yaml
  • taskfile.yml
  • taskfile.yaml
  • Taskfile.dist.yml (a “default” that can be overridden by a local Taskfile)

Run task --list to verify Task can see your file:

$ task --list
task: Available tasks for this project:
* build:       Build the project
* test:        Run tests

If the list is empty or “No Taskfile found”, check the working directory and filename.

For Taskfiles in subdirectories:

task -d ./build/Taskfile.yml build
# or:
task --taskfile ./build/Taskfile.yml build

Pro Tip: Always include version: '3' at the top of Taskfile.yml. Older version: '2' syntax is deprecated but still parses with subtle differences.

Fix 2: Variables and Templating

Task uses Go’s text/template syntax. Vars are accessed with {{.VarName}}:

version: '3'

vars:
  APP_NAME: myapp
  VERSION:
    sh: git describe --tags --always   # Compute from shell
  BIN: "./bin/{{.APP_NAME}}"           # Reference other vars

tasks:
  build:
    cmds:
      - go build -ldflags "-X main.version={{.VERSION}}" -o {{.BIN}} ./cmd/...
    vars:
      LOCAL_VAR: hello  # Task-scoped, only inside this task

Three sources of values (lowest to highest precedence):

  1. vars: at file level.
  2. vars: at task level.
  3. --<var-name>=value CLI args.

To pass at runtime:

task build VERSION=1.2.3

For env vars (vs Task vars):

env:
  CGO_ENABLED: "0"
  GOOS: "linux"

tasks:
  build:
    env:
      GOARCH: "amd64"   # Task-scoped env
    cmds:
      - go build -o ./bin/app ./cmd/...

Env vars are exported to the shell that runs cmds. Task vars are template-expanded before the shell sees them.

Common Mistake: {{.PATH}} inside a cmd. Task’s templating happens first, replacing it with Task’s idea of PATH. If you want the shell’s $PATH, use $PATH (shell expansion):

cmds:
  - echo $PATH       # Shell expands this
  - echo {{.PATH}}   # Task tries to template-expand .PATH first

Fix 3: sources/generates for Incremental Builds

tasks:
  build:
    sources:
      - "**/*.go"
      - "go.mod"
      - "go.sum"
    generates:
      - "./bin/myapp"
    cmds:
      - go build -o ./bin/myapp ./cmd/...

Task computes a checksum of sources and stores it in .task/checksum/. If checksums match the last successful run and generates files still exist, the task is skipped.

To force a run:

task build --force

To check status without running:

task build --status
# Prints whether the task is up-to-date (exit 0) or stale (non-zero).

For tasks that don’t produce a file but should still run-once based on inputs:

tasks:
  install-deps:
    sources:
      - "package.json"
      - "package-lock.json"
    cmds:
      - npm install
    method: checksum  # or "timestamp" (faster, less reliable)

method: checksum (default) reads file contents — accurate but slower for many files. method: timestamp uses mtime — faster but breaks if your editor “touches” files.

Pro Tip: Pair sources with generates for outputs. If generates is empty, Task only knows whether sources changed, not whether the output exists. Without generates, deleting your ./bin/myapp doesn’t trigger a rebuild.

Fix 4: Dependencies Run in Parallel

deps: lists tasks to run before this one — in parallel:

tasks:
  release:
    deps: [test, lint, build]
    cmds:
      - ./scripts/release.sh

test, lint, and build run concurrently. release.sh runs only after all three succeed.

For sequential execution, use cmds: - task: <name>:

tasks:
  release:
    cmds:
      - task: test
      - task: lint
      - task: build
      - ./scripts/release.sh

The four steps run in order; any failure stops the chain.

For mixed parallel + sequential:

tasks:
  ci:
    deps: [test, lint]    # Parallel
    cmds:
      - task: build       # Sequential after deps
      - task: publish     # Sequential after build

Fix 5: Includes for Modular Taskfiles

For large projects, split into multiple Taskfiles:

# Taskfile.yml (root)
version: '3'

includes:
  frontend:
    taskfile: ./frontend/Taskfile.yml
    dir: ./frontend
  backend:
    taskfile: ./backend/Taskfile.yml
    dir: ./backend

tasks:
  build:
    deps: [frontend:build, backend:build]

dir: sets the working directory for tasks inside the included file. Without it, included tasks run from the root.

To run from the root:

task frontend:build
task backend:test

For internal-only tasks (not callable from root):

includes:
  internal:
    taskfile: ./scripts/Taskfile.yml
    internal: true

internal: true hides the included tasks from task --list output but they’re still callable internally.

Common Mistake: Forgetting dir: on an included file. Tasks inside it then run from the parent’s directory, breaking relative paths in commands.

Fix 6: Watch Mode

task build --watch

Watch re-runs the task whenever sources change. Without explicit sources, watch does nothing useful:

tasks:
  test:
    sources:
      - "**/*.go"
      - "**/*_test.go"
    cmds:
      - go test ./...
task test --watch
# Edits to any .go file trigger re-test.

For a persistent watch task, write a wrapper:

tasks:
  dev:
    desc: "Run tests in watch mode"
    cmds:
      - task: test
        vars: { WATCH: "1" }
    watch: true   # Forces watch even without --watch flag

  test:
    sources:
      - "**/*.go"
    cmds:
      - go test ./...

watch: true at the task level makes watch the default for that task.

Pro Tip: Combine watch with clear: true to wipe the terminal between runs:

tasks:
  dev:
    sources: ["**/*.go"]
    cmds:
      - clear
      - go test ./...
    watch: true

Fix 7: Preconditions and status

Block a task from running unless conditions are met:

tasks:
  deploy-prod:
    preconditions:
      - sh: "[ \"$ENV\" = \"production\" ]"
        msg: "Set ENV=production to deploy to prod"
      - sh: "git diff --quiet"
        msg: "Working tree is dirty. Commit changes first."
    cmds:
      - ./scripts/deploy.sh

preconditions run before the task. Each sh: is a shell command; non-zero exit blocks the task with the msg.

For “skip if already up-to-date” semantics (different from sources):

tasks:
  install:
    status:
      - test -f ./bin/myapp
    cmds:
      - go install ./cmd/...

status: runs each command; if all succeed (exit 0), the task is considered up-to-date and skipped.

status vs sources:

  • sources — fingerprint of inputs. Re-run when inputs change.
  • status — arbitrary check. Re-run when status check fails.

Use sources for build artifacts; status for things like “is this tool installed” or “is the file populated”.

Fix 8: Run-Once Semantics

For tasks that should run at most once per Task invocation (even if listed in multiple deps):

tasks:
  setup:
    run: once
    cmds:
      - ./scripts/setup.sh

  build:
    deps: [setup]
    cmds: [go build ./...]

  test:
    deps: [setup]
    cmds: [go test ./...]

  all:
    deps: [build, test]
task all
# setup runs once (not twice from build and test).

run: once makes Task deduplicate the task during the run.

For tasks that should always rerun:

tasks:
  log-deploy:
    run: always
    cmds: [echo "Deploy at $(date)"]

run: always opts out of the standard skip-if-up-to-date behavior.

Pro Tip: Default is run: when_changed (skip if sources/status say so). Override only when you need different semantics.

Still Not Working?

A few less-obvious failures:

  • Task seems to ignore my changes. Cache stuck. Clear: rm -rf .task/.
  • exec: "task": executable file not found. Not installed. Install: brew install go-task (macOS) / go install github.com/go-task/task/v3/cmd/task@latest.
  • Includes break with absolute paths in subtasks. Use ${PWD} from the Taskfile’s perspective, or define dir: on the include.
  • Variables don’t pass to included Taskfiles. Includes have their own var scope. To share: define in root and pass via vars: on the include:
includes:
  frontend:
    taskfile: ./frontend/Taskfile.yml
    vars: { ENV: "{{.ENV}}" }
  • silent: true doesn’t silence everything. It silences Task’s “task: [name] cmd” prefix, but the cmd itself still prints stdout. Use cmds: - cmd: ... silent: true for per-cmd silence.
  • Shell-specific syntax fails on Windows. Task uses sh by default — Windows doesn’t have it natively. Install Git Bash or WSL, or set set: -e and use cross-platform commands.
  • Long output gets truncated. Task buffers output by default. For streaming output (logs, watch), set output: prefixed in Taskfile.yml.
  • deps runs sequentially in dry-run. task --dry shows the plan in order even though execution is parallel. The parallel execution happens at runtime; dry-run just lists what would run.

For related build tool and automation issues, see Lefthook not working, Pre-commit not working, mise not working, and Bun shell not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles