Task (go-task) (Polyglot)
A single-binary, YAML-configured generic task runner written in Go — "a fast, cross-platform build tool inspired by Make" — that gives a polyglot monorepo a DAG of deps, file-fingerprinted up-to-date checks, and includes-based namespacing, but deliberately stops short of being a package manager or a workspace resolver.
| Field | Value |
|---|---|
| Language | Go (the binary); workspace config in YAML (Taskfile.yml) |
| License | MIT (Copyright 2016 Andrey Nering and contributors) |
| Repository | go-task/task |
| Documentation | taskfile.dev · schema reference · CLI reference |
| Category | Generic Task Runner |
| Workspace model | No native workspace concept; a "monorepo" is a root Taskfile.yml that includes sub-Taskfiles as namespaces |
| First released | v1 (Feb 2017); the schema-"3" line (v3.0.0) in 2020 |
| Latest release | v3.51.1 (May 16, 2026) |
Latest release:
v3.51.1, published May 16, 2026 (releases). Task is still on the schema-version: '3'line it has carried since 2020; the3.xminor stream adds features without breaking the Taskfile format. Recent3.5xwork focused on Remote Taskfiles hardening (TLS / mTLS via--cacert/--cert/--cert-key, a--trusted-hostsflag andremote.trusted-hostsconfig), templating performance (skipping Go-template evaluation for static strings), and new template functions (absPath,joinEnv,joinUrl). Source citations below are againstmainand the official docs as of June 5, 2026.
Overview
What it solves
Task occupies the niche its README states plainly (README.md):
"A fast, cross-platform build tool inspired by Make, designed for modern workflows."
Where Make uses a tab-sensitive DSL, a single global namespace, and mtime-only staleness, Task keeps Make's two good ideas — a target depends on other targets and don't redo work whose inputs are unchanged — and rebuilds the surface in declarative YAML with a single statically-linked Go binary, "zero dependencies," that runs the same Taskfile.yml on Linux, macOS, and Windows. It is squarely a member of the generic task runner family alongside just, mise, and Make: it orchestrates commands, not packages. It has no dependency resolver, no lockfile, no package store, and no remote-execution backend — capabilities owned by the language-specific package managers (Cargo, uv, pnpm) and the heavyweight build engines (Bazel, Buck2) elsewhere in this survey.
For a monorepo, that scoping is the whole point. Task is the glue layer: a root Taskfile.yml that includes each sub-package's Taskfile.yml under a namespace, declares which library builds must precede which application build via deps, and skips any leg whose sources haven't changed. The actual dependency installation in each member is delegated to that member's native tool (go build, cargo build, npm ci, dub build). In that respect Task is to a polyglot monorepo what moon is — minus moon's project graph, content- addressable cache, and toolchain manager.
Design philosophy
Three principles shape the tool, all visible in the config surface and the docs:
Convention over a build language. A Taskfile is data (YAML), not a Turing-complete build language like Bazel's Starlark or Buck2's. The most a task does dynamically is run shell via
mvdan/sh(an embedded Go shell interpreter, soTaskfiles are cross-platform withoutbash) and interpolate Gotext/templatevariables. There is no rule abstraction, no providers, no aspects.Incremental, file-based correctness — not hermeticity. Task decides whether to skip a task by fingerprinting declared
sourcesagainst declaredgenerates(a checksum or a timestamp), exactly Make's contract made explicit and content-aware. It makes no attempt to sandbox a task or capture undeclared inputs; an unlisted input that changes will silently produce a stale "up to date." This is the same correctness ceiling as moon's declared-inputs model, and well below Bazel's sandboxed actions.A single binary, zero install ceremony. The entire tool is one Go executable; a Taskfile needs no
node_modules, no JVM, no plugin install. This is the ergonomic axis on which it beats Nx / Turborepo (which assume a Node toolchain) and the JVM build tools (Gradle, Maven, sbt).
Within this survey Task is the canonical "minimal, polyglot, file-fingerprinting generic runner" data point: compare it against just (a command runner with no up-to-date checks or DAG-level change detection), Make (its mtime-only ancestor), and moon / Turborepo (which add the project graph, remote cache, and affected-detection Task omits).
How it works
A Task project is anchored by a Taskfile.yml (or .yaml; a Taskfile.dist.yml committed-defaults variant is also recognised). The top of the file declares the schema version, optional global vars / env / dotenv, optional includes, and a tasks map. Each task carries cmds (what to run), optional deps (what must run first), and optional sources / generates (the fingerprint inputs and outputs).
# Taskfile.yml
version: '3'
vars:
BIN: ./bin/app
tasks:
build:
deps: [assets] # runs concurrently before build
sources:
- '**/*.go'
generates:
- '{{.BIN}}'
cmds:
- go build -o {{.BIN}} .
assets:
sources:
- 'assets/**/*'
generates:
- 'public/bundle.css'
cmds:
- npm run build:cssRunning task build resolves the DAG, runs assets first (because builddeps on it), then runs build — but only the legs whose sources checksum differs from the cached value in .task/ actually execute. The data model is a flat ast.Task struct (taskfile/ast/task.go) carrying exactly these fields: Cmds []*Cmd, Deps []*Dep, Sources []*Glob, Generates []*Glob, Status []string, Preconditions []*Precondition, Method string, Run string, Dir string, and — populated only during the merge of included files — Namespace string and FullName string.
The short forms
For trivial tasks Task accepts terse syntax — a bare string is the single cmd:
tasks:
hello: echo "Hello, World!" # == { cmds: ['echo "Hello, World!"'] }Calling tasks from tasks (sequential vs. parallel)
deps are parallel prerequisites; to force an order you call a task from within cmds using the task: form, which runs synchronously in sequence:
tasks:
release:
cmds:
- task: build # runs to completion first
- task: package
vars: { TARGET: 'linux' } # pass vars to the called taskThis deps-are-parallel / cmds-task:-are-sequential split is the core scheduling primitive and the most common source of confusion (see Weaknesses).
Workspace declaration & topology
Task has no first-class workspace, project, or member concept — and this is the most important fact about it for this survey. There is no [workspace] block, no members = [...] glob, no project graph. A "monorepo" is expressed entirely through the includes mechanism: a root Taskfile.yml pulls in each sub-package's Taskfile.yml and exposes its tasks under a namespace (guide):
# repo-root Taskfile.yml
version: '3'
includes:
docs: ./documentation # tasks become `docs:<name>`
api:
taskfile: ./services/api/Taskfile.yml
dir: ./services/api # run api's tasks in their own dir
lib:
taskfile: ./libs/core
internal: true # not exposed on the CLI / --listThe included file's tasks are addressed as docs:build, api:test, etc. The Include schema (schema) carries the knobs that make this usable as a monorepo topology:
includes key | Effect |
|---|---|
taskfile | Path to the included Taskfile.yml or to a directory containing one |
dir | Working directory for the included tasks (defaults to the caller's dir, not the includee's) |
internal | Hide the namespace's tasks from --list and direct CLI invocation (used for library internals) |
flatten | Merge the included tasks into the root namespace with no prefix |
optional | Don't error if the file is missing |
aliases | Alternative namespace names |
excludes | Specific tasks to drop from the include |
vars | Variables injected into the included Taskfile |
checksum | An expected file checksum (pins a remote/included file's contents) |
IMPORTANT
Discovery is explicit, never glob-based. Unlike Cargo's members = ["crates/*"], pnpm's packages: globs, or moon's projects globs, Task's includes map enumerates every member by hand — there is no includes: ["packages/*"] wildcard. Adding a sub-package to the monorepo means adding a line to the root Taskfile.yml. This is the single largest gap between Task and a true workspace tool, and the reason community threads repeatedly ask for "an idiomatic root Taskfile for a monorepo" (discussion #1517).
NOTE
A subtle footgun: by default an included Taskfile's tasks run in the directory Task was invoked from, not in the directory of the included file. Monorepo setups must set dir: (per-include) or dir: (per-task) so each member's commands resolve paths relative to that member, mirroring how moon anchors tasks to their project root automatically.
Dependency handling & isolation
This dimension barely applies — and saying so plainly is the honest answer for a generic task runner. Task does not resolve packages, does not hoist, symlink, or maintain a virtual store, and has no lockfile. Each member's real dependencies are installed by that member's native package manager, which Task merely invokes:
Dependency installation is a delegated
cmd. A member'sTaskfile.ymlhas aninstall(ordeps) task that shells out togo mod download,cargo fetch,npm ci,uv sync, ordub upgrade. Task's job is to order these and skip the unchanged ones, not to perform resolution. The isolation model is therefore entirely whatever the underlying manager provides.There is no
workspace:-protocol equivalent. Task has no notion of one member depending on a sibling member's package. Cross-member ordering is expressed purely as task ordering — "buildlibbeforeservice" — viaincludes+deps:yaml# services/api/Taskfile.yml version: '3' includes: core: ../../libs/core # the sibling library tasks: build: deps: - core:build # build the library member first cmds: - go build ./...The dependency on the sibling is a task edge, not a package edge: Task guarantees
core:buildruns beforeapi:build, but the linkage ofapiagainstcore's artifact is the language toolchain's concern (areplacedirective ingo.mod, apath =dependency inCargo.toml, apath=indub.sdl).Variable inheritance is the only "config inheritance." A root
Taskfile'svarsanddotenvpropagate down to includes (and an include can override viaincludes.<ns>.vars), giving a lightweight version of the centralized-config story that Cargo's[workspace.package]inheritance provides — but for shell variables, not dependency versions.
WARNING
Because the .task/ checksum cache is per-directory, a library included by two services and built in each service's dir is fingerprinted and built twice — its cache is not shared across the two include sites (issue #852). Task has no global, content-addressed artifact store to deduplicate this the way moon's CAS or Bazel's does.
Task orchestration & scheduling
This is where Task earns the "build tool" half of its tagline.
The DAG. When Task runs, it merges every included Taskfile into one ast.Taskfile (namespaces flattened into qualified names), then compiles the requested task plus its transitive deps into a directed acyclic graph. deps edges are the prerequisites; cmds with a task: reference are sequential sub-invocations. Cyclic deps are detected and rejected.
Concurrency. deps of a single task run in parallel by default — the docs are explicit (guide):
"Dependencies run in parallel, so dependencies of a task should not depend one another."
Across top-level targets, task a b c runs them sequentially unless --parallel / -p is given; --concurrency / -C N caps the total number of concurrently executing tasks (0 = unlimited). To avoid a diamond-shaped DAG running a shared dependency multiple times within one invocation, a task sets run: once (the other run modes are always, the default, and when_changed, which re-runs once per distinct variable set).
Change detection — fingerprinting. A task with sources declared is skipped when its fingerprint is unchanged. The method chooses the algorithm (schema, internal/fingerprint/):
method | Mechanism | Stored in |
|---|---|---|
checksum | (default) SHA-256 over the contents of all resolved sources globs; compared to a saved digest | .task/checksum/ |
timestamp | Compares the newest sources mtime against the oldest generates mtime (Make-style) | .task/timestamp/ |
none | No fingerprint; the task always runs | — |
The documented contract (guide):
"When given, Task will compare the checksum of the source files to determine if it's necessary to run the task. If not, it will just print a message like
Task "js" is up to date."
Implementation-wise, internal/fingerprint is a small strategy hierarchy (sources_checksum.go, sources_timestamp.go, sources_none.go, all behind a checker.go interface), with status.go and the status: task field providing an escape hatch: status is a list of shell commands whose exit code decides freshness (exit 0 ⇒ up to date), letting a task define "am I stale?" with arbitrary logic when file globs aren't enough (e.g. "does this Docker image tag already exist in the registry?"). preconditions is the logical inverse — commands that, on non-zero exit, fail the task and everything depending on it.
Watch mode. task --watch / -w (or watch: true on a task) re-runs a task whenever its sources change, polling at --interval (default 5s). This is a local dev-loop feature, not a build-server primitive.
IMPORTANT
Task has no Git-aware affected-detection. There is nothing like moon's / Turborepo's / Nx's --affected <ref> that computes "which members changed since main." Task's only change detection is the per-task sources fingerprint. Restricting a monorepo run to "what changed" must be hand-built — e.g. a task whose status shells out to git diff --quiet HEAD~1 -- <dir>.
Caching & remote execution
Task's caching is local-only and skip-only, fundamentally different from the artifact-replay caches elsewhere in this survey:
What is cached is a fingerprint, not an artifact. The
.task/directory stores source checksums/timestamps, not thegeneratesoutputs. On a cache "hit," Task skips re-running the command and leaves the existing output files in place; on a miss it re-runs and rewrites the fingerprint. It never archives, restores, or transports build outputs. Contrast Turborepo / moon / Bazel, which store the outputs keyed by hash and replay them (including stdout/stderr) — so a cache hit on a fresh checkout reconstructs artifacts that were never built locally. Task cannot do that: a fresh checkout has nogeneratesoutputs, so every task runs.No remote cache, no REAPI, no remote execution. There is no content-addressable store, no Bazel Remote-Execution-API client, no shared-team cache. The
.task/directory is per-checkout and not designed to be shared (and.gitignored in practice).Remote Taskfiles ≠ remote cache. Task does ship a Remote Taskfiles experiment, but it is about fetching the configuration, not caching build results. A Taskfile can
includeanhttp(s)://orgit::URL, or be the entrypoint via--taskfile <url>(remote-taskfiles). The security model is trust-on-first-use plus checksum-change detection:"Whenever you run a remote Taskfile, Task will create and store a checksum of the file that you are running. If the checksum changes, then Task will print another warning to the console to inform you that the contents of the remote file has changed."
and on caching the fetched file:
"Whenever you run a remote Taskfile, the latest copy will be downloaded from the internet and cached locally."
Relevant flags:
--download(force a fresh fetch),--offline/TASK_OFFLINE(use only the cached copy),--clear-cache(purge),--expiry(cache TTL, default immediate), plus--yes/--trust/--trusted-hostsand the TLS options (--cacert,--cert,--cert-key) for the trust prompt.
NOTE
The practical consequence for a monorepo: Task's incrementality is real on a warm checkout (don't rebuild what didn't change) but evaporates on CI, where every run is a fresh clone with an empty .task/. Teams that want cross-machine caching pair Task with an external layer — a CI artifact cache keyed on the same source globs, or simply reach for moon / Turborepo / Bazel instead.
CLI / UX ergonomics
Task's command boundary is task [flags] [task...] [VAR=value...]. There is no global-vs-targeted split to learn: you name the namespaced task(s) you want, and pass CLI variables as trailing KEY=value pairs.
| Invocation | Meaning |
|---|---|
task build | Run the build task (and its deps) in the current Taskfile |
task docs:serve | Run the serve task from the docs include namespace |
task build test | Run build then test sequentially |
task -p build test | Run them in parallel (--parallel) |
task build VERSION=1.2.0 | Pass VERSION as a CLI variable (highest precedence) |
task --list / -l | List documented tasks (those with desc) |
task --list-all / -a | List all tasks, including undocumented |
task --status build | Exit non-zero if build is not up to date (no execution; CI gate) |
task --force / -f | Ignore fingerprints; run even if up to date |
task --dry / -n | Print the commands without running them |
task --watch / -w | Re-run on sources change |
task -d <dir> / -t <file> | Point at a non-default working dir / Taskfile |
task -g <task> | Run from the global Taskfile in $HOME (cross-project helpers) |
task -C N | Cap concurrency at N |
task -o group | Buffer each task's output into a labelled group (vs. interleaved default) |
Namespacing gives the monorepo ergonomics: task --list from the root shows api:build, docs:serve, lib:test, … and task api:test docs:build runs a hand-picked subset. The selection model is explicit enumeration — there is no --filter <glob>, no -p <project> package selector, no --affected/--since graph slicing. To "test everything," authors write an aggregator task that deps-on every member's test task, or that loops with the for: matrix construct over a list. Compare Turborepo's --filter, pnpm's --filter, moon's :task broadcast + --query, or Cargo's -p / --workspace — none of which Task offers natively.
NOTE
Task's ergonomic win is the inverse of its filtering weakness: a contributor needs to learn nothing beyond task --list and task <name>. There are no graph flags, no project selectors, no remote-cache config. For small-to-medium monorepos that simplicity is the feature; for large ones the lack of --affected/--filter becomes the bottleneck.
Strengths
- One static binary, zero runtime deps. No Node, JVM, or plugin install; the same
Taskfile.ymlruns identically on Linux, macOS, and Windows (commands run through the embeddedmvdan/shinterpreter, not a system shell). - Genuinely polyglot glue. Because every member's real build is just a
cmd, Go, Rust, Python, PHP, D, and shell coexist with no language assumptions — a natural fit for a heterogeneous monorepo's outer loop. - Make's good ideas, made declarative. A real DAG with parallel
deps, plus content-awaresources/generatesfingerprinting (checksum/timestamp), beats Make's tab-DSL and mtime-only staleness while keeping the model small. - Low learning curve / incremental adoption.
task --list+task <name>is the entire UX; a repo can wrap its existing scripts one task at a time. - Flexible escape hatches.
status:(custom freshness check),preconditions:,requires:(mandatory vars),for:matrix loops, anddefer:cleanups cover cases pure file-globs can't. - Namespacing via
includesgives a workable per-member structure andinternal/flatten/aliasesto shape the public task surface.
Weaknesses
- No workspace model. Members are enumerated by hand in
includes; there is no glob discovery of sub-packages, no project graph, no member metadata. - No dependency resolution, no lockfile, no isolation. Task orders and skips commands; everything package-related is delegated to the native managers, and cross-member linkage is a task edge, not a package edge.
- Cache is skip-only and local-only. It stores fingerprints, not outputs; no artifact replay, no remote/shared cache, no REAPI, no remote execution — so incrementality vanishes on a fresh CI checkout.
- No Git-aware affected-detection. No
--affected/--since; "run only what changed across the repo" must be hand-built withstatus:+git diff. - No graph filtering ergonomics. No
--filter/-p/--query; task selection is explicit enumeration plus aggregator tasks. deps-parallel vscmds-task:-sequential is a recurring footgun, as is the default that included tasks run in the caller's directory.- Double-build of shared includes: a library included by two members is fingerprinted and built per-site (
.task/is not shared) (issue #852).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Declarative YAML, not a build language (Starlark, etc.) | Low ceremony; data is diffable and toolable; no DSL to learn | No abstraction/rules/providers; logic must live in shell cmds or status hatches |
Single Go binary + embedded mvdan/sh shell | Zero install deps; truly cross-platform without a system bash | Shell semantics differ subtly from real bash; advanced shell features unavailable |
includes namespaces instead of a workspace/project model | Reuses one mechanism for "split a Taskfile" and "compose a monorepo" | Members enumerated by hand (no glob); no project metadata, graph, or member discovery |
deps run in parallel; sequencing via cmds: [{task: …}] | Maximizes concurrency by default; explicit order only where needed | Surprising to newcomers; ordered prerequisites are verbose; diamond deps need run: once |
sources/generates fingerprint (checksum default) | Content-aware "is it up to date?" beats Make's mtime; deterministic skips | Not hermetic — undeclared inputs cause stale hits; per-dir cache double-builds shared includes |
| Cache stores fingerprints, not artifacts; local only | Tiny, dependency-free, no cache server to run | No artifact replay; no remote/shared cache; incrementality lost on fresh CI checkouts |
| No package resolver / lockfile (delegate to native managers) | Stays a runner; polyglot by construction; no resolver to maintain | No unified lockfile, no isolation, no workspace:-protocol; version drift is the user's problem |
status/preconditions/requires escape hatches | Arbitrary freshness/guard logic when file globs are insufficient | Pushes correctness onto hand-written shell; easy to get subtly wrong |
Explicit task enumeration on the CLI (no --filter/--affected) | Trivial mental model: task --list + task <name> | Doesn't scale to large graphs; "run what changed" / subset selection must be hand-built |
Sources
- go-task/task — GitHub repository (source for the quoted README tagline; MIT, written in Go)
- taskfile.dev — official documentation · Guide (deps-parallel, up-to-date/checksum quotes)
- Taskfile schema reference — top-level /
Include/Task/Cmdkeys,method&runenums - CLI reference —
--parallel/-p,--concurrency/-C,--force,--watch,--status,--list taskfile/ast/task.go— theast.Taskstruct fields (Deps,Sources,Generates,Namespace, …)internal/fingerprint/—sources_checksum.go/sources_timestamp.go/sources_none.gostrategies- Remote Taskfiles experiment — remote include trust model, checksum-change warning,
--offline/--download - Releases —
v3.51.1(May 16, 2026) - Issue #852: shared-include cache not shared · Discussion #1517: root Taskfile for a monorepo
- Related: Make · just · mise · moon · Turborepo · Nx · Cargo · uv · pnpm · Bazel · Buck2 · Gradle · Maven · sbt · D landscape