Migrating from Taskfile.yml
A Taskfile gives your team an ad-hoc way to run build, test, and deploy tasks, defined in YAML. Atmos gives you the same capability as a native, documented feature: custom commands and workflows. Task and Atmos both use declarative YAML, so most of the mapping from one field to another is direct. Two Task features map to a dedicated field rather than plain steps, covered below. Your Terraform code and scripts do not need to change.
Install the atmos-migration skill so Claude Code, Cursor, GitHub Copilot, and other AI coding
assistants can apply this guide directly to your repository:
atmos ai skill install atmos-migration
See AI Agent Skills for details.
Key Concepts at a Glance
| Taskfile.yml concept | Atmos equivalent |
|---|---|
desc: | Command description: |
cmds: (list of shell commands) | steps: (type: shell, or type: atmos for any atmos command) |
deps: (runs at the same time by default) | Command dependencies.commands, concurrent by default |
vars: / env: | Command flags: (with default:) / env: map |
sources: / generates: (freshness check) | Step inputs.sources / artifacts.paths |
includes: (multi-file composition) | Auto-discovered atmos.d/*.yaml, or separate workflow files |
internal: true task | Command internal: true (weaker guarantee -- see below) |
deps: Becomes dependencies.commands
Task runs deps: at the same time by default. Atmos custom-command and workflow steps, in
contrast, run one after another by default — so a deps: entry is not a step and never becomes
one. It maps to the command-level
dependencies.commands
field, which resolves through the same DAG scheduler as
parallel/matrix needs: and
runs concurrently by default — matching Task's deps: behavior directly, not working around it:
- Before (Taskfile.yml)
- After (atmos.yaml)
tasks:
deploy:
desc: Plan and apply the given environment
deps: [test, lint]
cmds:
- terraform -chdir=terraform apply -var-file=envs/dev.tfvars
commands:
- name: deploy
description: Plan and apply the given environment
dependencies:
commands: [test, lint]
steps:
- type: atmos
command: terraform apply infra -s dev
infra is a placeholder Atmos component name, not the terraform verb repeated. Moving the old
terraform/ directory's .tf files to components/terraform/infra/ (the default
components.terraform.base_path is components/terraform) is one option -- swap infra for
whatever you actually name the component. Alternatively, keep the existing terraform/ directory
where it is: set components.terraform.base_path: "." and add metadata.component: terraform
on the infra stack component -- metadata.component points the stack component at the physical
directory, so no files need to move.
dependencies.commands also matches a behavior Task itself has that a hand-rolled parallel
step does not: if two commands both depend on the same one — for example both test and lint
depending on build — Atmos runs build exactly once and dedups it, the same as Task's own
deps: graph. A parallel step calling atmos build from two different places would run it
twice. If one dependency itself depends on another (lint depends on build, and deploy
depends on test and lint), declare that directly on lint's own dependencies.commands — the
scheduler resolves the whole transitive graph itself, still deduping build to a single run.
Reach for a parallel step instead of dependencies.commands only for concurrency inside a
single command's own steps, not between named commands — for example, running several shell
commands side by side that were never their own Task tasks to begin with.
sources:/generates: Becomes inputs/artifacts
Task skips a task's cmds: when its sources: files match its generates: outputs, checked by
default with a content hash (Task also supports method: timestamp for an mtime-based check).
The step-level inputs.sources and
artifacts.paths fields are the direct match, with the same
checksum-by-default/timestamp-as-an-option choice:
commands:
- name: build
description: Compile the deployable artifact
steps:
- type: shell
command: go build -o bin/handler ./cmd/handler
inputs:
sources: ["cmd/**/*.go"]
artifacts:
paths: ["bin/handler"]
With no explicit when:, declaring inputs/artifacts on a step is enough — it implicitly means
when: checksum.changed, and the step is skipped when the hash of the matched source files
matches the hash recorded after the last successful run. Run the same command twice in a row and
the second run skips build entirely; edit a matched source file and the next run executes it
again. See inputs for the full field reference, including the
timestamp.changed fact for an mtime-based check matching Task's method: timestamp mode.
require/assert is a different, older step type — it only
checks that a file, tool, or directory exists, not whether it is fresh, so it does not replace
inputs/artifacts.
The scope is different, too: Task's sources:/generates: gates the task's entire cmds:
list, but inputs/artifacts are declared per step — skipping one step does not stop later
steps in the same command from running. If a task has more than one cmds: entry and the
freshness decision must gate all of them together, combine them into a single shell/script
step rather than spreading inputs/artifacts across several migrated steps.
internal: true Tasks
Set internal: true on the custom command. The command still runs (atmos <name> ..., or as a
default: target, or from another command's steps), but it's excluded from atmos --help
listings and completion suggestions — matching how an internal: true task disappears from
task --list.
This is not the same guarantee Task gives you. In Task, internal: true also blocks direct
invocation — running an internal task by name from the CLI fails with an error; it is only
reachable from another task's deps:/cmds:. Atmos's internal: true maps to Cobra's Hidden
field: it removes the command from help output and shell completion, but the command still
executes if invoked directly (atmos <name> ...). If the user has a Task helper that must
genuinely be unreachable on its own — not just hidden from discovery — migrating it straight to
internal: true does not reproduce that constraint. Tell the user to audit any such helpers and,
if true unreachability matters, add their own guard inside the command (for example a
precondition or an early check on an expected caller-only flag/env var) rather than relying on
internal: true alone. Reserve inlining the logic into a caller's step for a helper that's
genuinely single-caller and has no reason to be invoked on its own.
Before and After
- Before (Taskfile.yml)
- After (atmos.yaml)
version: '3'
vars:
ENV: '{{.ENV | default "dev"}}'
tasks:
build:
desc: Compile the deployable artifact
cmds:
- go build -o bin/handler ./cmd/handler
sources:
- cmd/**/*.go
generates:
- bin/handler
test:
desc: Run unit tests
deps: [build]
cmds:
- go test ./...
lint:
desc: Run static analysis
cmds:
- golangci-lint run ./...
commands:
- name: build
description: Compile the deployable artifact
steps:
- type: shell
command: go build -o bin/handler ./cmd/handler
inputs:
sources: ["cmd/**/*.go"]
artifacts:
paths: ["bin/handler"]
- name: test
description: Run unit tests
dependencies:
commands: [build]
steps:
- type: shell
command: go test ./...
- name: lint
description: Run static analysis
steps:
- type: shell
command: golangci-lint run ./...
includes: Splits into Commands and Workflows
Task's includes: field joins several Taskfiles into one. Atmos has two matching methods.
Choose the one that fits what you are splitting:
- To split command definitions across files, put them in files such as
atmos.d/commands.yamlor.atmos.d/commands.yaml. Atmos auto-discoversatmos.d//.atmos.d/in the config directory (and, as a lower-priority fallback, at the git/worktree root) -- noimport:entry is needed for this specific location. Useimport:only when splitting across a directory Atmos does not auto-discover. See Imports. - To split multi-step chains, use separate workflow files. Atmos workflows already live one file
per purpose, under
workflows.base_path. Unlikeatmos.d/.atmos.d, there is no default forworkflows.base_path-- add it explicitly (for exampleworkflows.base_path: "stacks/workflows") the first time your migration reaches a workflow, oratmos workflow <name>fails with'workflows.base_path' must be configured in 'atmos.yaml'.
What You Gain
- A larger set of built-in step types. A Taskfile task runs shell commands only. Atmos adds
step types for orchestration (
parallel,matrix,wait), user prompts (confirm,choose,input), and output (table,markdown,toast), with more than 30 types in total. See Step Types for the full list. - Interactive commands. A step such as
confirmorchoosecan pause a command and ask the user a question. A Taskfile task cannot do this without a custom shell script. - Automatic tool installation. A command can list the tools it needs under
dependencies.tools. Atmos installs the correct version before the command runs. See Toolchain Configuration. Task has no built-in match for this. - One interface across every command.
task --listshows task names and descriptions. Atmos addsatmos <command> --helpwith the same flag and argument format for every command, in addition to the CLI-wideatmos --help.
Migration Checklist
- List every task. Mark each one as independent, or part of a
deps:chain - Turn
desc:/cmds:into commanddescription:/steps: - Turn any
deps:chain intodependencies.commands, to keep Task's concurrent-by-default behavior - Turn
vars:/env:into commandflags:(with defaults) andenv:maps - Turn
sources:/generates:into stepinputs.sources/artifacts.paths-- combine multiplecmds:entries into one step if the freshness decision must gate all of them together - Split
includes:into auto-discoveredatmos.d/*.yamlfiles, separate workflow files, or both - Mark
internal: truetasks asinternal: truecustom commands