# Migrating from Justfiles

A Justfile gives your team an ad-hoc way to run build, test, and deploy tasks, through named
recipes. Atmos gives you the same capability as a native, documented feature: custom commands
and workflows. Just's named parameters with default values map more directly onto Atmos flags
and arguments than Make's or Task's syntax does, so this migration is direct. Your Terraform
code and scripts do not need to change. Atmos replaces only the Justfile's recipes.

:::tip 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:

```shell
atmos ai skill install atmos-migration
```

See [AI Agent Skills](/ai/skills) for details.
:::

## Why Some Teams Choose Just Over Make

Just recipes do not need tab indentation, the way Make recipes do. A recipe's parameters are
named and typed, instead of set through implicit `$(VAR)` substitution. Neither point changes the
migration to Atmos. A custom command does not care how the source tool formatted its recipes. It
only cares what the recipe does.

## Key Concepts at a Glance

| Justfile concept                                          | Atmos equivalent                                              |
|----------------------------------------------------------|-----------------------------------------------------------------|
| `recipe param='default':`                                  | Command `flags:`/`arguments:` with a `default:`                  |
| `recipe: dep1 dep2` (recipe dependency)                     | Steps in order, or a `parallel` step with `needs:`                |
| `# comment` above a recipe                                  | Command `description:` (replaces `just --list`)                   |
| `export VAR := value`                                       | Command or step `env:` map                                        |
| `set dotenv-load`                                           | `env: !include .env` on the command, workflow, or step             |
| `set shell := [...]`                                        | Per-step `type: script` with `interpreter:`                       |
| `[private]` recipe                                          | Command `internal: true`                                            |
| `{{ }}` interpolation                                       | `{{ .Flags.<name> }}` / `{{ .Arguments.<name> }}` (a different tool) |

## Before and After

### Before (justfile)

```just
set dotenv-load := true

export AWS_REGION := "us-east-1"

# Build the deployable artifact
build:
    go build -o bin/handler ./cmd/handler

# Run tests (builds first)
test: build
    go test ./...

# Deploy to the given environment (defaults to dev)
deploy env='dev': build test
    cd terraform && terraform apply -var-file=envs/{{env}}.tfvars

[private]
_clean:
    rm -rf bin/
```

### After (atmos.yaml)

```yaml
# AWS_REGION (and anything else in .env) applies to every Atmos operation, not just one
# command's own steps -- this matches the source Justfile's `set dotenv-load`/`export`,
# which apply to every recipe. A command-level `env:` map only reaches that command's own
# steps, so `test` invoking `atmos build` (a separate process) and `deploy` invoking
# `atmos test` would not inherit it.
env:
  <<: !include .env
  AWS_REGION: us-east-1

commands:
  - name: build
    description: Build the deployable artifact
    steps:
      - type: shell
        command: go build -o bin/handler ./cmd/handler

  - name: test
    description: Run tests (builds first)
    steps:
      - type: atmos
        command: build
      - type: shell
        command: go test ./...

  - name: deploy
    description: Deploy to the given environment (defaults to dev)
    flags:
      - name: env
        shorthand: e
        default: "dev"
    steps:
      - type: atmos
        command: test
      - type: atmos
        command: terraform apply infra -s {{ .Flags.env }}
```

`infra` is a placeholder Atmos component name, not the `terraform` verb repeated. Move the old
`terraform/` directory's `.tf` files to `components/terraform/infra/` (the default
`components.terraform.base_path` is `components/terraform`), then 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.

`-s {{ .Flags.env }}` only selects _which stack_ runs, the way `terraform.apply` picks a stack by
name (`dev`, `staging`, `prod`); 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:

**File:** `stacks/dev.yaml (moved to components/terraform/infra/)`

```
components:
  terraform:
    infra:
      vars: !include ../components/terraform/infra/envs/dev.tfvars
```

**File:** `stacks/dev.yaml (no-move, terraform/ stays put)`

```
components:
  terraform:
    infra:
      metadata:
        component: terraform    # points at the existing `terraform/` directory
      vars: !include ../terraform/envs/dev.tfvars
```

See [Migrating from Native Terraform](/migration/native-terraform) for the full `.tfvars`/stack
mapping.

## Named Parameters Become Flags and Arguments

Just's `recipe param='default':` syntax maps directly to Atmos `flags:` (or `arguments:` for a
positional value), each with a matching `default:`. Read the value inside a step as
`{{ .Flags.env }}`. Do not use Just's own `{{env}}` syntax. It is a different template engine
that runs at a different time.

**File:** `atmos.yaml`

```yaml
commands:
  - name: deploy
    description: Deploy to the given environment
    flags:
      - name: env
        shorthand: e
        default: "dev"
    steps:
      - type: atmos
        command: terraform apply infra -s {{ .Flags.env }}
```

```shell
atmos deploy --env staging
```

## Recipe Dependencies Become Steps

A recipe dependency, such as `deploy: build test`, gets the same treatment as a Makefile target
chain. See [Migrating from Makefiles](/migration/makefile) for the general method, including when
to use a `parallel` step instead of plain steps in order.

## `[private]` Recipes and Environment Settings

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 a `[private]` recipe's behavior in `just --list`.
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.

Atmos loads `.env` files natively with `env: !include .env`. Atmos parses the dotenv format
(including `export VAR=value`, comments, quoting, and `${VAR}` expansion) and merges the result
into the command, workflow, or step `env:` map:

**File:** `atmos.yaml`

```yaml
commands:
  - name: build
    description: Build the deployable artifact
    env: !include .env
    steps:
      - type: shell
        command: go build -o bin/handler ./cmd/handler
```

If the `.env` file holds secrets rather than plain configuration, prefer Atmos's store or secrets
integration instead of a plaintext `.env` file.

## What You Gain

- **A larger set of built-in step types.** A Justfile recipe 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](/workflows/steps/type) for the full list.
- **Interactive commands.** A step such as `confirm` or `choose` can pause a command and ask the
  user a question. A Justfile recipe 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](/cli/configuration/toolchain). A Justfile has no built-in match for
  this.
- **One interface across every command.** `just --list` shows recipe names and parameters.
  Atmos adds `atmos <command> --help` with the same flag and argument format for every command,
  plus typed flags, shorthands, and defaults enforced by Atmos itself.

## Migration Checklist

- \[ ] List every Justfile recipe. Mark each one as independent, or part of a chain
- \[ ] Turn recipe parameters with defaults into command `flags:`/`arguments:`
- \[ ] Turn recipe dependencies into steps in order, or into a workflow
- \[ ] Turn `export VAR := value` into `env:` maps
- \[ ] Replace `set dotenv-load` with `env: !include .env` (or a store/secrets integration for secret values)
- \[ ] Mark `[private]` recipes as `internal: true` custom commands
- \[ ] Stop relying on `just --list`. `atmos --help` replaces it

## Common Questions

### Can I keep .env loading with set dotenv-load?

Yes. Use `env: !include .env` on the command, workflow, or step. Atmos parses the dotenv file
natively and merges its values into `env:`. Reserve Atmos's store or secrets integration for
values that are actually secret, since a plain `.env` file is not encrypted.

### What replaces \[private] recipes?

Set `internal: true` on the custom command. It stays fully runnable, including as a `default:`
target or from another command's steps, but disappears from `atmos --help` listings and
completion suggestions.

### Does \{\{ }} interpolation carry over as-is?

No. Just evaluates `{{ var }}` with its own built-in expression language, not Go's `text/template`
package. Atmos's `{{ .Flags.var }}` is a real Go template, rendered by Atmos itself. Change each
reference to `{{ .Flags.<name> }}` or `{{ .Arguments.<name> }}`.

### What if I use Make or Task instead of Just?

See [Migrating from Makefiles](/migration/makefile) or
[Migrating from Taskfile.yml](/migration/taskfile).
