Migrating from Justfiles
This guide shows how to move Just recipes to Atmos. Find the correct shape for the Justfile below. Then follow the matching steps. For the full tutorial, see atmos.tools/migration/justfile.
Just recipe bodies do not require tab indentation, unlike Make. Just's named parameters with
default values map closely to Atmos custom command flags: and arguments:. This is the
closest match of the three task runners this skill covers. If the Justfile also selects a
Terraform environment, also use from-native-terraform.md for the
Terraform-specific steps.
Find the Shape of the Justfile
| Shape | Steps |
|---|---|
| Recipes with named parameters and default values | Shape A |
Recipe dependencies (build: test) | Shape B |
set dotenv-load, export VAR := ..., set shell := [...] | Shape C |
Shape A: Recipes with Named Parameters
Before:
# Build the deployable artifactbuild:go build -o bin/handler ./cmd/handler# Run static analysislint:golangci-lint run ./...[private]_clean:rm -rf bin/
Steps:
- Turn the
# commentabove a recipe into the command'sdescription:field. Atmos shows this text inatmos --helpandatmos <command> --help. This replacesjust --list. - Turn a recipe's named parameter with a default value, such as
deploy env='dev':, into a commandflags:entry with a matchingdefault:value. Inside a step, read the value as{{ .Flags.env }}. Do not use Just's own{{env}}syntax. See Common Problems below. - Set
internal: trueon a command created from a[private]recipe. 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 recipe'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: Build the deployable artifactsteps:- type: shellcommand: go build -o bin/handler ./cmd/handler- name: lintdescription: Run static analysissteps:- type: shellcommand: golangci-lint run ./...
Shape B: Recipe Dependencies
Before:
# Run tests (builds first)test: buildgo test ./...# Deploy to the given environment (defaults to dev)deploy env='dev': build testcd terraform && terraform apply -var-file=envs/{{env}}.tfvars
Steps: use the same method as Make's dependency chains. See
from-makefile.md Shape B. Use a
type: atmos step with command: build to call another custom command -- type: atmos preserves
step-level stack context and structured output handling, which a type: shell step running
atmos build does not.
commands:- name: testdescription: Run tests (builds first)steps:- type: atmoscommand: build- type: shellcommand: go test ./...- name: deploydescription: Deploy to the given environment (defaults to dev)flags:- name: envshorthand: edefault: "dev"steps:- type: atmoscommand: test- type: atmoscommand: terraform apply infra -s {{ .Flags.env }}
infra is a placeholder Atmos component name, not the terraform verb repeated. Moving the
recipe's Terraform code to components/terraform/infra/ (the default
components.terraform.base_path is components/terraform) is one option -- swap infra for
whatever the user actually names 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.
-s {{ .Flags.env }} only selects which stack runs; it does not, by itself, load that
environment's Terraform variables the way the source -var-file=envs/{{env}}.tfvars did. Bring
the per-environment .tfvars files in through each stack file instead, one per environment
(stacks/dev.yaml, stacks/staging.yaml, stacks/prod.yaml), each pointing at its own file. The
relative path depends on which of the two options above you picked:
# stacks/dev.yaml (moved to components/terraform/infra/)components:terraform:infra:vars: !include ../components/terraform/infra/envs/dev.tfvars
# stacks/dev.yaml (no-move, terraform/ stays put)components:terraform:infra:metadata:component: terraform # points at the existing `terraform/` directoryvars: !include ../terraform/envs/dev.tfvars
See Migrating from Native Terraform for the full .tfvars/stack
mapping.
Shape C: Environment and Shell Settings
Before:
set dotenv-load := trueset shell := ["bash", "-uc"]export AWS_REGION := "us-east-1"
Steps:
- Turn
export VAR := valueinto a command or stepenv:map. set dotenv-loadmaps toenv: !include .envon the command, workflow, or step. Atmos parses the dotenv file natively (includingexport VAR=value, comments, quoting, and${VAR}expansion) and merges the result intoenv:. If the values are secrets rather than plain config, use Atmos's store or secrets integration instead of a plaintext.envfile.set shell := [...]changes the shell for every recipe in the Justfile. Atmos has no matching command-level setting. Usetype: scriptwith an explicitinterpreter:field on the one step that needs a different interpreter.
commands:- name: builddescription: Build the deployable artifactenv:<<: !include .envAWS_REGION: us-east-1steps:- type: shellcommand: go build -o bin/handler ./cmd/handler
Common Problems
{{ }} interpolation looks like Atmos templates but is not
Just's {{ var }} syntax looks like Atmos's {{ .Flags.var }} syntax, but the two are not the
same templating tool. Just evaluates {{ ... }} with its own built-in expression language
(variables, operators, string and path functions), not Go's text/template package. Atmos's
{{ .Flags.var }} syntax is a real Go template, rendered by Atmos itself at a different time.
Do not copy Just interpolation syntax into Atmos YAML. Change each reference to the matching
{{ .Flags.<name> }} or {{ .Arguments.<name> }} form.
[private] recipes map to internal: true
The custom command schema has an internal: true field. It excludes the command from atmos --help
listings and completion suggestions while leaving it fully runnable -- directly, as a default:
target, or from another command's steps. This is the direct equivalent of a [private] recipe,
and it covers cases plain step-inlining cannot: a helper called from more than one recipe, or one
a user invokes by name for manual debugging.
Only inline a [private] recipe's logic into a caller's step when it is genuinely single-caller
and has no reason to be invoked on its own -- in that case a separate internal command is just
unnecessary indirection.
If a [private] recipe is never called by any public recipe (an orphaned helper, not a
dependency), internal: true no longer forces the same discovery you'd get from step-inlining --
it would just as quietly hide dead code as reachable helper code. Confirm with the user whether
the recipe is still needed at all before migrating it; if it is, ask whether it should become a
internal command, a step inside whichever command ends up needing it, or a short script the user
maintains separately.
Command echo differs between just and Atmos
By default, Just prints each recipe line before running it (sh -x-style), so just build's
visible output includes every command line, not just what those commands print. Atmos type: shell steps run silently by default -- only the command's own stdout/stderr shows. The migrated
command's side effects match the original recipe, but the terminal output will look sparser side
by side. Tell the user this if they compare just <recipe> output to atmos <command> output
directly; it is a visible difference, not a bug.
Confirm set shell with the user; dotenv-load has a direct replacement
set dotenv-load maps directly to env: !include .env -- no confirmation needed unless the
.env file holds secrets, in which case ask whether to use Atmos's store or secrets integration
instead. set shell has no command-level equivalent; ask the user if a non-default shell matters
to their workflow, then apply type: script with interpreter: to the specific steps that need
it.
What Not To Do
- Do not assume
{{ }}means the same thing after you move it into Atmos YAML. - Do not invent a visibility value beyond the documented
internal: trueboolean (no "public"/"private" enum, no partial visibility). - Do not drop
set shellbehavior without telling the user;dotenv-loadmaps directly toenv: !include .env, so it does not need the same case-by-case confirmation.