Atlas Agent Skill for Claude Code, Codex, and Cursor
The Atlas agent skill teaches a coding agent to run the Atlas CLI: plan and lint migrations, write and
run schema and migration tests, detect drift, run Data Scripts, and answer operational questions from
Atlas Cloud, such as which databases are synced, pending, or failed, and which deployments failed. It
follows the Agent Skills open standard, so the same files work in Claude
Code, OpenAI Codex, Cursor, GitHub Copilot, and other agents that read SKILL.md.
The skill is one SKILL.md and nine reference files. The agent reads SKILL.md when a request matches
its description and opens a reference only when the task needs it:
| File | Covers |
|---|---|
SKILL.md | Workflow selection, the inspect, validate, diff, lint, test, apply sequence, dev-database scoping, migration directory commands, key rules |
references/schema-sources.md | HCL, SQL, and ORM schema sources (GORM, Drizzle, SQLAlchemy, Django, Ent, Sequelize, TypeORM), composite schemas, dev URLs per dialect |
references/versioned.md | migrate diff in depth: sources, directory formats, the diff policy, baselines, new, edit, rebase, checkpoint, down, migrate apply flags, pre-execution checks, multi-tenant apply, registry push |
references/declarative.md | schema apply flags and approval, the review policy, the full schema plan lifecycle (plan, edit, push, pull, lint, validate, approve, list, rm), schema push |
references/cicd.md | Versioned and declarative pipelines: lint or plan on PRs, push to the registry on merge, deploy from atlas://, GitHub Actions workflows, autorebase, and the GitLab, CircleCI, Bitbucket, Azure DevOps, Kubernetes, and Terraform equivalents |
references/lint.md | migrate lint and schema lint, changeset detection, every analyzer and check-code family, the lint policy block, nolint, and custom rules in HCL |
references/testing.md | The testing framework: test "schema", test "migrate", test "plan", and test "script" blocks, commands, variables, table-driven cases, the fix loop |
references/drift.md | atlas migrate drift, the pre-apply drift check, schema diff, exclude patterns, and when to point at Schema Monitoring |
references/cloud.md | atlas cloud repo, database, and migration commands, database and deployment statuses, infrastructure summaries, failed-deployment investigation, pending targets and plan approvals |
references/scripts.md | Data Scripts: atlas script exec, query, loop, test, and push, block reference, masking, and safety rules |
Migrations are the reason the skill exists. An agent can edit schema files the way it edits code, but a migration needs the dependency order of tables, views, functions, triggers, and constraints. It must pass the organization's lint policies, and the same schema change must produce the same migration regardless of which model generated it. The skill hands that work to Atlas and keeps the agent on the schema.
Installation
The skill files are served from https://atlasgo.io/skills/atlas/. The fastest install is to let the
agent do it: paste the prompt for your agent, and it downloads the files into its skills directory.
- Claude Code
- OpenAI Codex
- Cursor
- GitHub Copilot
Paste into Claude Code. This installs the skill for every project on the machine:
Install the Atlas skill: download https://atlasgo.io/skills/atlas/SKILL.md to ~/.claude/skills/atlas/SKILL.md and
each reference file listed in its Files section to ~/.claude/skills/atlas/references/<name>.
Overwrite existing files without editing them. Then list the installed files.
To install it for one repository instead, so it is committed and shared with the team, replace
~/.claude/skills/atlas with .claude/skills/atlas in the prompt.
No restart is needed. Claude Code invokes the skill on its own when a request matches its
description, and you can invoke it directly with /atlas:
/atlas summarize the state of our production databases
/atlas lint the latest migration and explain every finding
/atlas has the staging database drifted from its migrations?
The allowed-tools field in the frontmatter pre-approves the read-only Atlas commands the skill runs
most (atlas whoami, atlas cloud ..., atlas schema inspect, atlas migrate status, lint, diff, and
validate), so status checks run without a permission prompt. Commands that change a database, such as
atlas migrate apply and atlas schema apply, still prompt. Other agents ignore this field.
To update, paste the same prompt again. See Claude Code with Atlas for subagents, slash-command skills, and hooks.
Paste into Codex. This installs the skill for every project (~/.codex/skills); use
.codex/skills/atlas instead for one repository:
Install the Atlas skill: download https://atlasgo.io/skills/atlas/SKILL.md to ~/.codex/skills/atlas/SKILL.md and
each reference file listed in its Files section to ~/.codex/skills/atlas/references/<name>.
Overwrite existing files without editing them. Then list the installed files.
Codex loads the skill when database operations are requested. See Codex with Atlas
for the AGENTS.md alternative.
Paste into the Cursor agent. Cursor loads skills from
.cursor/skills/ in the project:
Install the Atlas skill at the repository root: download https://atlasgo.io/skills/atlas/SKILL.md to .cursor/skills/atlas/SKILL.md and
each reference file listed in its Files section to .cursor/skills/atlas/references/<name>.
Overwrite existing files without editing them. Then list the installed files.
Enable Agent Skills in Cursor Settings → Rules → Import Settings. See Cursor with Atlas for the rules-file alternative.
Paste into the Copilot agent. Copilot loads skills from .github/skills/ in the repository:
Install the Atlas skill at the repository root: download https://atlasgo.io/skills/atlas/SKILL.md to .github/skills/atlas/SKILL.md and
each reference file listed in its Files section to .github/skills/atlas/references/<name>.
Overwrite existing files without editing them. Then list the installed files.
See GitHub Copilot with Atlas for the instructions-file alternative.
Manual install
The same thing from a shell. Set SKILL_DIR to your agent's skills directory (~/.claude/skills/atlas,
.claude/skills/atlas, ~/.codex/skills/atlas, .cursor/skills/atlas, or .github/skills/atlas):
SKILL_DIR=~/.claude/skills/atlas
mkdir -p "$SKILL_DIR/references"
curl -fsSL https://atlasgo.io/skills/atlas/SKILL.md -o "$SKILL_DIR/SKILL.md"
# Must match the Files section of SKILL.md.
for f in schema-sources versioned declarative cicd lint testing drift cloud scripts; do
curl -fsSL "https://atlasgo.io/skills/atlas/references/$f.md" -o "$SKILL_DIR/references/$f.md"
done
Or copy the file contents from the Skill Files section below.
Commit the project-level directory so every team member and CI agent runs the same workflows. The
skill never contains credentials: it runs every command with --env <name> and expects database URLs
to come from getenv() in atlas.hcl.
What the Skill Covers
Atlas lets the agent focus on editing the schema while providing the infrastructure for:
- Schema validation:
atlas schema validateafter every edit, before a migration is generated. - Migration generation:
atlas migrate diffproduces the same migration for the same change, in dependency order. - Linting and policy:
atlas migrate lintandatlas schema lintwith the built-in analyzers (destructive, data-dependent, backward-incompatible, locks and rewrites, naming, ownership), thelintpolicy block,nolintwith approval only, and custom rules written in HCL. - Testing: the agent writes functions, views, triggers, data migrations, and scripts together with
test "schema",test "migrate",test "plan", andtest "script"cases. Atlas runs them and reports failures for the agent to fix. - Drift detection:
atlas migrate drifton demand, the pre-apply drift check inatlas.hcl,atlas schema difffor any two states, and Schema Monitoring for continuous alerts. - Atlas Cloud operations:
atlas cloud repo,database, andmigrationcommands for infrastructure summaries, database status (SYNCED,PENDING,FAILED), failed deployments, what is waiting for deployment, and pending plan approvals. - Data Scripts:
atlas script exec,query, andloopfor backfills, purges, reports, and invariant checks, with guards, assertions, output masking, and tests.
Example: schema change
User: Add an email column to the users table
Agent: I'll use Atlas to add this column safely.
1. Inspecting current schema...
$ atlas schema inspect --env dev
2. Adding column to schema file...
[edits schema.hcl]
3. Validating schema...
$ atlas schema validate --env dev
✓ Schema is valid
4. Generating migration...
$ atlas migrate diff --env dev "add_users_email"
Created: migrations/20260101120000_add_users_email.sql
5. Linting migration...
$ atlas migrate lint --env dev --latest 1
✓ No issues found
6. Ready to apply (dry-run first)...
$ atlas migrate apply --env dev --dry-run
Example: lint failure with a custom rule
User: Add a customer_notes table
Agent:
$ atlas migrate diff --env dev "add_customer_notes"
$ atlas migrate lint --env dev --latest 1
✗ disallow-null-columns: column body must be not null or have a default value
(custom rule from schema.rule.hcl)
Fixing: body TEXT NOT NULL DEFAULT '' in schema.hcl
$ atlas migrate hash --env dev
$ atlas migrate lint --env dev --latest 1
✓ No issues found
Example: infrastructure summary
User: Summarize the state of our databases and tell me who is waiting for a deployment
Agent:
$ atlas whoami
$ atlas cloud repo list
$ atlas cloud database list # every page
$ atlas cloud migration list --status FAILED
Org: acme
Repos (3): payments (migration dir, postgres), identity (schema, postgres),
analytics (migration dir, clickhouse)
Databases (18): prod 6 SYNCED / 1 PENDING / 1 FAILED, staging 5 SYNCED, dev 5 SYNCED
Waiting for deployment (PENDING):
prod-eu payments prod on 20260512141500, the other prod databases are on 20260514083000
Failed deployments:
#131 payments prod prod-us 20260514083000 FAILED 2026-05-15T09:30:00Z
Next step: atlas cloud migration describe --id 131 before the next prod deploy.
The skill reports cloud state and live database state separately: atlas cloud says what Atlas
Cloud knows, atlas migrate drift and atlas migrate status say what is in the database.
Example: data script
User: Backfill users.email_normalized from users.email in batches
Agent:
1. Reads atlas.hcl for the script { src } block
2. Writes scripts/backfill_email.script.hcl: a script "loop" with a keyset iterator,
an UPDATE per batch, and a post-loop assert that no row is left NULL
3. Writes a test "script" case that seeds rows and asserts the outcome
$ atlas script test --env dev
4. Runs it with an anchored name
$ atlas script loop --env prod --run '^backfill_email$'
Skill Files
The same files the install prompt downloads. Copy them if the agent cannot reach the network.
SKILL.md
---
name: atlas
description: "Database schema management, migrations, data scripts, and Atlas Cloud operations with the Atlas CLI. Use when: generating or applying migrations, diffing, linting, validating, or testing schemas, working with atlas.hcl, schema.hcl, or ORM schemas (GORM, Drizzle, SQLAlchemy, Django, Ent, Sequelize, TypeORM, Prisma), running data backfills, purges, or reports with atlas script, or answering questions about Atlas Cloud: which databases are synced, pending, or failed, recent or failed deployments, what is waiting for deployment, registry repos, and pending plan approvals."
allowed-tools: Bash(atlas version) Bash(atlas whoami) Bash(atlas cloud:*) Bash(atlas schema inspect:*) Bash(atlas schema validate:*) Bash(atlas schema diff:*) Bash(atlas schema lint:*) Bash(atlas schema plan list:*) Bash(atlas migrate status:*) Bash(atlas migrate lint:*) Bash(atlas migrate diff:*) Bash(atlas migrate hash:*) Bash(atlas migrate ls:*) Bash(atlas migrate validate:*) Bash(atlas migrate drift:*) Bash(atlas migrate test:*) Bash(atlas schema test:*) Bash(atlas schema plan test:*) Bash(atlas script test:*)
---
# Atlas: Schema Migrations, Data Scripts, and Cloud Operations
## Files
This skill is `SKILL.md` plus nine reference files under `references/`, each served at
`https://atlasgo.io/skills/atlas/references/<name>`: `schema-sources.md`, `versioned.md`, `declarative.md`,
`cicd.md`, `lint.md`, `testing.md`, `drift.md`, `cloud.md`, and `scripts.md`. Read a reference only when
the task needs it; "Choosing a Workflow" below says which.
## Security
Never hardcode credentials. Use environment variables in `atlas.hcl`:
```hcl
env "prod" {
url = getenv("DATABASE_URL")
}
```
Always run commands with `--env <name>` so database URLs stay in `atlas.hcl` and never enter the
conversation. Read `atlas.hcl` first to learn the environment names.
## Quick Reference
Use `--help` on any command for full docs and examples: `atlas migrate diff --help`.
```bash
# Setup
atlas version # Installed version
atlas whoami # Login status and org
atlas login # Needed by lint, test, drift, plan, scripts, schema lint, and cloud
# Schema
atlas schema inspect --env <name> # Inspect current schema
atlas schema validate --env <name> # Validate schema syntax/semantics
atlas schema diff --env <name> --from env://url --to file://schema.hcl # Live database vs desired schema
atlas schema lint --env <name> # Check schema policies
atlas schema test --env <name> # Test schema logic
# Declarative workflow
atlas schema plan --env <name> # Pre-plan changes for review
atlas schema apply --env <name> --dry-run # Preview changes
atlas schema apply --env <name> # Apply schema changes
# Versioned workflow
atlas migrate diff --env <name> "migration_name" # Generate migration
atlas migrate lint --env <name> --latest 1 # Lint the newest migration
atlas migrate test --env <name> # Test migrations
atlas migrate apply --env <name> --dry-run # Preview changes
atlas migrate apply --env <name> # Apply migration
atlas migrate status --env <name> # Live database vs migration directory
atlas migrate down --env <name> --dry-run # Preview reverting the last migration
atlas migrate hash --env <name> # Recompute atlas.sum after manual edits
atlas migrate new --env <name> "name" # Empty migration file for hand-written SQL
atlas migrate checkpoint --env <name> # Squash history into a checkpoint file
atlas migrate rebase --env <name> <version> # Rebase a migration onto newer files
atlas migrate push --env <name> # Push the directory to the Atlas Registry
# Linting and testing (see references/lint.md, references/testing.md)
atlas migrate lint --env ci # CI: new files vs the registry; --git-base master only without a registry
atlas schema lint --env <name> # Lint the whole schema against policy
atlas schema test --env <name> --run <case> # Schema tests: functions, views, triggers
atlas schema plan test --env <name> # Test a declarative plan file
# Drift (see references/drift.md)
atlas migrate drift --env <name> # Database vs migration history, exit 1 on drift (login)
atlas schema diff --from <url> --to <url> --dev-url <dev> # Any two states
# Atlas Cloud (see references/cloud.md)
atlas cloud repo list # Registry repos with synced/failed/pending counts
atlas cloud database list --env-name <env> # Every tracked database and its status
atlas cloud migration list --status FAILED # Failed deployments
atlas schema plan list --env <name> --pending # Plans waiting for approval
# Data Scripts (see references/scripts.md)
atlas script exec --env <name> --run '^name$' # Transactional mutation
atlas script query --env <name> --run '^name$' -q # Read or report
atlas script loop --env <name> --run '^name$' # Batched backfill or purge
atlas script test --env <name> # Test scripts on the dev database
```
## Choosing a Workflow
```
What is the request?
├─ A schema change
│ ├─ Project has migrations/ dir or a migration block in atlas.hcl?
│ │ ├─ Yes → Versioned: migrate diff → lint → test → apply (references/versioned.md)
│ │ └─ No → Declarative: schema apply --dry-run → apply (references/declarative.md)
│ ├─ Change must be reviewed and approved before it runs?
│ │ └─ schema plan (declarative) or a PR with migrate lint (versioned)
│ ├─ Iterating on a local database?
│ │ └─ schema apply --auto-approve for fast edit-apply cycles
│ └─ Not sure → Read atlas.hcl first
├─ CI/CD: lint on PRs, push to the registry, deploy from it
│ └─ references/cicd.md
├─ A data change (backfill, purge, report, invariant check)
│ └─ Data Scripts: references/scripts.md
├─ A lint failure, a lint policy, or a custom rule
│ └─ references/lint.md
├─ Tests for functions, views, triggers, data migrations, or plans
│ └─ references/testing.md
├─ "Has this database drifted?" or a drift check for deploys
│ └─ references/drift.md
├─ A question about deployments, database status, or the registry
│ └─ Atlas Cloud: references/cloud.md
└─ ORM or schema source setup
└─ references/schema-sources.md
```
`atlas schema apply` applies schema changes directly to a database without migration files. Use it for
fast iteration during development: edit the schema, run `schema apply`, see the result.
## Example
Versioned project (a `migration` block in `atlas.hcl`):
```
User: Add an email column to the users table
Agent steps:
1. atlas schema inspect --env dev # understand current state
2. Edit schema source file # add email column
3. atlas schema validate --env dev # verify syntax
4. atlas migrate diff --env dev "add_users_email" # generate migration
5. atlas migrate lint --env dev --latest 1 # check for issues
6. atlas migrate apply --env dev --dry-run # preview before applying
```
Declarative project (a `schema` block, no migration directory):
```
Agent steps:
1. Edit schema source file # add email column
2. atlas schema validate --env dev # verify syntax
3. atlas schema apply --env dev --dry-run # read the planned SQL to the user
4. atlas schema apply --env dev # apply (or --auto-approve on a local database)
```
## Core Concepts
### Configuration File (atlas.hcl)
Always read the project's `atlas.hcl` first. It contains the environment configurations:
```hcl
env "<name>" {
url = getenv("DATABASE_URL")
dev = "docker://postgres/17/dev?search_path=public"
migration {
dir = "file://migrations" # or "atlas://<repo>" for the Atlas Registry
}
schema {
src = "file://schema.hcl"
}
script {
src = "file://scripts" # Data Scripts source
}
}
```
### Dev Database
Atlas uses a temporary dev database to process and validate schemas. The URL scope must match the
target: schema-scoped when the project manages one schema, database-scoped when it manages several
schemas, extensions, or event triggers.
```bash
# Schema-scoped (single schema, most common)
--dev-url "docker://mysql/8/dev"
--dev-url "docker://postgres/17/dev?search_path=public"
--dev-url "sqlite://dev?mode=memory"
--dev-url "docker://sqlserver/2022-latest/dev?mode=schema"
# Database-scoped (multiple schemas, extensions, or event triggers)
--dev-url "docker://mysql/8"
--dev-url "docker://postgres/17/dev"
--dev-url "docker://sqlserver/2022-latest/dev?mode=database"
```
Using the wrong scope causes errors (`ModifySchema is not allowed`) or silently drops database-level
objects (extensions, event triggers) from migrations. For PostGIS or pgvector schemas, use
`docker://postgis/latest/dev` or `docker://pgvector/pg17/dev`.
If the schema depends on extensions or external objects, use a `docker` block with a `baseline`:
```hcl
docker "postgres" "dev" {
image = "postgres:17"
schema = "public"
baseline = <<SQL
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SQL
}
env "local" {
src = "file://schema.hcl"
dev = docker.postgres.dev.url
}
```
## Workflows
### 1. Schema Inspection
Start with an overview before diving into details. The default output is HCL. Use
`--format "{{ json . }}"` for JSON or `--format "{{ sql . }}"` for SQL.
```bash
# List tables (overview first, JSON output)
atlas schema inspect --env <name> --format "{{ json . }}" | jq ".schemas[].tables[].name"
# Full SQL schema
atlas schema inspect --env <name> --format "{{ sql . }}"
# Filter with --include/--exclude (useful for large schemas)
atlas schema inspect --env <name> --include "users_*" # Only matching tables
atlas schema inspect --env <name> --exclude "*_backup" # Skip matching tables
atlas schema inspect --env <name> --exclude "*[type=trigger]" # Skip triggers
# Open a visual ERD in the browser (requires atlas login)
atlas schema inspect --env <name> -w
```
### 2. Schema Comparison (Diff)
```bash
# Live database vs desired schema. --from and --to are always required; --env supplies the dev database.
# env://url reads the env's url so the database URL never appears on the command line.
# On a versioned database add --exclude atlas_schema_revisions, or the revisions table shows up as a drop.
atlas schema diff --env <name> --from env://url --to file://schema.hcl --exclude atlas_schema_revisions
# Compare specific sources
atlas schema diff --env <name> --from file://migrations --to file://schema.hcl
```
### 3. Migration Generation
```bash
# Generate a migration from the schema diff
atlas migrate diff --env <name> "add_users_table"
# With explicit parameters
atlas migrate diff \
--dir file://migrations \
--dev-url docker://postgres/17/dev \
--to file://schema.hcl \
"add_users_table"
```
### 4. Schema Validation
Validate schema definitions after every edit and before generating migrations:
```bash
atlas schema validate --env <name>
atlas schema validate --dev-url docker://postgres/17/dev --url file://schema.hcl
```
If valid, the command exits successfully. If invalid, it prints the error (unresolved references,
syntax issues, unsupported attributes).
### 5. Migration Linting
```bash
atlas migrate lint --env <name> --latest 1 # Lint the latest migration
atlas migrate lint --env ci # CI: new files vs the registry (no registry: --git-base master)
atlas schema lint --env <name> # Check the whole schema against policy
```
The analyzers catch destructive changes (`DS*`), data-dependent changes (`MF*`),
backward-incompatible changes (`BC*`), table locks and rewrites (`MY*`, `PG*`), naming and ownership
policy, and custom rules written in HCL. Policy lives in the `lint` block of `atlas.hcl`.
Fixing lint issues:
- Unapplied migrations: edit the file, then run `atlas migrate hash --env <name>`.
- Applied migrations: create a corrective migration. Never edit an applied migration.
- `-- atlas:nolint <code>` only with the user's approval.
Codes, policy configuration, and custom rules: `references/lint.md`.
### 6. Testing
```bash
atlas schema test --env <name> # Functions, views, triggers, constraints, queries
atlas migrate test --env <name> # Data migrations between versions
atlas schema plan test --env <name> # Declarative plan files
atlas script test --env <name> # Data Scripts
```
Tests are HCL cases (`test "schema" "<name>" { exec, catch, assert, ... }`) run against the dev
database. Write a test with every function, view, trigger, and data migration you add. Block syntax,
env configuration, and the fix loop: `references/testing.md`.
### 7. Applying Migrations
```bash
atlas migrate apply --env <name> --dry-run # Always preview first
atlas migrate apply --env <name> # Apply
atlas migrate status --env <name> # Verify
```
Before applying to a shared or production environment, check Atlas Cloud state first (failed or
in-flight deployments on the same targets). See the pre-deployment checklist in `references/cloud.md`.
### 8. Declarative Plan and Approval
In the declarative workflow, `atlas schema plan` saves a migration plan to the Atlas Registry so it
can be reviewed and approved before `atlas schema apply` runs it:
```bash
atlas schema plan --env <name> # Plan, lint, review, approve and push
atlas schema plan --env <name> --pending # Push for someone else to approve
atlas schema plan list --env <name> --pending # Plans waiting for approval
atlas schema plan approve --url "atlas://<repo>/plans/<name>"
atlas schema apply --env <name> # Applies the approved plan as-is
```
Plan editing, `push`, `pull`, `lint`, `validate`, the review policy, and `schema push`:
`references/declarative.md`. Migration directory formats, the diff policy, baselines, `down`,
`checkpoint`, `rebase`, pre-execution checks, and multi-tenant apply: `references/versioned.md`.
### 9. CI/CD
Lint (or plan) on pull requests, push the directory or schema to the Atlas Registry on merge, deploy
from `atlas://<repo>` with drift and pre-execution checks. Workflows for GitHub Actions and the
equivalents for GitLab, CircleCI, Bitbucket, Azure DevOps, Kubernetes, and Terraform:
`references/cicd.md`.
## Standard Workflow
1. `atlas schema inspect --env <name>`: understand the current state (skip on an empty database)
2. Edit schema files
3. `atlas schema validate --env <name>`: check syntax
4. `atlas migrate diff --env <name> "change_name"`: generate the migration
5. `atlas migrate lint --env <name> --latest 1`: validate (requires login)
6. `atlas migrate test --env <name>`: test (requires login)
7. If issues: edit the migration, then `atlas migrate hash`
8. `atlas migrate apply --env <name> --dry-run`, then apply
If a step aborts with `command requires 'atlas login'` or `available only to Atlas Pro users`, tell the
user that step was skipped and why, and continue with the remaining steps. Do not stall on it.
## Drift Detection
Drift is a difference between a database and its source of truth. Four tools, one per question:
| Question | Tool |
|----------|------|
| Does this database match its migration history, now or on a schedule? | `atlas migrate drift --env <name>` (exit 1 on drift, cron-friendly; requires `atlas login`) |
| Block a deploy if the target drifted | `check "migrate_apply" { drift { on_error = FAIL } }` in `atlas.hcl` |
| How do two states differ (declarative, or database vs database)? | `atlas schema diff --from <url> --to <url>` (both flags required) |
| Agent-based continuous monitoring with alerts | Schema Monitoring in Atlas Cloud |
Commands, flags, `exclude` patterns, and the reporting workflow: `references/drift.md`.
## Atlas Cloud
`atlas cloud` commands report what Atlas Cloud knows: registry repos, every tracked database with its
sync status (`SYNCED`, `PENDING`, `FAILED`) and current version, and every deployment event
(`PASSED`, `FAILED`, `NO_ACTION`, `DRY_RUN`). Use them to answer operational questions:
| Question | Command |
|----------|---------|
| Summarize our database infrastructure | `atlas cloud repo list`, then `atlas cloud database list` per env |
| Which deployments failed? | `atlas cloud migration list --status FAILED` |
| What happened in a deployment? | `atlas cloud migration describe --id <id>` |
| Which databases are waiting for a deployment? | `atlas cloud database list` and filter `Status=PENDING` |
| Which plans are waiting for approval? | `atlas schema plan list --env <name> --pending` |
| What version is production on? | `atlas cloud database list --env-name prod` |
Full command reference, report templates, and decision rules: `references/cloud.md`.
## Data Scripts
`atlas script` runs data operations written as HCL: `exec` for transactional mutations, `query` for
reads and reports, and `loop` for batched backfills and purges, with `condition` guards, `assert`
checks, `expect_rows`, output masking, and tests. Use a script for data work and a migration for schema
work. Full reference: `references/scripts.md`.
## Schema Sources
For HCL schemas, ORM integrations (GORM, Drizzle, SQLAlchemy, Django, Ent, Sequelize, TypeORM),
composite schemas, and dev-database dialect URLs, see `references/schema-sources.md`.
## Onboarding an Existing Project
To start managing an existing database with versioned migrations:
```bash
# 1. Export the current schema to code
atlas schema inspect --env <name> --format '{{ sql . | split | write "src" }}'
# 2. Generate a baseline migration from the exported schema
atlas migrate diff --env <name> "baseline" --to "file://src"
# 3. Mark the baseline as applied on existing databases (use the version from the filename)
atlas migrate apply --env <name> --baseline '<version>'
```
The baseline migration captures the current state without executing it on existing databases. On new
databases, it runs in full to create the initial schema.
## Troubleshooting
```bash
atlas version # Check installation
atlas whoami # Check login and org
atlas migrate hash --env <name> # Repair migration integrity after manual edits
```
- `command requires 'atlas login'` or `available only to Atlas Pro users`: the command needs a
login. Run `atlas login`, or `atlas login --token "$ATLAS_TOKEN"` in CI. Report the skipped step and
continue.
- Missing driver error: ensure `--url` or `--dev-url` is correctly specified.
- `ModifySchema is not allowed`: the dev URL scope does not match the target. See Dev Database above.
## Key Rules
1. Read `atlas.hcl` first and use the environment names from it.
2. Never hardcode credentials. Use `getenv()` and `--env`.
3. Run `atlas schema validate` after schema edits.
4. Always lint before applying migrations.
5. Always dry-run before applying.
6. Run `atlas migrate hash` after editing migration files.
7. Never edit an applied migration. Create a corrective migration instead.
8. Never ignore lint errors. Fix them or get explicit user approval.
9. Run `atlas login` once per machine. Linting, testing, drift, `schema plan`, the review policy,
scripts, ERD, and Atlas Cloud need it. Without it, report the gated step and continue.
10. Before deploying to shared environments, check Atlas Cloud for `FAILED` or `PENDING` targets
and recent failed deployments (`references/cloud.md`).
11. Use Data Scripts, not ad-hoc SQL, for data changes: guard with `condition`, assert the outcome,
and mask PII in reports (`references/scripts.md`).
12. Report cloud state and live database state separately. `atlas cloud` says what Atlas Cloud
knows; `atlas migrate status` says what is in the database.
## Documentation
- [CLI Reference](https://atlasgo.io/cli-reference)
- [Versioned Migrations](https://atlasgo.io/versioned/diff)
- [Declarative Workflow](https://atlasgo.io/declarative/apply)
- [Declarative Plan and Approval](https://atlasgo.io/declarative/plan)
- [Versioned CI/CD Setup](https://atlasgo.io/versioned/setup-cicd)
- [Declarative CI/CD Setup](https://atlasgo.io/declarative/setup-cicd)
- [GitHub Actions](https://atlasgo.io/integrations/github-actions)
- [Migration Linting](https://atlasgo.io/versioned/lint)
- [Lint Analyzers](https://atlasgo.io/lint/analyzers)
- [Custom Lint Rules](https://atlasgo.io/lint/rules)
- [Schema Testing](https://atlasgo.io/testing/schema)
- [Migration Testing](https://atlasgo.io/testing/migrate)
- [Drift Detection](https://atlasgo.io/versioned/drift-detection)
- [Schema Monitoring](https://atlasgo.io/monitoring)
- [Data Scripts](https://atlasgo.io/scripts)
- [Atlas Cloud Deployments](https://atlasgo.io/cloud/deployment)
- [Onboard Existing Database](https://atlasgo.io/versioned/import)
- [ORM Integrations](https://atlasgo.io/orms)
- [Dev Database](https://atlasgo.io/concepts/dev-database)
references/schema-sources.md
# Schema Sources Reference
## HCL Schema
```hcl
data "hcl_schema" "<name>" {
path = "schema.hcl"
}
env "<name>" {
schema {
src = data.hcl_schema.<name>.url
}
}
```
## External Schema (ORM Integration)
The `external_schema` data source imports SQL schema from an ORM or external program.
```hcl
# GORM (Go)
data "external_schema" "gorm" {
program = ["go", "run", "-mod=mod", "ariga.io/atlas-provider-gorm", "load", "--path", "./models", "--dialect", "postgres"]
}
# Drizzle (TypeScript)
data "external_schema" "drizzle" {
program = ["npx", "drizzle-kit", "export"]
}
# SQLAlchemy (Python)
data "external_schema" "sqlalchemy" {
program = ["python", "-m", "atlas_provider_sqlalchemy", "--path", "./models", "--dialect", "postgresql"]
}
# Django (Python)
data "external_schema" "django" {
program = ["python", "manage.py", "atlas-provider-django", "--dialect", "postgresql"]
}
# Ent (Go)
env "<name>" {
schema {
src = "ent://ent/schema"
}
}
# Sequelize (Node.js)
data "external_schema" "sequelize" {
program = ["npx", "@ariga/atlas-provider-sequelize", "load", "--path", "./models", "--dialect", "postgres"]
}
# TypeORM (TypeScript)
data "external_schema" "typeorm" {
program = ["npx", "@ariga/atlas-provider-typeorm", "load", "--path", "./entities", "--dialect", "postgres"]
}
```
Wire into an environment:
```hcl
env "<name>" {
schema {
src = data.external_schema.<orm>.url
}
}
```
## Composite Schema
Combine multiple schema sources into one:
```hcl
data "composite_schema" "app" {
schema "users" {
url = data.external_schema.auth_service.url
}
schema "graph" {
url = "ent://ent/schema"
}
schema "shared" {
url = "file://schema/shared.hcl"
}
}
```
## Dev-Database Dialects
The dev URL format depends on whether your project uses **schema-scoped** or **database-scoped** migrations. Getting this wrong causes errors like `ModifySchema is not allowed` or silently drops database-level objects (extensions, event triggers) from migrations.
**Schema-scoped** (single schema — most common): include the database name and schema scope so Atlas creates objects in the correct schema. Use this when all tables live in one schema (e.g., `public`).
| Dialect | Dev URL (schema-scoped) |
|------------|------------------------------------------------------|
| MySQL | `docker://mysql/8/dev` |
| MariaDB | `docker://maria/latest/dev` |
| PostgreSQL | `docker://postgres/17/dev?search_path=public` |
| SQLite | `sqlite://dev?mode=memory` |
| SQL Server | `docker://sqlserver/2022-latest/dev?mode=schema` |
| ClickHouse | `docker://clickhouse/23.11/dev` |
**Database-scoped** (multiple schemas or database-level objects): omit the schema scope so Atlas can manage multiple schemas and detect database-level objects like extensions and event triggers.
| Dialect | Dev URL (database-scoped) |
|------------|------------------------------------------------------|
| MySQL | `docker://mysql/8` |
| MariaDB | `docker://maria/latest` |
| PostgreSQL | `docker://postgres/17/dev` |
| SQL Server | `docker://sqlserver/2022-latest/dev?mode=database` |
| ClickHouse | `docker://clickhouse/23.11` |
**PostgreSQL with extensions** — use PostGIS or pgvector images when the schema uses those extensions:
```
docker://postgis/latest/dev?search_path=public
docker://pgvector/pg17/dev?search_path=public
```
**How to choose:** Check the project's `atlas.hcl` or target database URL. If it includes `search_path=public` (Postgres) or a specific database name (MySQL), use schema-scoped. If the project manages multiple schemas, extensions, or event triggers, use database-scoped.
See https://atlasgo.io/concepts/dev-database for additional drivers and options.
references/lint.md
# Linting Reference (`atlas migrate lint`, `atlas schema lint`)
Atlas lints changes before they run. `migrate lint` analyzes migration files for destructive changes,
backward-incompatible changes, data-dependent changes, table locks, naming violations, and custom
rules, and it exits non-zero on errors so CI blocks the change. `schema lint` runs the same policy
rules against the whole desired schema.
| Command | Analyzes | Reports |
|---------|----------|---------|
| `atlas migrate lint --env <name>` | The new migration files: those not yet pushed to the registry, or with `--latest N` / `--git-base` the files those flags select | Only findings introduced by those files |
| `atlas schema lint --env <name>` | The entire desired schema | Every finding in the schema |
Use `migrate lint` in the versioned workflow and in CI. Use `schema lint` to audit an existing schema
against policy. Run `atlas login` before linting.
## Running `migrate lint`
```bash
atlas migrate lint --env <name> # registry project: files not yet pushed to the registry
atlas migrate lint --env <name> --latest 1 # the newest N migration files (local development)
atlas migrate lint --env <name> --git-base master # no registry: files added since the git base branch
atlas migrate lint --env <name> -w # any of the above: open the report in the browser
atlas migrate lint --env <name> --format '{{ json . }}' # any of the above: machine-readable report
```
Changeset detection picks which files to analyze. Read `atlas.hcl` first and use the case that
matches the project:
1. Registry comparison (most projects): the directory is pushed to the Atlas Registry
(`migration.repo.name` is set, or `dir` is an `atlas://` URL). The local directory is compared with
the latest pushed state. No flag and no git configuration needed; this is the CI default whenever
a registry repo exists.
2. `--latest N`: the last N files by version. For local development, just pass `--latest 1`; no
configuration needed.
3. `--git-base <branch>` (plus `--git-dir <path>` when the directory is not the repo root): files added
on the current branch compared with the base branch. Only for projects that do not push to the
registry.
The equivalent `atlas.hcl` settings:
```hcl
# Registry project: nothing to configure for lint beyond the repo.
env "ci" {
migration {
dir = "file://migrations"
repo {
name = "app"
}
}
}
# No registry: compare with the git base branch.
env "ci" {
lint {
git {
base = "master"
dir = "<path>" # optional working directory for git
}
}
}
```
`migrate lint` needs a dev database (`dev` in the env or `--dev-url`) to replay the files. The dev
URL scope must match the project (see `SKILL.md`, Dev Database).
## Built-in Analyzers
Each analyzer owns a family of check codes. The report prints the code and a link to its doc.
| Analyzer | Codes | Detects | Default |
|----------|-------|---------|---------|
| Destructive changes | `DS101`-`DS103` | Dropping schemas, tables, non-virtual columns | Error |
| Constraint drops | `CD101`-`CD103` | Dropping foreign-key, check, or primary-key constraints | Warning |
| Data-dependent changes | `MF101`-`MF104` | Changes that may fail on existing data: unique index on a populated column, NOT NULL column without a default | Warning |
| Backward-incompatible changes | `BC101`-`BC104` | Renaming or dropping tables and columns that running code still uses | Warning |
| Non-linear changes | (no code) | Files added out of order, or edited after they were pushed | Warning |
| Table locks and rebuilds (MySQL) | `MY101`-`MY148` | ALTERs that lock or copy the table | Warning |
| Concurrent index policy (PostgreSQL) | `PG101`-`PG110` | Index changes without `CONCURRENTLY`, missing `atlas:txmode none`, constraint creation that takes `ACCESS EXCLUSIVE` | Warning |
| Blocking changes (PostgreSQL) | `PG301`-`PG308` | Type changes that rewrite the table, volatile defaults, constraints that scan the whole table | Warning |
| Nested transactions | `TX101`, `TX201` | Transaction statements inside a migration that Atlas already wraps | Warning |
| Naming conventions | `NM101`-`NM106` | Names that violate the configured pattern | Warning |
| Ownership policy | `OW101`, `OW102` | Changes to objects the author's team does not own | Warning |
| SQL injection | `SA101` | Unsafe string concatenation in functions and procedures | Warning |
| Vulnerable extensions | per CVE | Installed extensions with a published CVE | Warning |
| Statement rules | per rule | Custom regex rules over raw statements | As configured |
The full catalog, with an example and a fix for every code, is at https://atlasgo.io/lint/analyzers.
## Configuring Lint Policy
The `lint` block in `atlas.hcl` sets policy globally or per environment. An env-level block inherits
and overrides the global one.
```hcl
lint {
# Declarative workflow: when "schema apply" needs manual approval.
# ERROR: only when the lint report has errors. WARNING: on warnings too. ALWAYS (default).
review = ERROR
destructive {
error = false # report drops as warnings instead of failing
force = true # developers cannot override with nolint
allow_table { match = "drop_.+" } # deprecation workflow: tables renamed drop_* may be dropped
allow_column { match = "drop_.+" }
}
data_depend { error = true }
incompatible {
error = true
drop_column { message = "deprecate ${self.table.name}.${self.name} in the application first" }
}
non_linear {
error = true
on_edit = WARN # relax non-linear errors when editing the latest file
}
naming {
match = "^[a-z_]+$"
message = "must be snake_case"
index {
match = "^[a-z_]+_idx$"
message = "indexes end with _idx"
}
}
# Per-check overrides.
check "PG301" { error = true }
check "DS102" { skip = true }
# Custom rules written in HCL (see below).
rule "hcl" "policy" {
src = ["schema.rule.hcl"]
}
# Custom report format.
format = <<EOS
{{- range $f := .Files }}{{ json $f }}{{ end }}
EOS
}
```
`review = ERROR` also gates `atlas schema apply`: the apply auto-approves when the plan lints clean
and asks for approval when the linter reports an error.
## Silencing a Finding
Annotate the statement with `-- atlas:nolint` to exclude it from analysis. Prefer fixing the change.
Never add `nolint` without telling the user why the finding is a false positive.
```sql
-- atlas:nolint -- all analyzers, this statement
ALTER TABLE t1 DROP COLUMN c1;
-- atlas:nolint destructive -- one analyzer by name
ALTER TABLE t2 DROP COLUMN c2;
-- atlas:nolint DS103 -- one check by code
ALTER TABLE t3 DROP COLUMN c3;
```
A directive on the first line of the file (`-- atlas:nolint` before any statement) applies to the
whole file. `destructive { force = true }` disables `nolint` for destructive checks.
## Custom Rules
Custom rules are HCL files with the `.rule.hcl` extension. Three blocks: `predicate` defines a reusable
condition, `rule "schema"` applies to the desired schema (reported by both `schema lint` and
`migrate lint`), and `rule "migrate"` applies to the change itself (`migrate lint` only). Reference:
https://atlasgo.io/hcl/rule.
```hcl
# schema.rule.hcl
# A predicate: true when a column is NOT NULL or has a default.
predicate "column" "not_null_or_have_default" {
or {
default { ne = null }
null { eq = false }
}
}
# Schema rule: applies to every column in the schema.
rule "schema" "disallow-null-columns" {
description = "require columns to be not null or have a default value"
table {
column {
assert {
predicate = predicate.column.not_null_or_have_default
message = "column ${self.name} must be not null or have a default value"
}
}
}
}
# Migration rule: applies only to tables added by the analyzed migrations.
rule "migrate" "disallow-add-table-with-null-columns" {
description = "disallow adding tables with null columns"
add {
table {
column {
assert {
predicate = predicate.column.not_null_or_have_default
message = "column ${self.name} must be not null or have a default value"
}
}
}
}
}
```
- `predicate "<type>" "<name>"`: `type` is an object kind of the driver (`table`, `column`, `index`,
`foreign_key`, `function`, `trigger`, `policy`, `role`, `user`, `permission`, ...). Reference it as
`predicate.<type>.<name>`. Predicates can take arguments through `variable` blocks.
- `rule "schema" "<name>"` and `rule "migrate" "<name>"`: `description` is required and appears in the
report. Nested blocks traverse the schema (`table { column { ... } }`) or the change
(`add { table { ... } }`, `modify`, `drop`). Inside, `match` filters and `assert` checks.
- `self` is the current object; `${self.name}`, `${self.table.name}`, `${self.schema.name}` interpolate.
Wire the file in with `lint { rule "hcl" "<name>" { src = ["schema.rule.hcl"] } }`, globally or per
env, then run `migrate lint` or `schema lint`. Examples for columns, foreign keys, functions, audit
tables, RLS, security invoker views, roles, and permissions: https://atlasgo.io/lint/rules.
## Agent Workflow: Lint Failed
1. Read the code and message. `--format '{{ json . }}'` gives structured output (with `--latest 1`
locally; no extra flag in CI when the directory is in the registry).
2. Decide whether the finding is real:
- Destructive (`DS*`): confirm with the user. Prefer a deprecation step (rename to `drop_*`, drop
later) or a backup.
- Data-dependent (`MF*`): add a default, backfill first, or split into two migrations.
- Backward-incompatible (`BC*`): coordinate the rename with application code, or add and migrate
instead of renaming.
- Concurrent index (`PG*`): use `CREATE INDEX CONCURRENTLY` and add `-- atlas:txmode none` at the
top of the file.
- Naming (`NM*`) and custom rules: rename to match the policy.
3. Fix the migration file (unapplied only), then `atlas migrate hash --env <name>` and re-lint.
4. If the change is applied already, write a new corrective migration instead.
5. Use `-- atlas:nolint <code>` only with the user's approval, and say so in the summary.
## Error Handling
| Error | Action |
|-------|--------|
| `checksum mismatch` | The directory was edited by hand. Run `atlas migrate hash --env <name>` |
| Lint exits 1 with no findings printed | Run with `-w` or `--format '{{ json . }}'` to see the full report |
| `--git-base` finds no files | The branch has no new files, or `dir` is wrong. Use `--latest 1` locally |
| Custom rule fails to parse | Check the `.rule.hcl` extension, the `predicate.<type>.<name>` reference, and that `description` is set |
| `command requires 'atlas login'` | Run `atlas login` |
## Documentation
- [Migration linting](https://atlasgo.io/versioned/lint)
- [Analyzers and check codes](https://atlasgo.io/lint/analyzers)
- [Custom linting rules](https://atlasgo.io/lint/rules)
- [Rule language reference](https://atlasgo.io/hcl/rule)
- [Lint block in atlas.hcl](https://atlasgo.io/atlas-schema/projects#configure-migration-linting)
- [Destructive change policy](https://atlasgo.io/guides/destructive-change-policy)
- [Review policy for schema apply](https://atlasgo.io/declarative/apply#review-policy)
references/testing.md
# Testing Reference (`atlas schema test`, `atlas migrate test`, `atlas schema plan test`, `atlas script test`)
The Atlas testing framework runs test cases written in HCL against a dev database. The agent writes
the schema logic (functions, views, triggers, constraints, policies), data migrations, or scripts,
writes the tests next to them, and Atlas executes both and reports failures to fix. Testing requires
`atlas login`.
| Command | Test block | Starts from | Use for |
|---------|-----------|-------------|---------|
| `atlas schema test --env <name>` | `test "schema"` | The desired schema, created on the dev database | Functions, views, triggers, constraints, RLS, queries |
| `atlas migrate test --env <name>` | `test "migrate"` | An empty database, migrated to a chosen version | Data migrations and the migration files themselves |
| `atlas schema plan test --env <name>` | `test "plan"` | A given schema snapshot, then a plan file applied | Declarative plans before approval |
| `atlas script test --env <name>` | `test "script"` | A fresh database with no schema | Data Scripts in isolation (see `references/scripts.md`) |
All four accept `--run <regexp>` to select cases by name, `--var name=value` for input variables, and
optional paths to test files as trailing arguments.
## Configuration
Declare the test files on the env (or globally) so `--env` finds them:
```hcl
env "dev" {
src = "file://schema.hcl"
dev = "docker://postgres/17/dev?search_path=public"
migration {
dir = "file://migrations"
}
test {
schema {
src = ["schema.test.hcl", "plan.test.hcl"] # plan tests are declared here too
vars = { seed_file = "seed.sql" }
}
migrate {
src = ["migrate.test.hcl"]
}
script {
src = ["scripts.test.hcl"]
}
}
}
```
Test files use the `.test.hcl` extension. `test "plan"` files are listed under `test.schema.src`,
or passed as an argument: `atlas schema plan test --env dev plan.test.hcl`. Each `test` block has two labels: the kind and the case name.
A case is a list of commands run in order; the first failing command aborts the case. The dev database
is reset between cases regardless of the result.
## Commands Available in Every Test Kind
| Command | Purpose |
|---------|---------|
| `exec { sql, format, output \| match }` | Runs SQL that must succeed. With `output` (exact, after trimming) or `match` (regexp) the result is compared, serialized as `csv` (default) or `table` |
| `catch { sql, error }` | Runs SQL that must fail; `error` matches the message |
| `assert { sql, error_message }` | SQL must return one row with one true value |
| `log { message }` | Prints a message to the test output |
| `external { program, working_dir, output \| match }` | Runs a program (a Go test, a seeding script) and checks its stdout |
| `script "exec" \| "query" \| "loop" { file, run, vars, output \| match \| error, as }` | Runs Data Scripts inside the case |
| `cleanup { sql }` | Runs after the case, whatever the result, in reverse order of definition |
Case-level arguments: `skip = <bool>` (can be an expression), `parallel = true` (schema tests only;
use for stateless cases), and `for_each` for table-driven cases.
## Schema Tests
Atlas creates the desired schema on the dev database, runs the case, and cleans up. On PostgreSQL a
template database can be cloned instead of rebuilt to keep setup fast for large schemas.
```hcl
# schema.test.hcl
test "schema" "postal_code_domain" {
parallel = true
exec {
sql = "SELECT '12345'::us_postal_code"
}
catch {
sql = "SELECT 'hello'::us_postal_code"
error = "invalid input value"
}
}
test "schema" "order_total_trigger" {
exec {
sql = <<-SQL
INSERT INTO orders (id, customer_id) VALUES (1, 10);
INSERT INTO order_items (order_id, price, qty) VALUES (1, 5, 2), (1, 3, 1);
SQL
}
assert {
sql = "SELECT total = 13 FROM orders WHERE id = 1"
error_message = "trigger did not recompute orders.total"
}
}
test "schema" "upper" {
for_each = [
{ input = "hello", expected = "HELLO" },
{ input = "world", expected = "WORLD" },
]
exec {
sql = "SELECT upper('${each.value.input}')"
output = each.value.expected
}
}
```
Input variables: declare `variable "name" { type = string }` in the test file, reference it as
`var.name`, and set it from `test { schema { vars = { ... } } }` in `atlas.hcl` or `--var`.
What to test in a schema: functions and procedures (inputs, edge cases, error paths with `catch`),
views (row counts and joins against seeded data), triggers (side effects after INSERT/UPDATE/DELETE),
check constraints and domains (`catch` on invalid values), row-level security (queries under a role),
and the application's own queries (`exec` with `output`).
## Migration Tests
Every case starts from the zero state of the migration directory. The pattern for testing a data
migration: migrate to the version before it, seed data, migrate to the tested version, assert.
```hcl
# migrate.test.hcl
test "migrate" "20260613061102_split_name" {
migrate {
to = "20260613061046" # the version before the one under test
}
exec {
sql = "INSERT INTO users (name) VALUES ('Ada Lovelace')"
}
migrate {
to = "20260613061102" # the version under test
}
exec {
sql = "SELECT first_name, last_name FROM users"
output = "Ada,Lovelace"
}
}
```
Migration tests cannot run in parallel. Use `skip` or `--run` to test only the latest migration during
development: `atlas migrate test --env dev --run 20260613061102`.
Write a migration test whenever a migration moves or transforms data (backfills, splits, merges,
type conversions), and whenever `migrate lint` reports a data-dependent change (`MF*`).
## Plan Tests
A declarative plan file records `From` and `To` fingerprints of the schema transition. A plan test
brings the dev database to the `From` state with a `schema` block, seeds data, applies the plan, and
asserts. The `schema` block state must match the plan's `From`, or the case fails.
```hcl
# plan.test.hcl
test "plan" "20260613061102" {
schema {
url = "file://snapshots/schema.v1.sql" # or atlas://<repo>?tag=v1, or a data source
}
exec {
sql = "INSERT INTO users (name) VALUES ('Ada Lovelace'), ('Grace Hopper')"
}
apply {
url = "file://plans/20260613061102.plan.hcl"
}
exec {
sql = "SELECT first_name, last_name FROM users ORDER BY id"
format = table
output = <<-TAB
first_name | last_name
-----------+----------
Ada | Lovelace
Grace | Hopper
TAB
}
}
```
Run with `atlas schema plan test --env <name>` before approving the plan (`references/cloud.md`,
plan approvals).
## Script Tests
`test "script"` starts from a fresh database. Seed with `exec`, run the script with the `script`
command, and assert on what it prints or leaves behind. Details and the `as` block for privilege
testing are in `references/scripts.md`.
## Agent Workflow
1. Read `atlas.hcl` for the env's `test` block and the `dev` URL. Add the block if missing.
2. Write the logic (function, view, trigger, migration, or script) and a test file next to it.
3. Run the matching command with `--run` scoped to the new case:
`atlas schema test --env dev --run order_total_trigger`.
4. Read the failure. The output shows the case, the command index, and the actual vs expected
output or the SQL error.
5. Fix the logic (not the assertion) unless the assertion was wrong. Re-run.
6. Run the full suite once before reporting: `atlas schema test --env dev` and
`atlas migrate test --env dev`.
7. Report which cases were added and that they pass.
## Error Handling
| Error | Action |
|-------|--------|
| `command requires 'atlas login'` or `available only to Atlas Pro users` | Run `atlas login`; report the skipped test run and continue |
| `no test files found` | Add `test { <kind> { src = [...] } }` to the env, or pass the file path as an argument |
| Output mismatch on whitespace | `output` is compared after trimming newlines only. Use `format = table` for aligned output, or `match` for a regexp |
| Plan test: `From` does not match | The `schema` block state differs from the plan's source state. Point `schema.url` at the exact pre-plan snapshot |
| Migration test hangs on a version | The `migrate { to }` version must exist in the directory; check `atlas migrate ls --env <name>` |
## Documentation
- [Schema testing](https://atlasgo.io/testing/schema)
- [Migration testing](https://atlasgo.io/testing/migrate)
- [Plan testing](https://atlasgo.io/testing/plan)
- [Data Scripts testing](https://atlasgo.io/scripts/testing)
- [Test block reference](https://atlasgo.io/hcl/testing)
- [Template databases for schema tests (PostgreSQL)](https://atlasgo.io/guides/postgres/schema-test-template-databases)
- [Testing guides: functions, views, triggers, procedures, domains, data migrations](https://atlasgo.io/guides/testing/functions)
references/drift.md
# Drift Detection Reference
Schema drift is a difference between what a database actually contains and what its source of truth
says it should contain: the migration directory at the applied version, or the desired schema in
the declarative workflow. Drift comes from manual hotfixes, out-of-band tools, and partially failed
deployments. Atlas detects it in four places. Pick the one that matches the question.
| Question | Tool | Docs |
|----------|------|------|
| Does this database match its migration history, now or on a schedule (cron, CI job)? | `atlas migrate drift` (requires `atlas login`) | https://atlasgo.io/versioned/drift-detection#migrate-drift |
| Block a deployment if the target has drifted | `check "migrate_apply" { drift {} }` in `atlas.hcl` | https://atlasgo.io/versioned/drift-detection#pre-apply-check |
| How does this database differ from the desired schema (declarative) or from another database? | `atlas schema diff` | https://atlasgo.io/declarative/diff |
| Agent-based continuous monitoring of every database, alerts, and the diff in a UI | Schema Monitoring in Atlas Cloud | https://atlasgo.io/monitoring/drift-detection |
| Compliance framing (SOC 2, change management) | Drift detection guide | https://atlasgo.io/guides/drift-detection |
The declarative workflow is self-correcting: `atlas schema apply` plans from the live state every time,
so drift is folded into the next apply. The versioned workflow assumes the database is exactly where the
last migration left it, which is why the checks below exist.
## `atlas migrate drift` (versioned, on demand or scheduled)
Requires `atlas login`. Reads the revisions table of the connected database, resolves the state the migration directory defines
at the last applied version, inspects the database, and diffs the two. Files after the applied version
are pending, not drift, and are reported as ignored. The command never changes the database, and it
takes the same advisory lock as `migrate apply`, so an in-flight deployment is not reported as drift.
```bash
atlas migrate drift --env prod # registry mode: dir = atlas://<repo>
atlas migrate drift --url "$DATABASE_URL" --dir file://migrations --dev-url docker://postgres/17/dev
atlas migrate drift --env prod --format '{{ json . }}'
```
Expected state comes from the Atlas Registry when `migration.dir` is an `atlas://` URL (or
`migration.repo.name` is set). Otherwise pass `--dev-url` and Atlas computes it from the local directory.
```
Drift Status: OK
-- Current Version: 20260423120000
-- Expected State: atlas://my-app (registry)
-- Pending Files: 0
```
Exit codes: `0` no drift, `1` drift found (the report lists the objects and the SQL that would
reconcile them). This is the background check: run it from a cron job, a scheduled CI workflow, or a
Kubernetes CronJob and it fails on drift without parsing output, so the same command serves an ad hoc
question and continuous checking. On an env with
`for_each` (multi-tenant), one report is printed per target and the run fails if any target drifted.
| Flag | Env attribute | Notes |
|------|---------------|-------|
| `--url` | `url` | Required |
| `--dev-url` | `dev` | Required for a local directory |
| `--dir` | `migration.dir` | Defaults to `file://migrations` |
| `--exclude` | `migration.exclude`, else `exclude` | Objects to ignore, see Excluding Objects |
| `--format` | `format.migrate.drift` | Go template for the report |
| `--no-cache` | | Bypass the expected-state cache |
| `--lock-timeout`, `--lock-name`, `--skip-lock` | `migration.*` | Advisory lock shared with `migrate apply` |
## Pre-Apply Check (versioned, at deploy time)
Runs at the start of every `atlas migrate apply` for the env and aborts before any file runs if the
target has drifted from the expected state at its current revision. Requires the directory to be
pushed to the Atlas Registry and at least one applied revision (a fresh database skips the check).
```hcl
env "prod" {
url = getenv("DATABASE_URL")
migration {
dir = "atlas://my-app"
}
check "migrate_apply" {
drift {
on_error = FAIL # or CONTINUE: report and keep deploying
exclude = ["audit_log", "monitoring_*"]
}
}
}
```
Rollout on an existing environment: start with `on_error = CONTINUE`, read the reported diff, add a
migration for unintentional drift, add `exclude` patterns for intentional objects, then switch to `FAIL`.
## Excluding Objects
Objects that intentionally live outside the migration scope (manually installed extensions, audit or
sidecar tables owned by another service, schemas owned by another team) would be reported on every
run. Exclude them with glob patterns. The pattern format follows the URL scope of `env.url`:
```hcl
# Schema scope (URL names one schema): patterns match objects in that schema
exclude = ["audit_log", "monitoring_*", "*[type=policy|function]"]
# Database scope (URL covers the database): qualify with the schema
exclude = ["audit.*", "public.monitoring_*", "*.*[type=trigger]"]
```
The revisions table and its schema are excluded automatically by `migrate drift` and the pre-apply
check. `schema diff` does not exclude them: pass `--exclude atlas_schema_revisions`.
## `atlas schema diff` (any workflow, ad hoc)
Compares any two states: a live database, a migration directory, an HCL or SQL file, or an ORM. Use it
to answer "what would it take to make A look like B", including between two databases.
```bash
atlas schema diff --env <name> --from env://url --to file://schema.hcl --exclude atlas_schema_revisions
atlas schema diff --from "$PROD_URL" --to "$STAGING_URL" --dev-url docker://postgres/17/dev
atlas schema diff --from file://migrations --to file://schema.hcl --dev-url docker://postgres/17/dev
```
The output is the SQL that turns `--from` into `--to`; "Schemas are synced, no changes to be made" means
no drift. `--from env://url` reads the env's `url` so the database URL is never typed. `--to` is the
env's schema source (`schema.src` in `atlas.hcl`). On a versioned database add
`--exclude atlas_schema_revisions`, or the revisions table is reported as a table to drop.
## Schema Monitoring (Atlas Cloud, agent-based)
Atlas Cloud Schema Monitoring inspects databases on a schedule through a lightweight agent and reports
drift against a deployed migration target or against another database's snapshot. It shows the diff as
ERD, HCL, and SQL, and notifies Slack or a webhook when drift appears. It is set up in the Atlas Cloud
UI, not from the CLI: https://atlasgo.io/monitoring/drift-detection. Use it when the user wants an agent
watching many databases with alerts, rather than a scheduled `migrate drift` job they operate themselves.
`atlas cloud database list` (see `references/cloud.md`) reports deployment status (`SYNCED`,
`PENDING`, `FAILED`), which is a different question from drift: a database can be `SYNCED` at its
version and still have drifted objects.
## Agent Workflow: "Has this database drifted?"
1. Read `atlas.hcl`: versioned (`migration` block) or declarative (`schema` block)?
2. Versioned: `atlas migrate drift --env <name>`. Report the status, the current version, the pending
file count, and the reconciling SQL if drift was found.
3. Declarative: `atlas schema apply --env <name> --dry-run` and report the SQL Atlas would apply
("Schema is synced, no changes to be made" means no drift). Without a login the lint section of that
output is redacted ("Run atlas login ... to view diagnostics"); say so rather than reporting "no issues".
`atlas schema diff --env <name> --from env://url --to <schema.src>` is the same answer as plain SQL.
4. If drift is real, propose either a corrective migration (`atlas migrate diff` after fixing the
schema source) or, for intentional objects, an `exclude` entry. Do not run `atlas schema apply`
or `atlas migrate apply` to "fix" drift without the user's approval.
5. If the user wants this checked continuously, suggest `atlas migrate drift` on a schedule (cron or
CI) for versioned projects, the pre-apply check for deploys, and Schema Monitoring for agent-based
monitoring with alerts.
## Documentation
- [Drift detection for versioned migrations](https://atlasgo.io/versioned/drift-detection)
- [Schema diff](https://atlasgo.io/declarative/diff)
- [Schema Monitoring drift detection](https://atlasgo.io/monitoring/drift-detection)
- [Drift detection and compliance](https://atlasgo.io/guides/drift-detection)
- [Pre-execution checks in migrate apply](https://atlasgo.io/versioned/apply#pre-execution-checks)
references/versioned.md
# Versioned Migrations Reference (`atlas migrate`)
The versioned workflow keeps a migration directory of SQL files plus an `atlas.sum` integrity file.
`atlas migrate diff` plans new files from the desired schema, `atlas migrate lint` checks them,
`atlas migrate apply` runs pending files against a database and records each one in the
`atlas_schema_revisions` table. This reference covers diff generation in depth, directory
maintenance, and apply-time controls. Linting is in `references/lint.md`, tests in
`references/testing.md`, drift in `references/drift.md`, CI/CD in `references/cicd.md`.
## `atlas migrate diff`
Computes the diff between the migration directory (replayed on the dev database) and the desired
state, and writes a new migration file. Same schema change, same output, whichever model asked for it.
```bash
atlas migrate diff --env <name> "add_users_email" # everything from atlas.hcl
atlas migrate diff add_users_email \
--dir "file://migrations" \
--to "file://schema.hcl" \ # HCL, SQL file or directory, ORM, or a database URL
--dev-url "docker://postgres/17/dev?search_path=public"
atlas migrate diff --env <name> "name" --edit # open the generated file in $EDITOR before saving
atlas migrate diff --env <name> "name" --format '{{ sql . " " }}' # indented SQL
```
| Flag | Purpose |
|------|---------|
| `--to` | Desired state: `file://schema.hcl`, `file://schema.sql`, `file://schema/` (directory), an ORM loader (see `references/schema-sources.md`), or a live database URL (see Baseline) |
| `--dir` | Migration directory URL, default `file://migrations`. Append `?format=<fmt>` for other tools' layouts |
| `--dev-url` | Dev database. Scope must match the target (see `SKILL.md`, Dev Database) |
| `--schema`, `-s` | Limit to named schemas |
| `--qualifier` | Qualify table names with a custom schema name when working on a single schema |
| `--edit` | Edit the file before it is written; `atlas.sum` is updated after the editor closes |
| `--format` | Go template for the output |
| `--lock-timeout` | Wait for the directory lock (multi-writer CI) |
A no-op diff (directory already matches `--to`) prints "The migration directory is synced with the
desired state, no changes to be made" and creates nothing.
### Migration directory formats
Use the default `atlas` format. Atlas can also write `golang-migrate`, `goose`, `flyway`, `liquibase`,
and `dbmate` layouts (`migration { format = golang-migrate }` in `atlas.hcl`, or
`--dir "file://migrations?format=golang-migrate"`) so an existing deployer keeps working, but those
formats are limited and many Atlas features are not available on them. Read `atlas.hcl` before
generating a file: a file in the wrong format breaks the deployer.
### Diff policy
The `diff` block in `atlas.hcl` shapes what `migrate diff` (and `schema apply`) generates:
```hcl
variable "destructive" {
type = bool
default = false
}
env "local" {
diff {
skip {
drop_schema = !var.destructive # do not plan DROP SCHEMA unless --var destructive=true
drop_table = !var.destructive
}
concurrent_index { # PostgreSQL: CREATE/DROP INDEX CONCURRENTLY
add = true # the file gets "-- atlas:txmode none"
drop = true
}
materialized {
with_no_data = true # CREATE MATERIALIZED VIEW ... WITH NO DATA
}
add_table { if_not_exists = true }
drop_table { cascade = true, if_exists = true }
add_column { if_not_exists = true }
add_index { if_not_exists = true }
}
}
```
`atlas migrate diff --env local --var destructive=true` lets a single run plan the drops.
### Excluding objects from the diff
`migration { exclude = ["*.constraint_name", "audit_*"] }` tells `migrate diff` to ignore objects that
live in the directory but not in the desired schema (hand-written constraints, tables owned by another
tool). Use it only for objects that the schema source cannot express.
## Baseline an Existing Database
To adopt versioned migrations on a database that already has a schema. Start with an empty migration
directory: against an existing directory, step 1 writes an ordinary diff, not a baseline.
```bash
# 1. First migration = the current state. env://url reads the env's url so no database URL is typed.
atlas migrate diff baseline --env <name> --to env://url
# or: atlas schema inspect --env <name> --format '{{ sql . | split | write "src" }}' then --to file://src
# 2. On every existing database, mark the baseline as applied without running it.
atlas migrate apply --env <name> --baseline "<version-from-filename>"
# 3. New databases run it in full.
atlas migrate apply --env <name>
```
`--allow-dirty` lets `migrate apply` start on a non-empty database that has no revisions table when a
baseline is not wanted (for example, a database that only contains objects excluded from the schema).
## Directory Maintenance
| Command | When |
|---------|------|
| `atlas migrate hash --env <name>` | After any manual edit of a migration file. Recomputes `atlas.sum`; `apply`, `lint`, `diff`, and `new` refuse a stale sum (`checksum mismatch`). `migrate new` and `migrate diff` rewrite `atlas.sum` themselves |
| `atlas migrate new --env <name> "name"` | Create an empty file for hand-written SQL (data fixes, statements Atlas cannot plan). Add `--edit` to open it |
| `atlas migrate edit --env <name> <version>` | Open an existing file in `$EDITOR` and re-hash when it closes |
| `atlas migrate rebase --env <name> <version>` | Move a migration to the end of the directory after a merge brought newer files in. Only for a file no environment has applied: rebase renames the file without reading the revisions table, so an applied file would run twice. Check `atlas migrate status` first. Re-lint afterwards: rebase reorders, it does not re-plan |
| `atlas migrate rm --env <name> <version>` | Remove an unapplied local file and update `atlas.sum`. Not for remote directories |
| `atlas migrate checkpoint --env <name> [tag]` | Write a checkpoint file that captures the whole directory state so new databases skip earlier files. Requires `--dev-url` or `dev` |
| `atlas migrate validate --env <name>` | Check `atlas.sum` and that every file parses |
| `atlas migrate ls --env <name>`, `atlas migrate show <version>` | List files, print one |
| `atlas migrate set --env <name> <version>` | Overwrite the revisions table to say the database is at `version`. Recovery only, with the user's explicit approval |
| `atlas migrate import --from "file://migrations?format=flyway" --to "file://atlas-migrations"` | Convert another tool's directory to Atlas format |
Rules: never edit a migration that any environment has applied; add a new file. After editing an
unapplied file, run `hash`, then `lint`. When `atlas.sum` conflicts in git, take either side, then run
`atlas migrate hash` and `atlas migrate rebase <your-version>` so your file sorts after the merged ones.
## `atlas migrate apply`
Applies pending files in order and records each in the revisions table. Always `--dry-run` first on
shared environments and check Atlas Cloud state (`references/cloud.md`).
```bash
atlas migrate apply --env <name> --dry-run # prints the SQL with per-statement timings; nothing runs
atlas migrate apply --env <name> # all pending
atlas migrate apply --env <name> 1 # at most one file
atlas migrate apply --env <name> --to-version 20260301120000
atlas migrate apply --env <name> --format '{{ json . }}'
atlas migrate apply --url "$DATABASE_URL" --dir "atlas://app?tag=latest" # deploy from the registry
```
| Flag | Purpose |
|------|---------|
| `[amount]` | Apply at most N files |
| `--to-version` | Stop at this version |
| `--baseline` | First run on an existing database: mark this version applied without executing it |
| `--allow-dirty` | Start on a non-clean database that has no revisions table |
| `--tx-mode file\|all\|none` | One transaction per file (default), one for the whole run, or none. Per-file override: `-- atlas:txmode none` at the top of the file (needed for `CREATE INDEX CONCURRENTLY`) |
| `--exec-order linear\|linear-skip\|non-linear` | `linear` (default) fails when a file older than the current version is pending (out-of-order merge); `linear-skip` ignores it; `non-linear` applies it |
| `--revisions-schema` | Schema that holds `atlas_schema_revisions` |
| `--lock-timeout`, `--lock-name`, `--skip-lock` | Advisory lock so two deployers do not race |
| `--dry-run` | Print the SQL and the pre-checks without executing |
| `--format` | Go template; `{{ json . }}` for machine-readable output |
MySQL and other engines without transactional DDL cannot roll back a failed file completely; after a
failure there, compare `atlas migrate status` with the revisions table before retrying.
### Pre-execution checks
A `check "migrate_apply"` block in the env runs before any file executes. `allow` rules that evaluate
true pass; `deny` rules that evaluate true block the run with `message`. Rules see
`self.planned_migration.files` and `.statements`:
```hcl
env "prod" {
check "migrate_apply" {
deny "too_many_files" {
condition = length(self.planned_migration.files) > 3
message = "Apply at most 3 files per run. Split the deployment."
}
deny "no_index_in_peak_hours" {
condition = anytrue([for s in self.planned_migration.statements : regexmatch("(?i)create +index", s)])
&& tonumber(formatdate("HH", timestamp())) >= 10
&& tonumber(formatdate("HH", timestamp())) <= 14
message = "CREATE INDEX is blocked between 10:00 and 14:00 UTC"
}
drift { # see references/drift.md
on_error = FAIL
}
}
}
```
Migration files can also carry their own checks (`-- atlas:txtar` pre-migration checks that abort
the file when a condition holds) and hooks; see the docs links below.
### Down migrations
```bash
atlas migrate down --env <name> --dry-run # revert the last applied file
atlas migrate down --env <name> 2 # the last two
atlas migrate down --env <name> --to-version 20260301120000
atlas migrate down --env <name> --to-tag <registry-tag>
```
Atlas plans the reverse of each applied file on the dev database, and validates hand-written down
files when the directory has them. Reverting data-destroying files cannot restore data; say so
before running `down` on anything but a development database.
### Multi-tenant apply
One `env` block with `for_each` expands to one target per tenant:
```hcl
data "sql" "tenants" {
url = var.url
query = "SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant_%'"
}
env "prod" {
for_each = toset(data.sql.tenants.values)
url = urlqueryset(var.url, "search_path", each.value)
migration {
dir = "atlas://app"
}
}
```
`atlas migrate apply --env prod` then applies to every tenant and reports per target; Atlas Cloud
records the run as one multi-target deployment (`references/cloud.md`).
## Registry
```bash
atlas migrate push --env <name> app # push the directory; tag defaults to the git commit
atlas migrate push --env <name> app:v1.2.0 # explicit tag
atlas migrate apply --url "$URL" --dir "atlas://app?tag=v1.2.0"
atlas migrate apply --url "$URL" --dir "atlas://app?version=20260301120000"
```
Pushing makes the directory an immutable, versioned artifact that CD can deploy without the source
repo, and it is what `migrate lint` compares against by default and what the drift checks read.
## Agent Workflow: Generate a Migration
1. Read `atlas.hcl`: env name, `migration.dir` (and its `format`), `diff` policy, `dev`.
2. Edit the schema source, then `atlas schema validate --env <name>`.
3. `atlas migrate diff --env <name> "<snake_case_name>"`. Read the generated file back to the user.
4. `atlas migrate lint --env <name> --latest 1`. Fix findings in the schema source and regenerate
(delete the file first with `atlas migrate rm`, or edit and `hash`).
5. If the change moves data, add a `test "migrate"` case (`references/testing.md`) and run it.
6. `atlas migrate apply --env <name> --dry-run`, then apply on the development database.
7. Commit the migration file and `atlas.sum` together.
## Error Handling
| Error | Action |
|-------|--------|
| `checksum mismatch` / `atlas.sum` out of sync | `atlas migrate hash --env <name>` |
| `migration file ... was not applied in order` (out-of-order) | Newer files landed first. `atlas migrate rebase <version>` locally, or `--exec-order linear-skip` only with approval |
| `connected database is not clean` | The database has objects but no revisions table. Baseline it (`--baseline`) or, if intended, `--allow-dirty` |
| `The migration directory is synced with the desired state` | Not an error: nothing to generate. Check the schema edit was saved and `--to` points at it |
| `ModifySchema is not allowed` | Dev URL scope does not match the target scope |
| Statement failed mid-file on MySQL | The file is partially applied. Compare `atlas migrate status` with the database, fix forward with a new file |
## Documentation
- [Migration authoring: migrate diff](https://atlasgo.io/versioned/diff)
- [Migration apply](https://atlasgo.io/versioned/apply)
- [Pre-execution checks](https://atlasgo.io/versioned/apply#pre-execution-checks)
- [Pre-migration checks in files](https://atlasgo.io/versioned/checks)
- [Migration hooks](https://atlasgo.io/versioned/pre-post-hooks)
- [Down migrations](https://atlasgo.io/versioned/down)
- [Checkpoints](https://atlasgo.io/versioned/checkpoint)
- [Directory integrity (atlas.sum)](https://atlasgo.io/concepts/migration-directory-integrity)
- [Import from other tools](https://atlasgo.io/versioned/import)
- [Troubleshooting](https://atlasgo.io/versioned/troubleshoot)
references/declarative.md
# Declarative Workflow Reference (`atlas schema apply`, `atlas schema plan`)
In the declarative workflow the desired schema (HCL, SQL, ORM, or another database) is the source of
truth. `atlas schema apply` inspects the target, plans the SQL that moves it to the desired state,
asks for approval, and runs it. `atlas schema plan` moves the planning and approval earlier: the plan
is reviewed and approved in a pull request, stored in the Atlas Registry, and `schema apply` later runs
exactly those statements without re-planning.
## `atlas schema apply`
```bash
atlas schema apply --env <name> --dry-run # print the plan, run nothing
atlas schema apply --env <name> # plan, lint, ask, apply
atlas schema apply --env <name> --auto-approve # no prompt (development only)
atlas schema apply --env <name> --edit # edit the plan in $EDITOR before it runs
atlas schema apply --env <name> --plan "atlas://app/plans/<name>" # run a specific approved plan
atlas schema apply --url "$URL" --to file://schema.sql --dev-url docker://postgres/17/dev
```
| Flag | Purpose |
|------|---------|
| `--url`, `-u` | Target database |
| `--to` | Desired state: HCL, SQL, ORM loader, migration directory (`file://migrations`), or another database. Repeatable |
| `--dev-url` | Dev database used to normalize the desired state and to lint the plan |
| `--schema`, `--exclude`, `--include` | Limit the scope |
| `--dry-run` | Print SQL and the lint report, apply nothing |
| `--auto-approve` | Skip the prompt. Never on shared environments unless the user asks |
| `--plan` | Use a named pre-approved plan from the registry |
| `--tx-mode` | `file` (default: one transaction) or `none`. A plan directive `atlas:txmode none` has the same effect |
| `--format` | `{{ json . }}` for machine-readable output |
Approval happens in one of three ways:
1. Manual: Atlas prints the SQL and prompts. The default.
2. Review policy: `lint { review = ERROR }` (or `WARNING`) auto-approves plans that lint clean and
prompts only when the report has errors (or warnings). `ALWAYS` is the default and always prompts.
Requires `atlas login`: once the policy is in `atlas.hcl`, every `schema apply` for that env,
including `--dry-run`, aborts without one.
3. A pre-approved plan for this exact transition exists in the registry: Atlas applies it with no
prompt and no re-planning.
The `diff` block (`skip`, `concurrent_index`, `materialized`, `add_table`, `drop_table`, ...) shapes the
plan the same way it shapes `migrate diff`; see `references/versioned.md`, Diff policy. A
`check "schema_apply"` block with `allow`/`deny` rules and `lint` policy apply before the plan runs,
and a drift comparison against the desired state is inherent: every apply re-plans from the live state.
## `atlas schema plan`
A plan is a file with three attributes: `from` and `to`, fingerprints of the schema states, and
`migration`, the SQL. `schema apply` looks up an approved plan whose `from` matches the current state
and whose `to` matches the desired state, and runs its `migration` verbatim. If the database moved,
the `from` no longer matches and the plan is not used.
```hcl
plan "20260923085308" {
from = "vJYpErjN4kWJpw4nRaJcEX3xx/jExj4a05Ll3Y7gXr4="
to = "B5OVckDEeHcaSdYCUMEfYe8CZN85ahLkef44hfwCe2g="
migration = <<-SQL
-- Add column "email" to table: "users"
ALTER TABLE "users" ADD COLUMN "email" text NOT NULL DEFAULT 'unknown';
UPDATE "users" SET "email" = "name" || '@example.com' WHERE "email" = 'unknown';
SQL
}
```
Prerequisites: `atlas login` (every `schema plan` subcommand needs it, including `--dry-run` and
`--save`), and the env's `schema` block names a registry repo:
```hcl
env "dev" {
dev = "docker://postgres/17/dev?search_path=public"
schema {
src = "file://schema.sql"
repo {
name = "app"
}
}
}
```
Create the repo with the first `atlas schema push --env dev` (or `atlas cloud repo create --type schema`).
### Lifecycle
```bash
atlas schema plan --env dev # plan from the registry's latest state (or url) to src,
# lint it, prompt "Approve and push", store as APPROVED
atlas schema plan --env dev --pending # store as PENDING for someone else to approve
atlas schema plan --env dev --dry-run # print the plan only
atlas schema plan --env dev --save -o add_email.plan.hcl # write the file locally instead of pushing
atlas schema plan --env dev --edit # open in $EDITOR, then approve and push
atlas schema plan --env dev --name add_email # explicit plan name in the registry
atlas schema plan --env dev --from "$URL" # plan from a live database instead of the registry
atlas schema plan --env dev -d "atlas:txmode none" # add a directive to the plan
atlas schema plan lint --env dev --file file://add_email.plan.hcl
atlas schema plan validate --env dev --file file://add_email.plan.hcl # from/to still match?
atlas schema plan push --env dev --file file://add_email.plan.hcl # push an edited local plan
atlas schema plan push --env dev --file file://add_email.plan.hcl --pending
atlas schema plan pull --url "atlas://app/plans/add_email" > add_email.plan.hcl
atlas schema plan list --env dev # plans for the current transition
atlas schema plan list --env dev --pending # only the ones waiting for approval
atlas schema plan approve --url "atlas://app/plans/add_email"
atlas schema plan rm --url "atlas://app/plans/add_email"
atlas schema plan test --env dev # run test "plan" cases (references/testing.md)
atlas schema plan new --env dev --name add_email # like plan, always creates a new plan file
```
| Flag (plan, new, push, validate) | Purpose |
|------|---------|
| `--from`, `--to` | Override the transition states. `--from` defaults to the env `url`, then the registry's last known state; `--to` to `schema.src` |
| `--repo` | Registry repo URL when the env has no `schema.repo` |
| `--name`, `--name-format` | Plan name; format is a Go template, e.g. `plan_{{ slice .ToHash 0 8 }}` |
| `--pending` | Push in `PENDING` state |
| `--auto-approve` | Approve without the prompt |
| `--skip-lint` | Skip the lint step |
| `--directive`, `-d` | Add `atlas:nolint ...` or `atlas:txmode none` directives to the plan |
| `--push`, `--save`, `-o` | Push to the registry, or save to a file |
### Editing a plan
Three ways, all ending with the plan in the registry:
1. `atlas schema plan --env dev --edit`: edit in place, then approve and push.
2. `atlas schema plan --env dev --save`, edit the file, `atlas schema plan push --env dev --file file://<path>`.
3. `atlas schema plan pull --url atlas://app/plans/<name> > x.plan.hcl`, edit the `migration`
attribute only, `atlas schema plan push --env dev --file file://x.plan.hcl`.
Edit only `migration`. Atlas re-checks that the edited SQL still brings `from` to `to`; a plan that
lands somewhere else is rejected as drift. This is how a data backfill (`UPDATE ...`) is added to a
column-add plan so both run together at deploy time.
### Approval and multiple plans
- `atlas schema plan` pushes as `APPROVED` unless `--pending`. A `PENDING` plan is approved with
`atlas schema plan approve --url ...`, in the registry UI, or by the `schema/plan/approve` CI step.
- Protected flows in the registry restrict who may push schemas, push approved plans, or approve.
- If two approved plans exist for the same transition (one per environment, say), `schema apply`
aborts with "multiple pre-planned migrations were found". Pass `--plan <name>` or `rm` the extra one.
- Approve only when the user asks. Approving authorizes the SQL to run on the next apply.
## `atlas schema push`
```bash
atlas schema push --env dev app # push src to the registry, tag = git commit
atlas schema push --env dev app --tag v1.2.0
atlas schema push --env dev app --version 20260301120000 --desc "add email"
```
The pushed schema is what `schema plan` uses as the last known state, what `schema apply` can deploy
from (`--to "atlas://app?tag=latest"`), and what the Kubernetes operator and Terraform provider read.
## Agent Workflow
Local development (no registry):
1. Edit the schema source, `atlas schema validate --env <name>`.
2. `atlas schema apply --env <name> --dry-run`. Read the SQL to the user.
3. `atlas schema apply --env <name>` (or `--auto-approve` on a throwaway local database).
Reviewed change (registry, plan workflow):
1. Edit the schema source, validate, then `atlas schema plan --env <name> --dry-run` and show the SQL.
2. If the plan needs data statements, `--save`, add them to `migration`, `plan lint`, then `plan push`.
3. Push with `--pending` unless the user is the approver. Report the plan URL.
4. The user (or CI on merge) approves. Deploy with `atlas schema apply --env <prod>`; Atlas picks up
the approved plan for that transition.
5. To see what is waiting: `atlas schema plan list --env <name> --pending` (`references/cloud.md`).
## Error Handling
| Error | Action |
|-------|--------|
| `The plan "From" hash does not match the current state hash` | The database (or `--from`) is not at the plan's source state. Re-plan from the real database URL, or pass the same URL the plan was made with |
| `multiple pre-planned migrations were found in the registry` | `atlas schema apply --plan <name>` or `atlas schema plan rm --url ...` |
| Plan rejected after editing (drift) | The edited SQL does not reach `to`. Keep the DDL Atlas planned; add only data statements |
| `schema.repo` not set / repository not found | Add `schema { repo { name = "..." } }`, run `atlas schema push` once, or `--repo` |
| Prompt appears in CI | Set `lint { review = ERROR }` (needs `atlas login` in CI), pre-approve a plan, or pass `--auto-approve` only where the pipeline is the approver |
| `available only to Atlas Pro users` | `schema plan`, the review policy, and `schema test` need a login. Run `atlas login` |
## Documentation
- [Declarative apply](https://atlasgo.io/declarative/apply)
- [Review policy](https://atlasgo.io/declarative/apply#review-policy)
- [Pre-planning and approving migrations](https://atlasgo.io/declarative/plan)
- [Plan file reference](https://atlasgo.io/hcl/plan)
- [Plan testing](https://atlasgo.io/testing/plan)
- [Schema diff](https://atlasgo.io/declarative/diff)
- [Declarative CI/CD setup](https://atlasgo.io/declarative/setup-cicd)
references/cicd.md
# CI/CD Reference
Atlas ships first-party CI integrations for GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines,
and Azure DevOps, plus deployers for Kubernetes (operator, Helm, ArgoCD, Flux), Terraform, and plain
pipelines. The shape is the same everywhere: on a pull request, lint (or plan) and comment the report;
on merge, push the artifact to the Atlas Registry; on deploy, apply from the registry.
Every CI step needs an Atlas Cloud bot token in a secret (`ATLAS_TOKEN`), created in Atlas Cloud under
Settings > Bots, and an `atlas.hcl` committed to the repo with an env for CI (dev database, lint policy).
How `migrate lint` decides which files are "new" depends on whether the directory is pushed to the
Atlas Registry. Read `atlas.hcl` and pick the matching case:
```hcl
# Case 1 (most projects): the directory is pushed to the registry on merge.
# Lint compares the PR's directory with the latest pushed state. No git settings needed.
env "ci" {
dev = "docker://postgres/17/dev?search_path=public" # or a service container URL
migration {
dir = "file://migrations"
repo {
name = "app" # the registry repo
}
}
}
# Case 2: no registry. Lint compares the PR branch with the base branch in git.
env "ci" {
dev = "docker://postgres/17/dev?search_path=public"
migration {
dir = "file://migrations"
}
lint {
git {
base = "master"
}
}
}
```
With a registry repo configured, do not add `git { base }` or `--git-base`: the registry comparison is
what makes lint independent of branch history and what the drift checks read from.
Keep database URLs out of the workflow files. Declare them as variables that read the CI environment,
and set the secrets on the job:
```hcl
variable "url" {
type = string
default = getenv("DATABASE_URL") # target database, from a CI secret
}
variable "dev_url" {
type = string
default = getenv("DEV_URL") # dev database: a docker:// URL or a service container
}
env "ci" {
url = var.url
dev = var.dev_url
schema {
src = "file://schema.sql" # declarative source
repo {
name = "app"
}
}
migration {
dir = "file://migrations" # versioned directory; one env can serve both workflows
}
}
```
Engines without transactional DDL (ClickHouse, Databricks) need `tx-mode: none` on the apply steps,
or `migration { tx_mode = none }` in the env, so Atlas does not wrap files in a transaction the engine
cannot roll back.
The docs are the reference for every action and input (links at the end). Two runnable repositories
show both pipelines end to end as additional examples: ClickHouse, with the databases started by
`docker compose` in the job and their URLs loaded into `GITHUB_ENV`
(https://github.com/atlasdemos/clickhouse/tree/main/.github/workflows), and Databricks, with URLs
from secrets and `tx-mode: none` (https://github.com/atlasdemos/databricks/tree/main/.github/workflows).
## Versioned Pipeline
| Stage | Trigger | What runs | GitHub Action |
|-------|---------|-----------|---------------|
| Lint | pull request | Replays the files not yet in the registry (or, without a registry, the files added since the base branch) on a dev database, comments findings on the PR, fails on errors | `ariga/atlas-action/migrate/lint@v1` |
| Diff (optional) | pull request | Plans migrations from the schema source and commits them to the PR when the developer did not | `ariga/atlas-action/migrate/diff@v1` |
| Test (optional) | pull request | `migrate test` and `schema test` on the dev database, results on the PR | `ariga/atlas-action/migrate/test@v1`, `schema/test@v1` |
| Autorebase | push to a non-default branch | Moves the branch's migration files after the ones on the target branch and updates `atlas.sum` | `ariga/atlas-action/migrate/autorebase@v1` |
| Push | merge to main | Pushes the directory to the registry, tagged with the commit and `latest` | `ariga/atlas-action/migrate/push@v1` |
| Deploy | release / manual / GitOps | `migrate apply` from `atlas://app?tag=...` against the target | `ariga/atlas-action/migrate/apply@v1`, operator, Terraform |
```yaml
# .github/workflows/atlas-ci.yaml
name: Atlas CI
on:
push:
branches: [master]
pull_request:
paths: ['migrations/*', 'schema.sql', 'atlas.hcl']
permissions:
contents: read
pull-requests: write
jobs:
lint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # only needed without a registry (--git-base)
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: ariga/atlas-action/migrate/lint@v1
with:
env: ci
env:
GITHUB_TOKEN: ${{ github.token }} # lets the action comment on the PR
- uses: ariga/atlas-action/migrate/test@v1
with:
env: ci
push:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: ariga/atlas-action/migrate/push@v1
with:
env: ci
dir-name: app
# Small projects deploy in the same job, right after the push, as in the atlasdemos example:
# - uses: ariga/atlas-action/migrate/apply@v1
# with:
# env: ci # url comes from DATABASE_URL through atlas.hcl
```
Without an `env`, pass the inputs directly: `dir: file://migrations`, `dir-name: app`,
`dev-url: ${{ env.DEV_URL }}`, and for apply `url: ${{ secrets.DATABASE_URL }}`.
```yaml
# .github/workflows/atlas-deploy.yaml
name: Deploy Migrations
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: ariga/atlas-action/migrate/apply@v1
with:
url: ${{ secrets.DATABASE_URL }}
dir: atlas://app?tag=latest # or ?tag=<commit> for a pinned release
```
Action inputs worth knowing: `env` and `config` select the `atlas.hcl` env; `vars` passes
`--var` values as a JSON string; `dir-name` is the registry repo slug; `tag` pins a registry version;
`tx-mode`, `allow-dirty`, and `dry-run` map to the CLI flags; `working-directory` for monorepos.
### Automatic migration generation (`migrate/diff`)
When the schema is code (HCL, SQL, or ORM) and a developer changes it without generating the
migration, `migrate/diff` plans it on the PR and commits the file to the migration directory. It also
fails when the directory and the schema are out of sync. `dir`, `to`, and `dev-url` are required but
come from `atlas.hcl` when `env` is set.
```yaml
# .github/workflows/atlas-diff.yaml
name: Generate Migrations
on:
pull_request:
paths: ['schema.sql', 'migrations/*', 'atlas.hcl']
jobs:
migrate-diff:
permissions:
contents: write # push the generated file to the PR branch
pull-requests: write # comment on the PR
env:
GITHUB_TOKEN: ${{ github.token }}
runs-on: ubuntu-latest
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }} # a PAT, so the pushed commit triggers the lint workflow
fetch-depth: 0
- name: config git to commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- uses: ariga/atlas-action/migrate/diff@v1
with:
env: ci # or explicit dir, to, and dev-url inputs
```
A commit pushed with the default `github.token` does not trigger other workflows; use a personal
access token (`PAT` secret) so `migrate/lint` runs on the generated file.
### Migration conflicts (`migrate/autorebase`)
Migration history is linear. When two branches add files, the second to merge conflicts on
`atlas.sum`, because the file is a checksum of the directory and is only correct once Atlas recomputes
it for the final order. `migrate/autorebase` moves the branch's migration files after the ones on the
base branch and updates `atlas.sum`. It reorders files without reading the schema, so pair it with
`migrate/lint`, which replays the directory in its new order; a lint failure goes back to the
developer, who fixes it at the schema level and runs `atlas migrate hash`.
```yaml
# .github/workflows/atlas-rebase.yaml
name: Rebase Atlas Migrations
on:
push:
branches-ignore: [master] # every branch except the default one
jobs:
migrate-auto-rebase:
permissions:
contents: write # push the rebased files
runs-on: ubuntu-latest
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }} # so the pushed commit triggers the lint workflow
fetch-depth: 0 # the rebase needs the branch history
- name: config git to commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- uses: ariga/atlas-action/migrate/autorebase@v1
with:
base-branch: master
dir: file://migrations
```
Trigger it on `push`, not `pull_request`: a conflicted PR no longer fires `pull_request` workflows,
which is exactly the state this action fixes. Without the action, the developer resolves it locally:
take either side of `atlas.sum`, `atlas migrate rebase <version>`, `atlas migrate hash`, re-lint.
The same two actions exist for other platforms: `migrate-diff` on GitLab, Azure DevOps, and
Bitbucket; `migrate_autorebase` on CircleCI, and `migrate autorebase` on Azure DevOps and Bitbucket.
### Deploying from the registry
`atlas://app?tag=latest` deploys whatever CI pushed last; `?tag=<commit>` or `?version=<version>` pins a
release. The same URL works in the Kubernetes operator (`AtlasMigration` resource), the Terraform
provider (`atlas_migration` resource), Helm hooks, ArgoCD and Flux, and ECS or Fly.io tasks. Every
deploy is recorded in Atlas Cloud; `references/cloud.md` shows how to read it back.
Add drift protection to the deploy env: `check "migrate_apply" { drift { on_error = FAIL } }`
(`references/drift.md`), and pre-execution `deny` rules for batch size or peak hours
(`references/versioned.md`).
## Declarative Pipeline
| Stage | Trigger | What runs | GitHub Action |
|-------|---------|-----------|---------------|
| Plan | pull request | Plans the transition from the registry's last state (or the database URL) to `schema.src`, lints it, comments the SQL on the PR, stores it as `PENDING` | `ariga/atlas-action/schema/plan@v1` |
| Approve | merge to main | Approves the pending plan in the registry | `ariga/atlas-action/schema/plan/approve@v1` |
| Push | merge to main | Pushes the schema to the registry (`latest` tag) | `ariga/atlas-action/schema/push@v1` |
| Deploy | release / GitOps | `schema apply` finds the approved plan for the transition and runs it with no prompt | `ariga/atlas-action/schema/apply@v1`, operator, Terraform |
```yaml
# .github/workflows/atlas-plan.yaml
name: Plan Declarative Migrations
on:
pull_request:
paths: ['schema.sql', 'atlas.hcl']
push:
branches: [master]
paths: ['schema.sql', 'atlas.hcl']
permissions:
contents: read
pull-requests: write
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: ariga/atlas-action/schema/plan@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
env: ci
approve-push:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
- uses: ariga/atlas-action/schema/plan/approve@v1
with:
env: ci
- uses: ariga/atlas-action/schema/push@v1
with:
env: ci
# Deploy on merge: apply runs the plan approved above with no prompt.
# Move this step to a separate deploy workflow for staged rollouts.
- uses: ariga/atlas-action/schema/apply@v1
with:
env: ci
```
The order on merge is approve, push, apply: approve turns the PR's `PENDING` plan into the approved
plan for this transition, push records the new desired state, and apply finds that plan and runs it.
Rules that keep the plan usable at deploy time:
- `schema/plan`, `schema/plan/approve`, and `schema/apply` must see the same database state. Either give
all three the same `url` (env var or `from` input) or let all three use the registry's last known
state. A mismatch fails with `The plan "From" hash does not match the current state hash`.
- Directives can be added from the PR description: `/atlas:nolint destructive` and
`/atlas:txmode none` are parsed by `schema/plan` and written into the plan.
- Ad-hoc approval: when `schema apply` runs in CD and no approved plan matches (a manual change
drifted the database, or the change skipped CI), the `lint { review }` policy decides whether it
pauses. With `review = ALWAYS` on `prod` it pauses and prints a registry link where a human approves.
## Other CI Platforms
| Platform | Integration | Docs |
|----------|-------------|------|
| GitLab CI | `migrate-lint`, `migrate-push`, `migrate-diff`, `schema-plan`, `schema-push` components | https://atlasgo.io/integrations/gitlab-ci-components |
| CircleCI | `atlas-orb` (`migrate_lint`, `migrate_push`, `migrate_autorebase`, `schema_plan`) | https://atlasgo.io/integrations/circleci-orbs |
| Bitbucket | `migrate/lint`, `migrate/push`, `migrate/autorebase`, `schema/plan`, `schema/push` pipes | https://atlasgo.io/integrations/bitbucket-pipes |
| Azure DevOps | `migrate lint`, `migrate push`, `migrate diff`, `migrate autorebase`, `schema plan`, `schema push` tasks | https://atlasgo.io/integrations/azure-devops |
| Kubernetes | Atlas Operator: `AtlasMigration` (versioned) and `AtlasSchema` (declarative) resources | https://atlasgo.io/integrations/kubernetes |
| Terraform | `atlas_migration` and `atlas_schema` resources | https://atlasgo.io/integrations/terraform-provider |
| ArgoCD / Flux | GitOps deployment with the operator | https://atlasgo.io/guides/deploying/k8s-argo, https://atlasgo.io/guides/deploying/k8s-flux |
| Go SDK | `atlasexec` for programmatic apply | https://atlasgo.io/integrations/go-sdk |
The CLI equivalents run anywhere: `atlas login --token "$ATLAS_TOKEN"`, then the same `migrate lint`,
`migrate push`, `schema plan`, `schema push`, and `migrate apply` / `schema apply` commands with
`--env ci` and `--format '{{ json . }}'` for machine-readable reports.
## Agent Workflow: Set Up CI/CD
1. Read `atlas.hcl` and decide versioned or declarative (`SKILL.md`, Choosing a Workflow).
2. Confirm a registry repo exists (`atlas cloud repo list`, `references/cloud.md`) or create it with the
first `migrate push` / `schema push`. Confirm `ATLAS_TOKEN` is available as a secret.
3. Add an `env "ci"` with a dev database and lint policy. Prefer `docker://` dev URLs on hosted runners
that have Docker; otherwise a service container.
4. Write the PR workflow (lint or plan, tests) and the merge workflow (push, plus approve for
declarative). Trigger paths on the migration directory, schema source, and `atlas.hcl`.
5. Write the deploy step from `atlas://<repo>` with drift and pre-execution checks on the deploy env.
6. Verify with a dry run: `atlas migrate lint --env ci` locally (`--git-base master` only without a registry), and a `--dry-run`
apply against a staging database.
7. Check the first CI deployment in Atlas Cloud: `atlas cloud migration list --repo <slug>`.
## Documentation
- [Versioned CI/CD setup](https://atlasgo.io/versioned/setup-cicd)
- [Declarative CI/CD setup](https://atlasgo.io/declarative/setup-cicd)
- [GitHub Actions reference](https://atlasgo.io/integrations/github-actions)
- [atlas-action repository, every action with inputs and examples](https://github.com/ariga/atlas-action)
- [Additional example: both pipelines on ClickHouse](https://github.com/atlasdemos/clickhouse/tree/main/.github/workflows)
- [Additional example: both pipelines on Databricks](https://github.com/atlasdemos/databricks/tree/main/.github/workflows)
- [Pre-approval workflow on GitHub Actions](https://atlasgo.io/integrations/github-actions/pre-approval)
- [Ad-hoc approval on GitHub Actions](https://atlasgo.io/integrations/github-actions/ad-hoc-approval)
- [Kubernetes operator](https://atlasgo.io/integrations/kubernetes)
- [Deployment guides](https://atlasgo.io/guides/deploying/intro)
- [Bot tokens](https://atlasgo.io/cloud/bots)
references/cloud.md
# Atlas Cloud Reference (`atlas cloud`)
`atlas cloud` commands read and manage resources in Atlas Cloud: the Atlas Registry (repos), the
databases those repos are deployed to (targets), and the deployment events recorded each time
`atlas migrate apply` or `atlas schema apply` reports to the cloud. They do not connect to databases.
Use them for operational visibility and pre-flight checks: infrastructure summaries, database status,
failed deployments, and what is waiting to be deployed. Use local commands (`atlas migrate status`,
`atlas schema diff`, `atlas migrate apply`) to inspect or change an actual database.
## Prerequisites
Every `atlas cloud` command requires an Atlas Cloud login. Check first, and stop if it fails:
```bash
atlas whoami # current user and org
atlas login # interactive login
atlas login --token "$TOKEN" # CI / non-interactive; ATLAS_TOKEN env var also works
```
The error `command requires 'atlas login'` means no session exists. `login with token failed:
Unauthorized` means a stale token: run `atlas logout`, then `atlas login`.
## Command Tree
```
atlas cloud
├── repo
│ ├── list Registry repos with synced / failed / pending database counts
│ ├── describe One repo: slug, type, driver, URL, database counts
│ ├── create Create a repo before the first push
│ ├── lingraph Lineage graph of a repo (object dependencies)
│ └── secgraph Security graph of a repo (roles, grants, access paths)
├── database
│ ├── list Every tracked database: env, status, current version
│ └── describe One database, including last deployment time
└── migration
├── list Deployment events, filterable by status, repo, env, database name
└── describe One deployment event, including per-target results
```
Every command accepts `--format <go-template>`. Use `--format '{{ json . }}'` for machine-readable
output. List commands accept `--page <n>`.
### `atlas cloud repo`
| Command | Flags |
|---------|-------|
| `repo list` | `--page` |
| `repo describe` | exactly one of `--id`, `--slug`, `--name` |
| `repo create` | `--type schema` (or `s`) / `--type migration_directory` (or `m`), `--name`, `--driver` (`postgres`, `mysql`, `mariadb`, `sqlite`, `mssql`, `clickhouse`, `cockroach`, ...), optional `--description`, `--skip-if-exists` |
| `repo lingraph` | `--slug`, optional `--open-lineage` for OpenLineage format |
| `repo secgraph` | `--slug` |
A repo is either a schema repo (declarative, pushed with `atlas schema push`) or a migration
directory (versioned, pushed with `atlas migrate push`). `repo list` and `repo describe` show how many
of the repo's databases are synced, pending, or failed.
### `atlas cloud database`
| Command | Flags |
|---------|-------|
| `database list` | `--env-name <env>` |
| `database describe` | exactly one of `--id`, `--ext-id` |
`database list` columns: `ID`, `NAME`, `ENV`, `STATUS`, `CURRENT VERSION`. `database describe` adds
`Last Deployment Time`.
| Status | Meaning |
|--------|---------|
| `SYNCED` | The database is on the latest version of its repo |
| `PENDING` | The database is behind the latest version pushed to the registry. A deployment is waiting to run |
| `FAILED` | The last deployment to this database failed |
Environment names match the `env` block names in `atlas.hcl` (`dev`, `staging`, `prod`; names vary
per project).
### `atlas cloud migration`
A migration event is one deployment: a `migrate apply` or `schema apply` reported to the cloud.
| Command | Flags |
|---------|-------|
| `migration list` | `--status` (repeatable: `PASSED`, `FAILED`, `NO_ACTION`, `DRY_RUN`), `--repo <slug>`, `--env-name <env>`, `--name <substring>` (database name contains), `--page` |
| `migration describe` | `--id <event-id>` |
`migration list` columns: `ID`, `REPO`, `TYPE`, `ENV`, `DATABASE`, `TARGETS`, `VERSION`, `STATUS`.
Multi-target deployments (multi-tenant) show `(multiple)` under `DATABASE` and a `succeeded/total` ratio
under `TARGETS`. `migration describe` adds `Completed At` and `Targets 8/8 succeeded`.
| Status | Meaning |
|--------|---------|
| `PASSED` | Deployment succeeded |
| `FAILED` | Deployment failed on at least one target |
| `NO_ACTION` | The target was already up to date, nothing ran |
| `DRY_RUN` | Preview only, nothing applied |
### Pagination
List commands return 20 records per page and end with a footer:
```
--------------------------------
Page: 1 Page Size: 20 Total: 45
```
If `Total > Page Size`, fetch `--page 2`, `--page 3`, ... until `Page == ceil(Total / Page Size)`.
Do not summarize a list as complete until every page is read. With `--format '{{ json . }}'` the same
metadata is in `PageInfo` (`Page`, `PageSize`, `Total`).
## Answering Operational Questions
| Question | Commands |
|----------|----------|
| Summarize our database infrastructure | `atlas whoami`, `atlas cloud repo list`, `atlas cloud database list` (all pages) |
| What is the status of our databases? | `atlas cloud database list`, optionally `--env-name <env>` |
| Which databases are waiting for a deployment? | `atlas cloud database list`, keep rows with `STATUS=PENDING` |
| Which deployments failed? | `atlas cloud migration list --status FAILED` (add `--env-name`, `--repo`, or `--name`) |
| Why did a deployment fail? | `atlas cloud migration describe --id <id>`, then `atlas cloud database describe --id <db>` |
| What version is production on? | `atlas cloud database list --env-name prod` |
| Is staging ahead of production? | `atlas cloud database list --env-name staging` vs `--env-name prod`, compare `CURRENT VERSION` |
| What ran in the last release? | `atlas cloud migration list --repo <slug> --status PASSED` |
| Which plans are waiting for approval? | `atlas schema plan list --env <name> --pending` (declarative repos) |
| What depends on this table? | `atlas cloud repo lingraph --slug <slug>` |
| Who can access what in this schema? | `atlas cloud repo secgraph --slug <slug>` |
| Is a repo healthy? | `atlas cloud repo describe --slug <slug>`, check the failed and pending counts |
### Infrastructure summary
When asked to summarize the infrastructure, status, or health of the account:
1. `atlas whoami` to name the org.
2. `atlas cloud repo list` for every repo and its synced / pending / failed counts.
3. `atlas cloud database list`, all pages, grouped by `ENV`.
4. `atlas cloud migration list --status FAILED`, all pages, for open failures.
5. For each repo that is a schema repo with a `plan` workflow, `atlas schema plan list --env <name> --pending`.
Report in this shape:
```
Org: acme (atlas whoami)
Repos (3): payments (migration dir, postgres), identity (schema, postgres), analytics (migration dir, clickhouse)
Databases (18):
ENV SYNCED PENDING FAILED
prod 6 1 1
staging 5 0 0
dev 5 0 0
Waiting for deployment (PENDING):
prod-eu payments prod on 20260512141500
Failed deployments:
#131 payments prod prod-us 20260514083000 FAILED 2026-05-15T09:30:00Z
Pending approvals: identity has 1 plan awaiting approval (atlas://identity/plans/20260515_add_index)
Next step: investigate event #131 (atlas cloud migration describe --id 131) before the next prod deploy.
```
State the filters used and which pages were fetched. Highlight `FAILED` first, `PENDING` second.
### "Who is waiting for deployment?"
Two different things can be waiting:
- A database with `STATUS=PENDING`: the registry has a newer version than the database. Deploying
(`atlas migrate apply` with `dir = "atlas://<repo>"`, or the CI/CD pipeline, or the Kubernetes
operator) will bring it forward. List them with `atlas cloud database list` and filter on `PENDING`.
- A declarative plan awaiting approval: `atlas schema plan list --env <name> --pending`. Approve with
`atlas schema plan approve --url "atlas://<repo>/plans/<name>"` only when the user asks; approval
authorizes the exact SQL in the plan to run on the next `atlas schema apply`.
Report both, and say which kind each item is.
## Agent Workflows
### Pre-deployment readiness
Before recommending `atlas migrate apply`, `atlas schema apply`, or a CI deploy to a shared environment:
```
- [ ] atlas whoami
- [ ] atlas cloud database list --env-name <env>
- [ ] atlas cloud migration list --status FAILED --env-name <env>
- [ ] atlas cloud repo describe --slug <slug>
- [ ] atlas migrate status --env <env> # live database, local command
```
Decision rules:
- Any target `FAILED`: investigate before applying. `migration list --status FAILED` filtered to the
env or repo, then `migration describe --id <id>`.
- Any target `PENDING`: a deployment may be in flight or a version is waiting. Confirm with the user
before starting another.
- All targets `SYNCED` and no recent `FAILED` events: cloud state is healthy. Still run
`atlas migrate status` and `atlas migrate lint` locally before applying.
- `repo describe` shows failed databases > 0: drill into `database list` and `migration list`.
### Investigate a failed deployment
1. `atlas cloud migration list --status FAILED --repo <slug>` (narrow with `--env-name` or `--name`).
2. `atlas cloud migration describe --id <id>` for the version, completion time, and per-target results.
3. `atlas cloud database describe --id <id>` on each affected target: current version vs expected.
4. `atlas migrate status --env <env>` to compare the live revision table with the cloud record.
5. Report: what failed, on which targets, at which version, and whether the database is now behind
(`PENDING`) or stuck (`FAILED`).
### Audit environments
Read `atlas.hcl` for the `env` block names. For each:
```bash
atlas cloud database list --env-name <env>
atlas cloud migration list --env-name <env>
```
Compare `CURRENT VERSION` across environments to spot version skew. Production behind staging is
normal; production ahead of staging violates promotion policy and should be flagged.
### Set up a new registry repo
1. `atlas cloud repo create --type migration_directory --name <name> --driver postgres`
(or `--type schema` for the declarative workflow).
2. Note the returned slug and URL.
3. Reference it in `atlas.hcl`: `migration { dir = "atlas://<slug>" }` or `schema { repo { name = "<slug>" } }`.
4. Push with `atlas migrate push --env <name>` or `atlas schema push --env <name>`.
### Environment promotion in atlas.hcl
The same database statuses are available inside `atlas.hcl` through the `cloud_databases` data source,
which lets a `prod` env pin `to_version` to the version already running in a lower environment:
```hcl
data "cloud_databases" "staging" {
repo = "payments"
env = "staging"
}
env "prod" {
url = getenv("DATABASE_URL")
migration {
dir = "atlas://payments"
to_version = data.cloud_databases.staging.targets[0].current_version
}
}
```
## Cloud vs Local Commands
| Concern | Atlas Cloud (`atlas cloud ...`) | Local (`atlas migrate/schema ...`) |
|---------|-------------------------------|-----------------------------------|
| Registry repos and artifacts | `repo list`, `repo describe` | `migrate push`, `schema push` |
| Tracked database status and version | `database list`, `database describe` | |
| Deployment history | `migration list`, `migration describe` | |
| Plans awaiting approval | | `schema plan list --pending` |
| Live database revision table | | `migrate status` |
| Schema drift vs desired state | | `schema diff`, `migrate lint` |
| Apply changes to a database | | `migrate apply`, `schema apply` |
Cloud commands answer "what does Atlas Cloud know?" Local commands answer "what is actually in the
database?" Use both when validating readiness, and keep the two apart in the report.
## Reporting Results
1. State the org (`atlas whoami`), the filters used, and how many pages were read.
2. Lead with `FAILED`, then `PENDING`, then the healthy summary.
3. For deployment events include `ID`, `REPO`, `ENV`, `VERSION`, `STATUS`, and `TARGETS` when present.
4. Distinguish cloud-reported state from live database state. Recommend `atlas migrate status` when the
user plans to apply changes.
5. Do not approve plans, create repos, or apply migrations from a status request. Suggest the command
and let the user decide.
## Error Handling
| Error | Action |
|-------|--------|
| `command requires 'atlas login'` | Run `atlas login`, or set `ATLAS_TOKEN` and run `atlas login --token` |
| `login with token failed: Unauthorized` | `atlas logout`, then `atlas login` |
| `repository not found` / `database not found` / `migration not found` | Verify the identifier. Run the matching `list` command first to get the ID or slug |
| `invalid --status value` | Use `PASSED`, `FAILED`, `NO_ACTION`, or `DRY_RUN` |
| `invalid --type` | Use `schema`/`s` or `migration_directory`/`m` |
| Mutually exclusive flags (`--id` vs `--slug`) | Provide exactly one identifier |
## Documentation
- [Inspecting deployments from the CLI](https://atlasgo.io/cloud/deployment#inspecting-deployments-from-the-cli)
- [CLI reference: atlas cloud](https://atlasgo.io/cli-reference#atlas-cloud)
- [Declarative plan and approval](https://atlasgo.io/declarative/plan)
- [Environment promotion](https://atlasgo.io/guides/environment-promotion)
- [Schema registry](https://atlasgo.io/cloud/features/registry)
references/scripts.md
# Data Scripts Reference (`atlas script`)
Data Scripts are HCL files that describe data operations: transactional mutations, reads and reports,
and batched loops. Atlas runs them with `atlas script exec`, `atlas script query`, and `atlas script loop`,
with transactions, guards, assertions, and output masking built in. They are Day 2 work that comes after
the schema is in place, run on demand, and not recorded in the migration history.
Run `atlas login` before using them.
## When to Use a Script
| Task | Use |
|------|-----|
| Backfill a column, re-key rows, re-encrypt a field | `script "exec"` or `script "loop"` |
| Delete or anonymize a user's data in bounded batches | `script "loop"` |
| Print a report, health check, or masked export | `script "query"` |
| Verify an invariant on a schedule (probe / monitor) | `script "exec"` with only `assert` blocks, or `script "query"` |
| Change the schema, or data that must ship with a schema change | A migration (`atlas migrate diff`), not a script |
| Lookup and reference rows that must match a desired state everywhere | The declarative `data` block, not a script |
A migration is applied once per database, in order, and Atlas records that it ran. A script runs every
time it is invoked, so the script carries its own safety: `condition` guards, `expect_rows`, and batch
bounds.
## Commands
```bash
atlas script exec --url "$URL" --file "file://scripts.hcl" --run '^archive_user$'
atlas script query --url "$URL" --file "file://scripts.hcl" --run '^vip_count$' --quiet
atlas script loop --url "$URL" --file "file://scripts.hcl" --run '^gdpr_purge$'
atlas script test --env <name> # run test "script" cases on the dev database
atlas script push --file "file://scripts" <repo> # publish to the Atlas Registry
```
Each verb runs only scripts of its own kind. Prefer `--env <name>`: the selected env supplies `url`
and the script source from its `script { src = "file://scripts" }` block, so `--file` becomes optional.
| Flag | Purpose |
|------|---------|
| `--url` | Runtime database the script runs against (omit when `--env` provides it) |
| `--file` | Script source: one file, a directory of `*.script.hcl` files, or a pushed `atlas://<repo>` archive |
| `--run <regexp>` | Select scripts by name. Unanchored: `--run purge` also matches `purge_v2`. Anchor it: `'^purge$'` |
| `--var name=value` | Bind a `variable` declared in the script |
| `-q`, `--quiet` | Print only the script's product (query sections and `output` lines), no streaming report |
| `--format '{{ json . }}'` | Render the run report through a Go template, for machine-readable results |
`--run` rules:
- Every match runs. One failure does not stop the others; errors are reported together at the end, and
scripts that already committed stay committed.
- A pattern that matches nothing prints `No scripts found to run` and exits `0`. In CI, assert on the
output, not the exit code.
- Omit `--run` and every script of that kind in the source runs.
## File Structure
A script file is a flat list of top-level blocks: runnable `script` wrappers plus the `variable`,
`locals`, and `mask` declarations they reference. Every script accepts a `description`.
```hcl
variable "days" {
type = number
default = 30
}
script "exec" "cancel_pending" {
description = "Cancel orders left pending for more than var.days days"
exec {
sql = "UPDATE orders SET status = 'canceled' WHERE status = 'pending' AND created_at < now() - ($1 || ' days')::interval"
args = [var.days]
}
}
```
Variables bind from `--var`, or from extra attributes on the selected `env` block (the env wins), with
`default` used when neither is set.
### SQL placeholders
Atlas never parses or rewrites SQL. Placeholders are driver-native and `args` bind scalars only:
| Driver | Placeholder | Expand a JSON list with |
|--------|-------------|-------------------------|
| PostgreSQL | `$1`, `$2` | `json_array_elements_text($1)` |
| MySQL | `?` | `JSON_TABLE(?, ...)` |
| SQLite | `?` | `SELECT value FROM json_each(?)` |
| SQL Server | `@p1` | `OPENJSON(@p1)` |
| ClickHouse | `?` | `arrayJoin(JSONExtract(?, 'Array(Int64)'))` |
| Oracle | `:1` | `JSON_TABLE(:1, ...)` |
To pass a list, `jsonencode` it: `args = [jsonencode(query.ids.rows[*].id)]`.
## `script "exec"`: Transactional Mutations
Blocks run in written order inside one transaction. A run ends in one of three ways: the body completes
and commits; an unmet `condition` or a fired `break` stops the script and commits the work done so far;
a failing `assert` or `check` aborts the run and rolls it back. The body needs at least one `exec`,
`assert`, or `check`.
| Block | Purpose |
|-------|---------|
| `tx { mode, on_error }` | `mode = AUTO` (default, one transaction) or `NONE`; `on_error = ROLLBACK` (default) or `COMMIT` |
| `condition "<name>" { sql }` | Pre-flight guard, only at the start of the body. Falsy (`false`/`0`) stops gracefully; `NULL` is an error. Wrap nullable expressions in `COALESCE(..., 0)` |
| `break "<name>" { sql \| expr }` | Stops mid-body when true, commits work so far. `expr` evaluates in-process, e.g. `length(query.pending.rows) == 0` |
| `assert "<name>" { sql, error_message }` | Invariant check. Falsy or `NULL` fails the run and rolls back |
| `check "<name>" { sql, output \| match, format }` | Compares serialized query output (CSV default, or TABLE) to an exact `output` or a regexp `match` |
| `exec "<name>" { sql, args, expect_rows }` | The write. `expect_rows` asserts the affected row count; a mismatch aborts |
| `query "<name>" { sql, args, rows { col = type } }` | Bound read: captures rows for later blocks as `query.<name>.rows[*].<col>` |
| `output { message }` | A line printed as the script's product, interpolating `${...}` |
| `http { ... }` | Call an external endpoint from the script |
```hcl
script "exec" "archive_user" {
condition "is_active" {
sql = "SELECT COALESCE(status = 'active', 0) FROM users WHERE id = $1"
args = [var.user_id]
}
exec "archive" {
sql = "UPDATE users SET status = 'archived' WHERE id = $1"
args = [var.user_id]
expect_rows = 1
}
assert "archived" {
sql = "SELECT status = 'archived' FROM users WHERE id = $1"
args = [var.user_id]
error_message = "user was not archived"
}
}
```
An exec script made of only `assert` blocks is a smoke test or post-deploy verifier: it writes nothing
and fails when an invariant no longer holds.
## `script "query"`: Reads and Reports
Read-only, no write transaction. Each inner `query` runs in written order and prints its result as a
section. Use it for recurring reports, health checks, masked exports, and reads that feed a later query.
| Block | Purpose |
|-------|---------|
| `query "<name>" { sql, args, format }` | Prints the result. `format = TABLE` (default, aligned grid) or `CSV` (no header; a single scalar prints bare) |
| `query "<name>" { sql, rows { col = type } }` | Bound query: prints nothing, exposes `query.<name>.rows`. Mutually exclusive with `format` and `mask` |
| `break "<name>" { sql \| expr, message }` | Ends the run early, e.g. when a report has nothing to say; sections already printed stay printed |
| `output { message }` | A custom line composed from the results |
| `mask { columns, method }` | Redacts result columns before they are printed (see Masking) |
| `http { ... }` | Calls an endpoint; a trailing `http` block posts the report (Slack, webhook, incident tool) |
```hcl
script "query" "health" {
query "orphans" {
sql = "SELECT count(*) AS orphan_orders FROM orders WHERE user_id NOT IN (SELECT id FROM users)"
format = CSV
}
query "top_ids" {
sql = "SELECT user_id FROM orders GROUP BY user_id ORDER BY sum(total) DESC LIMIT 5"
rows {
user_id = int
}
}
query "top_detail" {
sql = "SELECT email, plan FROM users WHERE id IN (SELECT value FROM json_each(?))"
args = [jsonencode(query.top_ids.rows[*].user_id)]
format = TABLE
mask {
columns = ["email"]
method = PARTIAL
keep_left = 2
keep_right = 4
}
}
output {
message = "top ${length(query.top_ids.rows)} spenders shown above"
}
}
```
Run with `--quiet` to get only the sections and `output` lines, or `--format '{{ json . }}'` for the
full report as JSON.
## `script "loop"`: Batched Work
Runs a `do` body repeatedly, once per page of an optional source, each iteration in its own transaction
by default. Use it for data migrations too large for one statement: purges, backfills, re-encryption.
Body order is enforced by the parser:
1. Pre-loop: zero or more `condition` blocks, run once before the iterator opens.
2. The loop: exactly one `do { }` and at most one `iterator "<mode>" { }`.
3. Post-loop: zero or more `assert` / `check` blocks, run once after the loop ends, outside any transaction.
`iterator` modes:
- `iterator "keyset"`: you write the paginated SELECT with a `> cursor` seek and `LIMIT`; Atlas threads
the cursor. Sub-blocks: `cursor { col = type }`, `batch { col = type }` (the page), `init { sql }`
(iteration 1), `next { sql, args = [cursor.col] }` (iterations 2+). The page is exposed as
`iterator.keyset.batch[*].<col>`.
- `iterator "range" { from, to, step }`: walks a numeric interval; the chunk is exposed as
`iterator.range.from` / `iterator.range.to`.
- No iterator: the `do` body repeats until a `break` fires or `policy.schedule` bounds it. `self.index`
is the 0-based iteration counter.
`do` commands, run in written order: `exec`, `query` with `rows`, `assert`, `check`, `break`, `continue`,
`log`, `output`, `sleep`, `http`, and an explicit `tx` command. `break` stops the whole loop, `continue`
skips the rest of the iteration; both commit the work done so far. `on_error = ABORT` (default) or
`CONTINUE` decides whether the loop proceeds after an iteration fails.
`policy` sub-blocks (all optional):
- `tx { mode = PER_ITERATION | MANUAL }`: `PER_ITERATION` (default) wraps each iteration in one transaction.
`MANUAL` autocommits unless grouped in a `do`-body `tx` command. `http` and the `tx` command require `MANUAL`.
- `schedule { every, limit, timeout, pause_when }`: fixed delay between iterations, max iterations,
wall-clock bound, and a boolean SQL probe (replica lag, connection count) that pauses the loop while true.
- `ramp { stage { size, iterations | hold } ... }`: staged batch-size ramp-up. The current size is
available in iterator SQL as `${ramp.size}`.
```hcl
script "loop" "purge_inactive" {
condition "have_inactive" {
sql = "SELECT count(*) > 0 FROM users WHERE active = false"
}
iterator "keyset" {
cursor { id = int }
batch { id = int }
init {
sql = "SELECT id FROM users WHERE active = false ORDER BY id LIMIT ${ramp.size}"
}
next {
sql = "SELECT id FROM users WHERE active = false AND id > $1 ORDER BY id LIMIT ${ramp.size}"
args = [cursor.id]
}
}
do {
exec {
sql = "DELETE FROM posts WHERE user_id IN (SELECT value::int FROM json_array_elements_text($1))"
args = [jsonencode(iterator.keyset.batch[*].id)]
}
exec {
sql = "DELETE FROM users WHERE id IN (SELECT value::int FROM json_array_elements_text($1))"
args = [jsonencode(iterator.keyset.batch[*].id)]
}
log {
message = "batch ${self.index}: purged ${length(iterator.keyset.batch)} users"
}
}
assert "all_gone" {
sql = "SELECT count(*) = 0 FROM users WHERE active = false"
}
policy {
schedule {
every = "200ms"
timeout = "30m"
}
ramp {
stage {
size = 100
iterations = 3
}
stage { size = 1000 }
}
}
}
```
## Masking
A `mask` block redacts result columns before they leave Atlas. Use it whenever a report, export, or
the agent's own context would otherwise receive PII (`email`, `ssn`, `phone`, `*_enc`).
| Method | Effect |
|--------|--------|
| `REDACT` | Replaces the whole value with a token |
| `PARTIAL` | Keeps the ends, stars the middle: `keep`, `keep_left`, `keep_right` |
| `HASH` | Deterministic digest: HMAC-SHA256 with `salt`, SHA256 without. Same input, same token |
| `REPLACE` | Regex substitution over the value |
`columns` takes exact names or globs. A mask on a `query` masks that query; a script-level `mask` is
a default for every query. Declare a top-level `mask "<name>"` once and apply it with `use = [mask.<name>]`.
Masking protects the serialized output, not data at rest, so it does not replace restricting the query.
## Testing Scripts
Scripts are tested with the Atlas testing framework. A `script` command inside a `test "schema"`,
`test "migrate"`, `test "plan"`, or `test "script"` block runs matching scripts on the dev database
and asserts on the result.
```hcl
# scripts.test.hcl
test "script" "archive_user" {
exec {
sql = "INSERT INTO users (id, status) VALUES (1, 'active')"
}
script "exec" {
file = "scripts.hcl"
run = "^archive_user$"
vars = { user_id = 1 }
}
script "query" {
file = "scripts.hcl"
run = "^user_status$"
vars = { user_id = 1 }
output = "archived"
}
}
```
```hcl
# atlas.hcl
env "dev" {
dev = "docker://postgres/17/dev"
test {
script {
src = ["scripts.test.hcl"]
}
}
}
```
```bash
atlas script test --env dev
atlas script test --env dev --run archive_user
```
- `output` (exact) and `match` (regexp) compare everything the script prints; `error` expects the run to
fail and matches the failure. The three are mutually exclusive.
- `as { role = "<role>" }` or `as { user = "<user>", password = "..." }` runs the script under another
principal to prove it works with exactly those privileges.
- `test "schema"` runs against the desired schema on a clean dev database; `test "migrate"` migrates to a
chosen version first; `test "script"` starts from a fresh database with no schema.
## Registry
`atlas script push` uploads a script source to the Atlas Registry. Every command that takes `--file`
then accepts `atlas://<repo>` in place of the local path, so a CronJob or CI job pulls scripts by name:
```bash
atlas script push --file "file://scripts" db-monitors
atlas script query --url "$URL" --file "atlas://db-monitors" --run '^pending_orders$' --quiet
```
Pushing again publishes a new version. In `atlas.hcl`, `script { src = "file://scripts" repo { name = "db-monitors" } }`
binds an env to its registry repo.
## Agent Workflow
1. Read `atlas.hcl`: find the env, its `script { src }` block, and whether a `test { script { src } }`
block exists.
2. Pick the kind: `exec` for a bounded mutation, `query` for a read, `loop` for anything that must run in
batches.
3. Write the script with guards: a `condition` that makes the run a no-op when there is nothing to do,
`expect_rows` on writes with a known row count, a post-loop `assert` on the invariant the loop should
leave behind.
4. Write a `test "script"` case that seeds data, runs the script, and asserts on the outcome. Run
`atlas script test --env <name>`.
5. Run the script with an anchored `--run '^name$'`. Review the streaming report, then the closing summary.
6. For reports the agent will read, add `mask` blocks on PII columns and use `--quiet` or `--format '{{ json . }}'`.
## Key Rules
1. Anchor `--run` (`'^name$'`) so a prefix match does not run a second script.
2. Never put credentials in a script or on the command line. Use `--env` with `getenv()` in `atlas.hcl`.
3. Every `loop` must be bounded: an exhausting iterator, a `break`, or `policy.schedule` `limit`/`timeout`.
4. `http` calls cannot be rolled back. They require `policy.tx.mode = MANUAL`; set `expect_status`,
or a 4xx/5xx reply counts as success.
5. Mask PII before it reaches a report, an export, or the agent's context.
6. A `NULL` from `condition` or `assert` is an error, not a graceful stop. Use `COALESCE`.
7. Use a migration, not a script, when the change belongs to the schema.
## Error Handling
| Error | Action |
|-------|--------|
| `No scripts found to run` | The `--run` regexp matched nothing. Check the script name and kind (`exec` vs `query` vs `loop`) |
| `exec kind requires at least one 'assert', 'check', or 'exec' block` | A body of only a bound `query` and `output`. Use `script "query"` for a read |
| `command requires 'atlas login'` or `available only to Atlas Pro users` | Run `atlas login` |
| `tx` command or `http` rejected | Set `policy { tx { mode = MANUAL } }` in the loop |
| `expect_rows` mismatch | The write affected a different row count. The run aborted and rolled back; inspect the predicate |
## Documentation
- [Data Scripts](https://atlasgo.io/scripts)
- [Transactional mutations](https://atlasgo.io/scripts/exec)
- [Queries and reports](https://atlasgo.io/scripts/query)
- [Batched loops](https://atlasgo.io/scripts/loop)
- [Masking](https://atlasgo.io/scripts/masking)
- [Testing scripts](https://atlasgo.io/scripts/testing)
- [Scheduled monitors on Kubernetes](https://atlasgo.io/scripts/kubernetes)