Custom Commands and Workflows Are Now a Complete Task Runner Replacement
If you've ever tried to move a Taskfile.yml over to Atmos, you've hit the gap: deps: had no
clean equivalent in custom commands, sources:/generates: up-to-date checking didn't exist at
all, and a failed lint step stopped your whole release pipeline even when you just wanted to see
every check's result. So teams ended up running two tools side by side — go-task for the parts
Atmos couldn't do, Atmos for everything else — instead of one.
The Problem
Atmos workflows and custom commands already covered most of what a task runner needs: steps, templating, conditionals, parallel execution. But a handful of real gaps kept people from fully retiring go-task:
- No dependency ordering between named commands or workflows. You could make steps within
one command run in parallel, but you couldn't say "run
buildbeforetestandlint, and don't runbuildtwice just because two things depend on it." - No up-to-date checking. go-task's
sources:/generates:skip a task when nothing has changed. Atmos had nothing like it — every step ran every time, even a slow compile step whose inputs hadn't changed since the last successful run. - No continue-on-error. A single failing step stopped everything downstream, even for steps — like a linter — where you'd rather collect every result and report at the end.
- No precondition shortcut. Skipping an install step when a tool is already on
PATHmeant hand-rolling a shell check. - Custom commands couldn't use
parallel/matrixat all. The migration guide's own suggested workaround — wrap dependents in aparallelstep withneeds:— silently failed on custom commands; it only worked in workflows.
The Fix
Custom commands and workflows now cover all of it, using the same when:/CEL condition engine and
scheduler Atmos already had — no second, bespoke mechanism bolted on.
Dependencies between commands and workflows
commands:
- name: build
steps: [...]
- name: test
dependencies:
commands: [build]
steps: [...]
- name: lint
dependencies:
commands: [build]
steps: [...]
- name: release
dependencies:
commands: [test, lint]
steps: [...]
build is declared as a dependency of both test and lint, but it only runs once — the graph
dedups identical dependency invocations automatically. Dependencies run concurrently by default.
Need the same command with different inputs? Parameterize it:
dependencies:
commands:
- name: build
flags: { env: dev }
- name: build
flags: { env: prod }
Both invocations run — different parameters mean different graph nodes. Workflows get the same
dependencies.workflows, including cross-file references via file:.
Skip steps that are already up to date
steps:
- name: compile
inputs:
sources: ["cmd/**/*.go", "go.sum"]
artifacts:
paths: ["bin/handler"]
command: go build -o bin/handler ./cmd/handler
No extra configuration needed — declaring inputs/artifacts alone means "skip this step unless
sources changed since the last successful run." Run it twice in a row and the second run skips
entirely. Power users can reference the underlying facts directly (checksum.changed,
timestamp.changed, or the raw per-file sources/artifacts records) for custom logic.
Skip a step when a tool is already installed
steps:
- name: install-stringer
preconditions:
tools: ["stringer"]
command: go install golang.org/x/tools/cmd/stringer@latest
Resolved via Go's exec.LookPath — no shell, so it works identically on Linux, macOS, and
Windows.
Continue past a failing step
steps:
- type: shell
command: golangci-lint run ./...
continue: always
- type: atmos
command: terraform apply vpc -auto-approve
continue: always mirrors GitHub Actions' continue-on-error: the step's own failure is still
visible, later steps still run, and the overall exit status is unaffected.
parallel/matrix now works in custom commands
The exact recipe that used to only work in workflows now works identically in custom commands:
commands:
- name: release
steps:
- type: parallel
steps:
- name: test
command: go test ./...
- name: lint
needs: [test]
command: golangci-lint run ./...
Also Shipped
A handful of smaller gaps closed alongside the above:
platformsviawhen:—when: "os == 'darwin'"instead of a dedicated field, reusing facts already available everywhere elsewhen:is.- Native command aliases —
aliases: [dep, d]on a custom command, registered in-process, distinct from the top-level subprocess-redirectaliases:config. internal: true— hide a command fromatmos help/atmos listwhile leaving it fully invocable, for commands meant only to be run as someone else's dependency.values:on flags and arguments — restrict a flag to a fixed set of choices, with static validation and an interactive picker when a required value is missing.
How to Use It
All of this is available today in custom commands and workflows — no flags to enable, no config
migration required. See dependencies,
inputs, artifacts,
preconditions, and continue for
the full field references, or the Alternatives page for how Atmos
compares to go-task more broadly.
Get Involved
If you're still running go-task alongside Atmos for one of these reasons, we'd like to hear about it — open a discussion on GitHub Discussions and tell us what's still missing.
