Migrating Tasks from Taskfile.yml
Use Atmos as a task runner for the build, test, lint, release, and maintenance tasks your team currently runs with Task (go-task). This guide maps Taskfile tasks, dependencies, variables, and freshness checks to Atmos custom commands and workflows.
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.
Start with Your Existing Tasks
Task-runner adoption starts with an atmos.yaml file in your project. Stacks, components,
Terraform, and cloud credentials are optional; none are required for the tasks in this guide.
The examples use a Go application, but the same mapping works for other languages, documentation
builds, release scripts, and local development tools.
You can migrate one task at a time. To start, expose an existing task through Atmos:
Run atmos build from the project directory. When you are ready, replace task build with
the task's own commands. The examples below show that next step. They assume your existing
application, scripts, and required tools are available.
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: Deploy the application to the selected environment
vars:
ENV: '{{.ENV | default "dev"}}'
deps: [test, lint]
cmds:
- ./scripts/deploy.sh "{{.ENV}}"
commands:
- name: deploy
description: Deploy the application to the selected environment
flags:
- name: env
default: "dev"
dependencies:
commands: [test, lint]
steps:
- type: shell
command: ./scripts/deploy.sh "{{ .Flags.env }}"
The Task ENV variable becomes the Atmos --env flag. atmos deploy passes the default
dev to your script; atmos deploy --env staging passes staging. The matching Task commands
are task deploy and task deploy ENV=staging.
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 the build step; 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. Review any such helpers and,
if direct invocation must be blocked, add a 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'
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:
- go vet ./...
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: go vet ./...
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
- Discoverable commands. Add descriptions, typed flags, defaults, and per-command help for
your team's tasks with
atmos --helpandatmos <command> --help. - Typed steps. Combine shell commands with HTTP requests, containers, parallel execution, matrices, and other step types.
- Interactive workflows. Add prompts, confirmations, and structured output where your tasks need them.
- Tool dependencies. Declare required CLI versions with
dependencies.toolsso Atmos installs them before running the command. See Toolchain Configuration.
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