Migrating from Taskfile.yml (go-task)
This guide shows how to move Task's tasks to Atmos. Find the correct shape for the Taskfile below. Then follow the matching steps. For the full tutorial, see atmos.tools/migration/taskfile.
Task and Atmos both use declarative YAML. This makes the migration mostly mechanical. It is the simplest of the three task-runner migrations this skill covers. If the Taskfile also selects a Terraform environment, also use from-native-terraform.md for the Terraform-specific steps.
Find the Shape of the Taskfile
| Shape | Steps |
|---|---|
Simple task (desc/cmds) | Shape A |
Task with deps: (parallel by default) | Shape B |
vars:/env: and sources:/generates: | Shape C |
includes: (multi-file composition) | see Common Problems |
deps: maps to command-level dependencies.commands (concurrent by default, deduped -- the
direct match). sources:/generates: maps to step-level inputs.sources/artifacts.paths
(implicit when: checksum.changed -- the direct match). Neither is a gap; both need the user to
add the matching field during migration, since neither carries over automatically.
Shape A: Simple Tasks
Before:
version: '3'tasks:build:desc: Compile the deployable artifactcmds:- go build -o bin/handler ./cmd/handlerlint:desc: Run static analysiscmds:- golangci-lint run ./...
Steps:
- Turn
desc:into the command'sdescription:field. - Turn each entry in
cmds:into atype: shellstep. If the line is a native Atmos verb, such asterraform planorterraform apply, use atype: atmosstep instead.type: atmosis reserved for native Atmos verbs only. If the line calls another custom command (for exampleatmos build), keep it as atype: shellstep withcommand: atmos build-- do not usetype: atmosfor that. - Set
internal: trueon a command created from aninternal: truetask. It runs normally (atmos <name> ..., as adefault:target, or from another command's steps) but is excluded fromatmos --helplistings and completion suggestions. Only inline the task's body into a caller's step when it is genuinely single-caller logic with no reason to be invoked on its own.
commands:- name: builddescription: Compile the deployable artifactsteps:- type: shellcommand: go build -o bin/handler ./cmd/handler- name: lintdescription: Run static analysissteps:- type: shellcommand: golangci-lint run ./...
Shape B: Task Dependencies (Parallel by Default)
Before:
tasks:test:desc: Run unit testsdeps: [build]cmds:- go test ./...deploy:desc: Plan and apply the given environmentdeps: [test, lint]cmds:- terraform -chdir=terraform apply -var-file=envs/dev.tfvars
Task runs deps: at the same time by default. Atmos custom-command and workflow steps 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 with a hand-built parallel step:
commands:- name: deploydescription: Plan and apply the given environmentdependencies:commands: [test, lint]steps:- type: atmoscommand: terraform apply infra -s dev
infra is a placeholder Atmos component name, not the terraform verb repeated. Move the
task's Terraform code to components/terraform/infra/ (the default
components.terraform.base_path is components/terraform), then swap infra for whatever the
user actually names the component.
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.
Shape C: Variables and Up-to-Date Checks
Before:
vars:ENV: '{{.ENV | default "dev"}}'tasks:build:desc: Compile the deployable artifactcmds:- go build -o bin/handler ./cmd/handlersources:- cmd/**/*.gogenerates:- bin/handler
Steps:
- Turn
vars: ENV: '{{.ENV | default "dev"}}'into a commandflags:entry withdefault: "dev". Task's Sprigdefaultfilter becomes the plaindefault:field. - Turn
env:into anenv:map. The two are almost identical.
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: builddescription: Compile the deployable artifactsteps:- type: shellcommand: go build -o bin/handler ./cmd/handlerinputs: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. This does not carry over on its own --
add inputs/artifacts to the migrated step yourself, matching the Taskfile's own
sources:/generates: lists. The require/assert step type 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.
Common Problems
includes: (multi-file composition)
Task's includes: field combines several Taskfiles into one. Atmos has two matching methods.
Pick the one that fits the content being split:
- 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 the user's migration reaches a workflow, oratmos workflow <name>fails with'workflows.base_path' must be configured in 'atmos.yaml'.
sources/generates maps to a different field than steps
See Shape C above. It does not carry over
automatically -- the user must add inputs/artifacts to the migrated step themselves. State
that directly. Do not gloss over it, and do not claim it "just works" without the field.
What Not To Do
- Do not drop
sources:/generates:without adding the matchinginputs/artifactsfields to the migrated step. It is a direct match, not a gap, but it does not carry over on its own. - Do not turn
deps:into plain sequential steps, or into a hand-builtparallelstep, without first considering command-leveldependencies.commands-- it is the direct match: concurrent by default, and it dedups a dependency shared by more than one command the same way Task's owndeps:graph does. - Do not describe
require/assertas a freshness or caching check. It only checks that something exists. - Do not turn every
internal: truetask into its own discoverable command by default. If it is called from only one task, inline it into that caller's step. If it needs to be called from more than one task, or invoked directly for debugging, make it aninternal: truecustom command.