Skip to main content

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.

Using an AI Coding Assistant?

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 conceptAtmos 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 taskCommand 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:

tasks:
deploy:
desc: Plan and apply the given environment
deps: [test, lint]
cmds:
- terraform -chdir=terraform apply -var-file=envs/dev.tfvars

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

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 ./...
atmos.yaml
commands:
- name: deploy
description: Plan and apply the given environment (default dev)
flags:
- name: env
default: "dev"
atmos deploy --env staging

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.yaml or .atmos.d/commands.yaml. Atmos auto-discovers atmos.d//.atmos.d/ in the config directory (and, as a lower-priority fallback, at the git/worktree root) -- no import: entry is needed for this specific location. Use import: 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. Unlike atmos.d/.atmos.d, there is no default for workflows.base_path -- add it explicitly (for example workflows.base_path: "stacks/workflows") the first time your migration reaches a workflow, or atmos 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 confirm or choose can 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 --list shows task names and descriptions. Atmos adds atmos <command> --help with the same flag and argument format for every command, in addition to the CLI-wide atmos --help.

Migration Checklist

  • List every task. Mark each one as independent, or part of a deps: chain
  • Turn desc:/cmds: into command description:/steps:
  • Turn any deps: chain into dependencies.commands, to keep Task's concurrent-by-default behavior
  • Turn vars:/env: into command flags: (with defaults) and env: maps
  • Turn sources:/generates: into step inputs.sources/artifacts.paths -- combine multiple cmds: entries into one step if the freshness decision must gate all of them together
  • Split includes: into auto-discovered atmos.d/*.yaml files, separate workflow files, or both
  • Mark internal: true tasks as internal: true custom commands

Common Questions