Atlas Agent Skills 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. A second skill,
atlas-onboard, onboards a repository onto Atlas (see Onboarding Skill). Both
install together.
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 Atlas skills, atlas, atlas-onboard, the database skills atlas-postgres
and atlas-mysql, the operator skill atlas-operator, and the
CI/CD skill atlas-action, are published from the
ariga/atlas repository. Claude Code installs them
as a plugin. Other agents install them with the skills CLI.
If you installed the skill earlier by pasting a prompt or with curl, delete that copy first, so the
agent loads one atlas skill: ~/.claude/skills/atlas or .claude/skills/atlas for Claude Code,
~/.codex/skills/atlas or .codex/skills/atlas for Codex, .cursor/skills/atlas for Cursor, and
.github/skills/atlas for GitHub Copilot.
- Claude Code
- OpenAI Codex
- Cursor
- GitHub Copilot
- Other agents
claude plugin marketplace add ariga/atlas
claude plugin install atlas@ariga
The atlas plugin holds all the skills and installs for every project on the machine. A plugin's skills
take the plugin name as a prefix. Claude Code invokes atlas:atlas on its own when a request matches
its description, and you can invoke it directly:
/atlas:atlas summarize the state of our production databases
/atlas:atlas lint the latest migration and explain every finding
/atlas:atlas has the staging database drifted from its migrations?
The allowed-tools field in the frontmatter pre-approves the Atlas commands the skills run most, so
they run without a permission prompt. Most only read, such as atlas whoami, atlas cloud ...,
atlas schema inspect, and atlas migrate status. atlas migrate diff and atlas migrate hash write
files in the project, and lint, validate, and the test commands run SQL on the
dev database. Commands that change a target database, such as
atlas migrate apply and atlas schema apply, still prompt. Other agents ignore this field.
Claude Code updates plugins from this marketplace only when you ask. Run
claude plugin update atlas@ariga, or turn on auto-update for the ariga marketplace under
/plugin > Marketplaces.
To install the plugin for everyone who works in a repository, commit this file. Claude Code installs the plugin for each teammate who trusts the repository folder:
{
"extraKnownMarketplaces": {
"ariga": {
"source": { "source": "github", "repo": "ariga/atlas" }
}
},
"enabledPlugins": {
"atlas@ariga": true
}
}
See Claude Code with Atlas for subagents, slash-command skills, and hooks.
Run it from the repository root:
npx skills add ariga/atlas -a codex -y
Codex loads skills from .agents/skills/, and loads atlas when a database task comes up. Mention it
with $atlas to invoke it directly. See Codex with Atlas for
the AGENTS.md alternative.
Run it from the repository root:
npx skills add ariga/atlas -a cursor -y
Cursor loads skills from .agents/skills/. Type /atlas in
Agent chat to invoke the skill directly. See Cursor with Atlas for the
rules-file alternative.
Run it from the repository root:
npx skills add ariga/atlas -a github-copilot -y
Copilot loads skills from .agents/skills/ in the Copilot CLI, the Copilot cloud agent, and agent mode
in VS Code and JetBrains. In VS Code, type /atlas in chat to invoke the skill directly. See
GitHub Copilot with Atlas for the instructions-file
alternative.
npx skills add ariga/atlas
The skills CLI also installs for Gemini CLI, OpenCode, Windsurf, and many other agents. It asks which
skills to install, so select both. Without -a, it installs for the agents it detects on your machine,
and asks when it finds more than one.
The skills CLI installs in the current directory and records the skills in skills-lock.json. Commit
.agents/skills/ and skills-lock.json so every teammate and CI agent runs the same workflows. Add
-g to install for every project on the machine instead, and run npx skills update to update.
Manual install
The same files from a shell, without Node.js. Set SKILLS_DIR to your agent's skills directory, such
as .agents/skills, .claude/skills, or ~/.claude/skills:
SKILLS_DIR=.agents/skills
base=https://atlasgo.io/skills
# Must match the Files sections of the two SKILL.md files.
for f in SKILL.md references/{schema-sources,versioned,declarative,cicd,lint,testing,drift,cloud,scripts}.md; do
curl -fsSL --create-dirs "$base/atlas/$f" -o "$SKILLS_DIR/atlas/$f"
done
for f in SKILL.md agents/openai.yaml references/stage-{0-plan,1-schema,2-workflow,3-ci,4-deploy,5-promote}.md; do
curl -fsSL --create-dirs "$base/atlas-onboard/$f" -o "$SKILLS_DIR/atlas-onboard/$f"
done
Or copy the file contents from the Skill Files section below.
The skills never contain credentials: they run every command with --env <name> and expect 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$'
Onboarding Skill
atlas-onboard takes a repository from no Atlas setup to production in six stages, the same path as
the Evaluating Atlas guide. It runs the Atlas CLI and ends each stage on a
check it runs itself. Ongoing work then goes to the atlas skill.
| Stage | What it sets up | Done when |
|---|---|---|
| 0. Plan | Login, an inventory of the database, the units, a pilot unit, and the workflow | You approve the units, the pilot, and the workflow |
| 1. Schema as code | atlas.hcl and the desired state: your database exported to SQL or HCL, or your ORM models | atlas schema apply --dry-run against the database reports no changes |
| 2. Migration workflow | Versioned: the baseline migration, and the edit, migrate diff, migrate lint loop. Declarative: the edit, schema apply loop | Versioned: atlas migrate diff reports the directory is synced. The project pull request is open |
| 3. CI | Lint (versioned) or plan (declarative) on pull requests, and a push to the Atlas Registry on merge | CI passes on the merge, and the registry has its version |
| 4. Deploy to staging | Staging deploys from the registry, and your current migration tool stops deploying to staging | Atlas Cloud records the staging deployment |
| 5. Promote to production | Environment promotion: production applies only the version staging runs | You run the first production deployment, on staging's version |
A unit is one Atlas project with its own directory, atlas.hcl, and registry repo. Stage 0 splits a
large database into units by schema or by service; databases that share one schema, such as tenant
databases, stay one unit. The pilot unit goes through every stage first, and each other unit repeats
stages 1 to 5.
The skill reads its progress from the default branch, open pull requests, and Atlas Cloud, so you can
stop and resume in a later session. It records only the decisions the repository cannot show (units,
workflow, deploy target, deferred stages) in .atlas-onboarding.json. Every file change lands as a pull
request. The agent asks before it creates Atlas Cloud resources, never asks for tokens or database
URLs, and reads URLs only from environment variables you export. It never applies to production: it
writes the production configuration, and you run the first production deployment.
Onboarding needs atlas login before the first inspection. Logged out, atlas schema inspect and
atlas schema diff cover schemas, tables, indexes, and constraints only. They skip views, functions,
triggers, and other objects without an error, so an export would miss them. The skill pins your
organization in atlas.hcl, which makes every command that reads it fail unless someone is logged in
to that organization. Stage 3 needs a bot token, which an organization admin creates in
Atlas Cloud.
Install and run
The installation above installs atlas-onboard with the atlas skill, whose
references every stage reads. The onboarding skill runs only when you start it, since it creates
branches, pull requests, and Atlas Cloud resources. Start it from the repository root:
- Claude Code
- OpenAI Codex
- Cursor
- GitHub Copilot
/atlas:atlas-onboard
The skill sets disable-model-invocation, so Claude Code never starts it on its own.
$atlas-onboard
Its agents/openai.yaml turns off implicit invocation, so Codex runs it only when you mention it.
Type it in Agent chat:
/atlas-onboard
Cursor honors disable-model-invocation, so it never starts the skill on its own.
Type it in VS Code chat, in agent mode:
/atlas-onboard
VS Code honors disable-model-invocation, so Copilot never starts the skill on its own.
To resume, start the skill again. It runs its checks for each unit and continues at the first one that fails.
What stays with you
The agent stops and asks you to:
- Run
atlas login, and approve the units, the pilot, and the workflow. - Choose the database the schema is read from. By default, the agent finds your local or development database and connects to it. It reads a remote environment only when you ask and give it the connection, exported in the shell that starts the agent: use a read-only role, or a sign-in with AWS IAM, Microsoft Entra ID, or GCP IAM. Connecting the agent to a production database is not recommended.
- Create the bot token and store it, and the deploy credentials, as CI secrets.
- Approve the first push to the Atlas Registry, and merge each pull request.
- Approve the staging deployment. Production deployments stay with you.
At the end, the agent adds an Atlas section to AGENTS.md and offers to commit the skills to the
repository, as Installation shows for each agent, so every teammate's agent follows the
same workflow. It then suggests next steps from the
guides that fit your project, such as reviewed and approved migrations,
a destructive change policy, and
drift detection.
Database Skills
atlas-postgres and atlas-mysql add what is specific to each database on top of the atlas skill.
The agent loads the matching one on its own when the project uses PostgreSQL, or MySQL or MariaDB, so
it follows the database's rules without being told. They install with the other skills, from the same
command in Installation. The onboarding skill reads the matching
one as soon as it finds the project's database, and recommends installing it when it is missing.
| Skill | Covers |
|---|---|
atlas-postgres | The dev database and URL scope, extensions, roles, grants and default privileges, row-level security, sequences and identity columns, enums and column type changes, partitions, concurrent indexes and lock-safe changes, managed services such as RDS, Cloud SQL, Supabase, and Neon, and connection poolers |
atlas-mysql | The dev database and URL scope, character sets and collations, server settings, users, roles and grants, enum and set columns, table copies that lint reports, migrations that fail halfway, RDS IAM users, and the differences on MariaDB |
Both skills also make the agent test every change with atlas schema test and atlas migrate test,
and turn conventions you state into custom lint rules.
To invoke one directly, use /atlas:atlas-postgres or /atlas:atlas-mysql in Claude Code,
$atlas-postgres or $atlas-mysql in Codex, and /atlas-postgres or /atlas-mysql in Cursor and
GitHub Copilot.
The files are in ariga/atlas, and served raw at
https://atlasgo.io/skills/atlas-postgres/SKILL.md and https://atlasgo.io/skills/atlas-mysql/SKILL.md,
with each skill's reference files under its references/ directory.
Kubernetes Operator Skill
atlas-operator covers the Atlas Kubernetes Operator on top of the atlas
skill: installing it with Helm, writing AtlasSchema and AtlasMigration resources, connecting them to
Atlas Cloud with a bot token, deploying through GitOps, and troubleshooting from status conditions and
events. The agent loads it on its own when a task involves the operator or deploying schema changes to
Kubernetes. It installs with the other skills, from the same command in Installation.
| Area | Covers |
|---|---|
| Setup | Helm install and chart values, database URLs and bot tokens in Secrets, URL scope, the dev database the operator creates |
| Workflows | AtlasSchema sources, policies, and pre-approved or ad-hoc plans; AtlasMigration registry tags, baselines, and protected down migrations |
| Operations | Deploying by changing the manifest, ordering with Argo CD, Argo Rollouts, Flux, and Octopus Deploy, drift detection with policy.drift and AtlasDriftCheck, security scans |
| Troubleshooting | Condition reasons and error messages, with their causes and fixes |
The skill makes the agent test changes from the CLI before they reach the cluster, and check each rollout from the resource's status.
To invoke it directly, use /atlas:atlas-operator in Claude Code, $atlas-operator in Codex, and
/atlas-operator in Cursor and GitHub Copilot. Its files are served raw at
https://atlasgo.io/skills/atlas-operator/SKILL.md, with the reference files under references/.
CI/CD Skill
atlas-action covers running Atlas in CI/CD pipelines on top of the atlas skill: the
GitHub Actions ariga/setup-atlas and ariga/atlas-action, and their
counterparts: GitLab CI components, the
CircleCI orb, the Bitbucket pipe, and the
Azure DevOps task. The agent loads it on its own when a task writes or
fixes a pipeline that runs Atlas. It installs with the other skills, from the same command in
Installation.
| Area | Covers |
|---|---|
| Setup | setup-atlas and the bot token, permissions and pull request comments, dev databases on runners, pinning versions |
| Versioned | migrate/lint, migrate/diff, migrate/hash, migrate/autorebase, migrate/push, migrate/test, migrate/apply, migrate/down, migrate/set |
| Declarative | schema/plan, schema/plan/approve, schema/push, and schema/apply with pre-approved and ad-hoc plans, schema/lint, schema/test |
| Operations | Drift detection with migrate/drift and monitor/schema, security/scan, Data Scripts, create-repo |
| Troubleshooting | Each error message with its cause and fix, and the questions users asked on GitHub |
The skill makes the agent lint and test every change on the pull request, deploy only what was pushed to the registry on merge, keep the tokens that can push or approve and every database URL out of jobs a branch can trigger, and leave deploys, approvals, and reruns to the user.
To invoke it directly, use /atlas:atlas-action in Claude Code, $atlas-action in Codex, and
/atlas-action in Cursor and GitHub Copilot. Its files are served raw at
https://atlasgo.io/skills/atlas-action/SKILL.md, with the reference files under references/.
Skill Files
The files the installation puts in place, also in ariga/atlas. 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 repo list:*) Bash(atlas cloud repo describe:*) Bash(atlas cloud database list:*) Bash(atlas cloud database describe:*) Bash(atlas cloud migration list:*) Bash(atlas cloud migration describe:*) 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 (each step's inputs and errors on each platform: the atlas-action skill)
├─ 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 such as
`modify schema "public" is not allowed when migration plan is scoped to one schema`, 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 (needs atlas login)
atlas schema inspect --env <name> --exclude "*_backup" # Skip matching tables
atlas schema inspect --env <name> --exclude "*.*[type=trigger]" # Skip triggers (database scope: "*.*.*[type=trigger]")
# 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`. Each step's inputs, outputs, tokens, permissions, and failure messages: the
`atlas-action` skill (https://atlasgo.io/guides/ai-tools/agent-skills#cicd-skill).
## 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
Run `atlas whoami` before inspecting, diffing, or baselining an existing database, and ask the user to
log in if it fails. Logged out, `atlas schema inspect` and `atlas schema diff` skip views, functions,
procedures, triggers, sequences, extensions, and other objects and still exit 0, so their output looks
complete when it is not.
Other commands that need a login fail 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.
## 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
For a guided onboarding of a whole project or team (inventory, splitting a large database into units,
CI, and deployment, each stage verified), the user can run the `atlas-onboard` skill
(https://atlasgo.io/guides/ai-tools/agent-skills#onboarding-skill).
To start managing an existing database with versioned migrations, log in first: a logged-out inspect
leaves views, functions, triggers, and other objects out of the baseline without an error. Then pin the
org in `atlas.hcl`, so any later command that reads it (`--env`) aborts unless someone is logged in to
that org. Commands that take `--url` directly do not read it, so run `atlas whoami` before them:
```hcl
atlas {
cloud {
org = "<org printed by atlas whoami>"
}
}
```
```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. Record the baseline as applied on each existing database (the version from the filename).
# Deploy envs can set baseline = "<version>" in their migration block instead.
# Preview first; --dry-run writes nothing:
atlas migrate apply --env <name> --baseline '<version>' --dry-run
# Then, only after the user approves the named target, record it (this writes the revisions table):
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.
- `'atlas login' is required for organization <org> as specified in atlas.hcl`: the project pins its
org. Ask the user to run `atlas login` with an account in that org.
- Missing driver error: ensure `--url` or `--dev-url` is correctly specified.
- `add`, `drop`, or `modify schema "<name>" is not allowed when migration plan is scoped to one schema`:
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, `migrate checkpoint`, `schema inspect --include`, and Atlas Cloud need it, and fail
without it: report the skipped step and continue.
Inspecting and diffing a database do not fail without it; they skip objects. Log in before either.
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 = ["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 `modify schema "public" is not allowed when migration plan is scoped to one schema`, 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`-`PG109` | Index changes without `CONCURRENTLY`, missing `atlas:txmode none`, constraint creation that takes `ACCESS EXCLUSIVE` | Warning |
| Blocking changes (PostgreSQL) | `PG301`-`PG320` | Type changes that rewrite the table, volatile defaults, constraints that scan the whole table, replication and autovacuum settings | Warning |
| Transaction safety | `TX101`, `TX201` | Statements that cannot run in one transaction (`TX101`), and `BEGIN`/`COMMIT` in a file that Atlas already wraps in a transaction (`TX201`) | 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
-- All analyzers, this statement:
-- atlas:nolint
ALTER TABLE t1 DROP COLUMN c1;
-- One analyzer, by name:
-- atlas:nolint destructive
ALTER TABLE t2 DROP COLUMN c2;
-- One check, by code:
-- atlas:nolint DS103
ALTER TABLE t3 DROP COLUMN c3;
```
Never put a comment after the directive on the same line: Atlas reads the rest of the line as analyzer
names and codes, so `-- atlas:nolint -- reason` no longer skips all analyzers. A directive at the top
of the file, followed by an empty line, 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 (`PG101`-`PG109`): 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"` | An empty database, or the schema its `schema` block loads | 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 = "violates check constraint"
}
}
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 an empty database. Load the schema with a `schema` block first, 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 reproduces
the drift, as if it had been executed on the expected state; it is not the fix). 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).
The registry stores the expected state only for untagged pushes, such as the `latest` push that the
`migrate/push` action makes by default. If the applied version was pushed only with a tag
(`atlas migrate push <repo>:<tag>`), the check prints `no state found for version ...` and is skipped,
even with `on_error = FAIL`. Push the version untagged as well.
```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 SQL that reproduces the drift 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
# --to takes an HCL or SQL file, a directory, an ORM loader, or a database URL
atlas migrate diff add_users_email \
--dir "file://migrations" \
--to "file://schema.hcl" \
--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` | How long to wait for the database lock (default 10s) |
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`. With a dev database (`--dev-url` or the env's `dev`), also replay every file on it |
| `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 pending SQL; only pre-migration checks run
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 pending SQL without executing it. Pre-migration checks still run |
| `--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
}
}
}
```
A migration file can also carry pre-migration checks (`-- atlas:txtar` files with assertions that
must hold before the file runs), and `hook` blocks in `atlas.hcl` run SQL inside the migration
transaction; 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; without a tag, updates `latest`
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 added 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 |
| `modify schema "<name>" is not allowed when migration plan is scoped to one schema` (or `add`, `drop`) | 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, created by an org admin in Atlas Cloud under
Settings > Bots, and an `atlas.hcl` committed to the repo with an env for CI (dev database, lint policy).
The docs name the secret `ATLAS_CLOUD_TOKEN` on GitHub Actions and GitLab CI, and `ATLAS_TOKEN` on
CircleCI, Bitbucket, and Azure DevOps; the CLI also reads `ATLAS_TOKEN` from the environment. If the
repo already has a token secret, use its name.
One bot token, a repository secret, serves every job. Keep the database URLs out of pull request jobs:
a job that a branch triggers runs that branch's workflow file and `atlas.hcl`, whose `data "external"`
blocks run programs, so it can read every secret it gets. The URLs live in an environment that only
the default branch can use, and only deploy jobs name it. The `atlas-action` skill covers each
platform.
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. The `migrate/lint`
action always compares with the registry (its `dir-name` input is required), so in case 2 lint with the
CLI in a plain step, `atlas migrate lint --env ci`, after checking out with `fetch-depth: 0`.
The registry repo must exist before the first CI run. Create it once, after the user approves the name:
from CI with the `create-repo` action, or with the first `atlas migrate push` / `atlas schema push`.
```yaml
# .github/workflows/atlas-create-repo.yaml: run once, from the Actions tab
name: Create Atlas Registry Repo
on:
workflow_dispatch:
jobs:
create-repo:
runs-on: ubuntu-latest
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
- uses: ariga/atlas-action/create-repo@v1
with:
name: app
type: migration_directory # or schema
driver: postgres
```
`type` takes `migration_directory` (or `m`) or `schema` (or `s`); the CLI rejects `migration`. The
action fails when the repo already exists, so run it once, not on every push. Its `url` output links to
the repo in Atlas Cloud.
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 on a dev database, comments findings on the PR, fails on errors (without a registry: `atlas migrate lint` in a plain step, case 2 above) | `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 in the job log | `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
jobs:
lint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # only the lint job comments
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_CLOUD_TOKEN }}
- uses: ariga/atlas-action/migrate/lint@v1
with:
env: ci
dir-name: app # required: the registry repo slug
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_CLOUD_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
environment: production # required reviewers; deployment branches: the default branch
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
- uses: ariga/atlas-action/migrate/apply@v1
with:
url: ${{ secrets.DATABASE_URL }} # an environment secret
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 does
not fail the PR for the missing file. `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_CLOUD_TOKEN }}
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }} # fine-grained, this repository only: the branch's code can read it
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_CLOUD_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_auto_rebase` 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
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # only the plan job comments
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_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_CLOUD_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.
The `schema/apply` action waits only with its `lint-review` input (and `wait-timeout`); without them
the step fails with the link, and a rerun after approval applies the plan.
## 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_auto_rebase`, `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`). If not, ask the user
to approve the name, then create it with the `create-repo` action (above) or the first
`migrate push` / `schema push`. Confirm the bot 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)
- The `atlas-action` skill: each step's inputs, outputs, tokens, and error messages on each CI platform
- [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`, `REPO`, `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
Creating a repo creates a resource in the user's organization: ask first, and check that the name is
free (`atlas cloud repo describe --slug <name>`). A push to an existing repo of the same name adds a
version to it.
1. The first push creates the repo: `atlas migrate push --env <name> <slug>` or
`atlas schema push --env <name> <slug>`. To create an empty repo instead:
`atlas cloud repo create --type migration_directory --name <slug> --driver postgres` (or
`--type schema` for the declarative workflow).
2. Envs that lint or push keep the local directory and name the repo:
`migration { dir = "file://migrations" repo { name = "<slug>" } }`, or `schema { repo { name = "<slug>" } }`.
3. Envs that deploy read from the registry: `migration { dir = "atlas://<slug>" }`.
### 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` with a value of the same type, such as `COALESCE(..., false)` |
| `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 EXISTS (SELECT 1 FROM users WHERE id = $1 AND status = 'active')"
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" {
schema {
url = "file://schema.sql" # first: the case starts on an empty database
}
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 an empty database, so load the schema with a
`schema` block before any `exec`.
## 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 `tx { mode = NONE }` in an `exec` script and
`policy { tx { mode = MANUAL } }` in a `loop`; 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 `tx { mode = NONE }` in an `exec` script, or `policy { tx { mode = MANUAL } }` in a 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)
The onboarding skill, served from https://atlasgo.io/skills/atlas-onboard/:
atlas-onboard/SKILL.md
---
name: atlas-onboard
description: "Guided onboarding of a repository onto Atlas, one verified stage at a time: plan the units and the workflow, define the schema as code, set up the migration workflow, add CI with the Atlas Registry, deploy to staging, and promote to production. Each stage ends on a check the agent runs itself, such as a dry run with no changes or a deployment recorded in Atlas Cloud. Use when the user asks to onboard, adopt, or set up Atlas for a project or a team, or to resume an onboarding."
disable-model-invocation: true
allowed-tools: Bash(atlas version) Bash(atlas whoami) Bash(atlas cloud repo list:*) Bash(atlas cloud repo describe:*) Bash(atlas cloud database list:*) Bash(atlas cloud database describe:*) Bash(atlas cloud migration list:*) Bash(atlas cloud migration describe:*) Bash(atlas schema inspect:*) Bash(atlas schema validate:*) Bash(atlas schema test:*) Bash(atlas migrate ls:*) Bash(atlas migrate validate:*) Bash(atlas migrate lint:*) Bash(atlas migrate diff:*) Bash(atlas migrate hash:*) Bash(atlas migrate import:*)
---
# Atlas Onboarding
Takes a repository from no Atlas setup to production in six stages, the same path as the Evaluating
Atlas guide (https://atlasgo.io/guides/evaluation/intro). Each stage ends on a check the agent runs and
shows the user. This skill holds the order, the decisions, and the checks. The `atlas` skill holds the
command reference.
## Files
This skill is `SKILL.md` plus seven files, each served at `https://atlasgo.io/skills/atlas-onboard/<path>`:
`agents/openai.yaml`, `references/stage-0-plan.md`, `references/stage-1-schema.md`,
`references/stage-2-workflow.md`, `references/stage-3-ci.md`, `references/stage-4-deploy.md`, and
`references/stage-5-promote.md`. Read a stage reference when its stage starts.
## Requires
The `atlas` skill, installed next to this one (`../atlas/SKILL.md`). If it is missing, download
https://atlasgo.io/skills/atlas/SKILL.md to `../atlas/SKILL.md` and each reference file listed in its
Files section to `../atlas/references/<name>` before starting. Paths below that start with `atlas/`
point into that skill.
On PostgreSQL, MySQL, or MariaDB, also the database skill, installed next to this one: `atlas-postgres`
(`../atlas-postgres/SKILL.md`), or `atlas-mysql` (`../atlas-mysql/SKILL.md`) for MySQL and MariaDB. It
holds the engine's rules, such as the dev database, the URL scope, and the lint findings to review.
Read it once stage 0 finds the engine, and follow it in every stage. If it is missing, recommend that
the user install it: `claude plugin update atlas@ariga` in Claude Code, or the
`npx skills add ariga/atlas -a <agent> -y` command again in other agents. Until then, read it from
https://atlasgo.io/skills/atlas-postgres/SKILL.md or https://atlasgo.io/skills/atlas-mysql/SKILL.md.
When stage 4 deploys with the Atlas Kubernetes Operator (4.6, 4.7), also read the `atlas-operator`
skill (`../atlas-operator/SKILL.md`). Install and read it the same way, from
https://atlasgo.io/skills/atlas-operator/SKILL.md when it is missing. It covers the operator's install,
tokens, resource policies, and troubleshooting.
For stage 3 and the CI deploy job of stage 4, also read the `atlas-action` skill
(`../atlas-action/SKILL.md`). Install and read it the same way, from
https://atlasgo.io/skills/atlas-action/SKILL.md when it is missing. It covers the Atlas CI steps on each
platform, their tokens and permissions, and the fix for each failed step.
## Stages
| Stage | What it sets up | Read | Done when |
|-------|-----------------|------|-----------|
| 0 Plan | Login, an inventory of the database, the units, the pilot, and the workflow | `references/stage-0-plan.md` | The user approved the units, the pilot, and the workflow |
| 1 Schema as code | `atlas.hcl` and the desired state: the database exported to SQL or HCL, or the ORM models | `references/stage-1-schema.md` | `atlas schema apply --env local --dry-run` reports no changes |
| 2 Migration workflow | Versioned: the baseline migration and the `migrate diff` loop. Declarative: the `schema apply` loop | `references/stage-2-workflow.md` | Versioned: `atlas migrate diff` reports the directory is synced; the project PR is open |
| 3 CI | Lint (versioned) or plan (declarative) on pull requests, push to the Atlas Registry on merge | `references/stage-3-ci.md` | CI passed on the merge, and the registry has its version |
| 4 Deploy to staging | Staging deploys from the registry, and the old tool stops deploying to staging | `references/stage-4-deploy.md` | Atlas Cloud records the staging deployment |
| 5 Promote to production | Production applies only the version staging runs (environment promotion) | `references/stage-5-promote.md` | The user ran the first production deployment, on staging's version |
Run the stages for the pilot unit first. Every other unit repeats stages 1 to 5 with the decisions
already recorded. After the pilot, Hand Off.
## Rules
1. Detect, do not assume. At the start of every session, run Detect the Stage. Progress lives in the
default branch, open pull requests, and Atlas Cloud, not in the conversation.
2. Log in before reading a database. Logged out, `atlas schema inspect` and `atlas schema diff` skip
views, functions, procedures, triggers, and other objects and still exit 0. A schema exported that
way loses them. The skip notice prints once per machine, so its absence proves nothing. Run
`atlas whoami` first.
3. Ask only at the decision points each stage lists. Everywhere else, take the stated default and say
which one you took.
4. Before the first file write for a pull request, create a branch `atlas-onboard/<unit>-<stage>` from
an up-to-date default branch with a clean working tree. Stage 2 continues on the stage 1 branch,
since both land in the project PR; every later stage starts its own branch. Stage files by path,
never `git add -A`. Every change lands as a pull request, and the PR is the review gate. A separate GitOps repository gets its
own PR.
5. Credentials of shared environments never enter the conversation. The user exports those URL
variables in the shell that starts the agent; a `!` command would put the value in the conversation.
Check a variable with `test -n "$STAGING_DATABASE_URL" && echo set`. Never print, parse, or echo such
a URL, and never open `.env`, `*.tfvars`, or other secret files. Local development credentials that
the repository defines, such as a compose service's password, are not secrets.
6. Close each stage with its check: show the command and its output. A check that cannot run (variable
unset, Docker not running, no network) is not a failed stage: fix the precondition and rerun it.
7. Explain as you go. Define each term from Terms in one sentence the first time you use it. After each
stage, say in two or three sentences what now exists and why, and link the page from the Docs Map.
8. Read schemas from a local or development database, which the agent may find and connect to on its
own. Read a remote environment only when the user asks for it and provides the connection; never look
for remote URLs or credentials. Connecting the agent to a production database is not recommended. For
a remote environment, recommend a read-only role, and a cloud sign-in where the database supports it
(stage 1, Remote Databases).
## Permissions
| Action | Rule |
|--------|------|
| Repo scan, `schema inspect`, `migrate lint`, `atlas cloud` reads, `--dry-run` (plans only, changes nothing) | Run |
| Writing files in the repository, branches, pull requests | Run; the PR is the gate |
| Creating Atlas Cloud repos (the first `migrate push` or `schema push`, or the `create-repo` action), bot tokens, CI secrets | Ask first |
| `migrate apply` or `schema apply` against anything but a local or `docker://` dev database | Explicit approval, a named target, and a dry run first |
| Production deploy configuration | Only in a separate PR, when the user asks for it |
| Approving any plan, review, deployment, workflow run, or pull request | Never; tell the user what is waiting and where |
## Project Layout
Each unit is one Atlas project in its own directory, next to the code that owns it (for example
`services/billing/db/`). A repository with one unit can use the root. A unit directory holds
`atlas.hcl`, the desired state (`schema/`, unless the source is an ORM), and, for versioned units, the
migration directory (`migrations/`). Every `atlas.hcl` uses the same env names, so Atlas Cloud shows
real environment names:
| Env | Used for | Written in |
|-----|----------|------------|
| `local` | The local or development database the schema is read from, where developers apply changes; generating and linting migrations | Stage 1 |
| `staging` | Deploying to staging; a read-only check before the first deploy | Stage 4 (stage 1 only when the schema is read from staging) |
| `ci` | Lint and registry push in CI | Stage 3 |
| `prod` | Production deployments, promoted from staging | Stage 5 |
Run every Atlas command from the unit's directory.
## Detect the Stage
1. `atlas whoami`. If it fails, ask the user to log in, then continue.
2. `git fetch origin`. Read the files below from the default branch
(`git show origin/<default>:<path>`), not from the working tree.
3. Look for work an earlier session left open: `git branch -r --list 'origin/atlas-onboard/*'` and open
PRs (`gh pr list` or `glab mr list` when installed; otherwise ask). Continue on an open branch instead
of starting another one.
4. Rerun the scan rows of stage 0 (0.1) for the CI system and the deploy tooling. They need no database.
5. For each unit in `.atlas-onboarding.json`, pilot first, find the last finished stage. Check from
stage 5 down; the first check that passes is the last finished stage. A stage listed under
`deferred` for the unit counts as finished.
| Stage | Finished when |
|-------|---------------|
| 5 | The `prod` env promotes from staging (`to_version`, a `version` in `src`, or a `deny` check), and `atlas cloud migration list --repo <repo> --env-name prod --status PASSED` lists a deployment |
| 4 | `atlas cloud migration list --repo <repo> --env-name staging` shows `PASSED` or `NO_ACTION` for the version `atlas migrate ls --dir atlas://<repo> --latest --short` prints |
| 3 | A workflow on the default branch runs Atlas lint (versioned) or plan (declarative) for the unit, and its last push run on the default branch passed |
| 2 | The project PR is merged: `atlas.hcl` and the source are on the default branch, and versioned units also have `migrations/atlas.sum` there |
| 1 | The unit's `atlas.hcl` and source exist on its onboarding branch, and `atlas schema apply --env local --dry-run` reports no changes |
| 0 | The unit is in `.atlas-onboarding.json` |
## Checklist
Copy this into the conversation and tick items as they close:
```
Stage 0 [ ] scan [ ] atlas login [ ] inventory [ ] units + pilot (user) [ ] workflow (user)
Stage 1 [ ] atlas.hcl [ ] desired state (export or ORM) [ ] dry run: no changes
Stage 2 [ ] baseline (versioned) [ ] new local database from the directory (versioned) [ ] change loop shown [ ] lint shown (versioned) [ ] project PR
Stage 3 [ ] bot token + secret (user) [ ] first push (ask) [ ] CI PR [ ] green run [ ] CI pushed the merge
Stage 4 [ ] cutover plan [ ] deploy config PR [ ] staging dry run [ ] staging deployment recorded
Stage 5 [ ] prod env with promotion (when asked) [ ] prod deploy behind approval [ ] first prod deploy (user)
Hand off [ ] AGENTS.md [ ] skill in repo [ ] next steps [ ] summary + next unit
```
## Decisions File
`.atlas-onboarding.json` at the repository root holds only what the repository cannot show. Update it
in place and commit each change with the PR of the stage that made it.
```json
{
"workflow": "versioned",
"units": [
{ "name": "identity", "dir": "services/identity/db", "repo": "identity", "schemas": ["identity"], "pilot": true },
{ "name": "billing", "dir": "services/billing/db", "repo": "billing", "schemas": ["billing"] }
],
"deploy": { "target": "argocd", "gitops_repo": "github.com/acme/gitops" },
"deferred": [{ "unit": "billing", "stage": 4, "reason": "deployments are owned by the platform team" }]
}
```
`workflow` is `versioned` or `declarative`. `repo` is the registry repo slug. Never store database URLs,
tokens, or status in this file: status comes from the checks above.
## Terms
- Desired state: the schema as it should be, written as SQL, HCL, or ORM models. Both workflows start
from it.
- Versioned workflow: `atlas migrate diff` writes each change to the desired state as a migration file,
reviewed and applied in order. Declarative workflow: no migration files; `atlas schema plan` and
`atlas schema apply` plan each change against the target database, and the plan is reviewed.
- Dev database: a temporary, isolated database Atlas uses as a sandbox to plan and check changes, usually
an ephemeral Docker container (https://atlasgo.io/concepts/dev-database#introduction). It
never holds data and is not the application's development database.
- Baseline: the first migration file of a versioned unit, a snapshot of the existing schema. Existing
databases record it as applied without running it.
- Unit: one Atlas project, a schema or a set of tables one team owns. Pilot: the first unit, taken
through every stage before the others.
- Atlas Registry: the copy of the migration directory or schema in Atlas Cloud that CI pushes and
deployments read.
- Bot token: an Atlas Cloud credential for CI, not tied to a person.
- Environment promotion: production applies only the version already deployed to staging.
## Hand Off
When the pilot finishes stage 5, or the last stage the user wants:
1. Add an Atlas section to `AGENTS.md`, creating the file if needed. If the repository has a
`CLAUDE.md`, add the line `@AGENTS.md` to it.
```markdown
## Database schema (Atlas)
- Unit `<unit>` in `<dir>`: `atlas.hcl`, desired state in `schema/` (or the ORM models), migrations in
`migrations/`. Run Atlas from `<dir>`.
- Change the desired state, then run `atlas migrate diff --env local <name>`. A new file in `schema/`
needs an `-- atlas:import` line at the top of `schema/main.sql`.
- Never edit an applied migration. After editing an unapplied one, run `atlas migrate hash --env local`.
- Before opening a PR: `atlas migrate lint --env local --latest 1`.
- Deployments run from <CI workflow or GitOps target>, staging first, then production promoted from
staging. Never run `migrate apply` or `schema apply` against a shared database from a laptop.
- Use the `atlas` skill for schema work.
```
For declarative units, the change lines are: change the desired state, then
`atlas schema apply --env local` against the local database; CI plans the change for review on the
PR. On PostgreSQL,
the last line names both skills: Use the `atlas` and `atlas-postgres` skills for schema work. On
MySQL and MariaDB, name `atlas-mysql` instead.
2. Offer to commit the skills, so every teammate's agent gets them:
- Claude Code: merge these keys into `.claude/settings.json`. Claude Code installs the Atlas plugin
for each teammate who trusts the repository folder.
```json
{
"extraKnownMarketplaces": {
"ariga": { "source": { "source": "github", "repo": "ariga/atlas" } }
},
"enabledPlugins": { "atlas@ariga": true }
}
```
- Codex, Cursor, GitHub Copilot: run `npx skills add ariga/atlas -a <agent> -y` from the repository
root, with `codex`, `cursor`, or `github-copilot`, and commit `.agents/skills/` and
`skills-lock.json`. If the install already put them there, commit them unchanged.
3. Next steps: offer the practices from the guides that fit what the scan found, one at a time. The
full list is https://atlasgo.io/guides/evaluation/advanced-topics.
| Practice | Guide |
|----------|-------|
| Production applies only reviewed and approved migrations | https://atlasgo.io/guides/reviewed-approved-migrations |
| Block accidental drops in CI, with a deprecation workflow | https://atlasgo.io/guides/destructive-change-policy |
| Detect drift at deploy time and between deployments | https://atlasgo.io/guides/drift-detection |
| Test functions, views, triggers, and data migrations | https://atlasgo.io/testing/schema |
| Lock-safe `NOT NULL` changes on PostgreSQL | https://atlasgo.io/guides/lock-safe-not-null |
| Teams change only the objects they own | https://atlasgo.io/guides/schema-ownership |
| Roles, permissions, and row-level security as code | https://atlasgo.io/guides/security-as-code |
| Staged rollouts for database-per-tenant | https://atlasgo.io/guides/database-per-tenant/rollout |
4. Summarize per stage: what exists now, the doc page for it, what was deferred and why, and which units
remain. For the next unit, the user runs this skill again; Detect the Stage starts it at stage 1.
## Docs Map
| Stage | Read |
|-------|------|
| 0 | [Connect](https://atlasgo.io/guides/evaluation/connect), [Choose a workflow](https://atlasgo.io/guides/evaluation/project-structure#choose-a-workflow), [Declarative vs versioned](https://atlasgo.io/concepts/declarative-vs-versioned) |
| 1 | [Verify Atlas and export your schema](https://atlasgo.io/guides/evaluation/verify-atlas), [Schema as code](https://atlasgo.io/guides/evaluation/schema-as-code), [Dev database](https://atlasgo.io/concepts/dev-database), [ORM guides](https://atlasgo.io/orms) |
| 2 | [Set up your migration workflow](https://atlasgo.io/guides/evaluation/setup-migrations), [Schema change workflow](https://atlasgo.io/guides/evaluation/developer-workflow), [Migration linting](https://atlasgo.io/versioned/lint) |
| 3 | [Database CI/CD](https://atlasgo.io/guides/evaluation/ci-cd), [Versioned CI/CD](https://atlasgo.io/versioned/setup-cicd), [Declarative CI/CD](https://atlasgo.io/declarative/setup-cicd) |
| 4 | [Deployments in Atlas Cloud](https://atlasgo.io/cloud/deployment), [Deployment guides](https://atlasgo.io/guides/deploying/intro), [Existing databases](https://atlasgo.io/versioned/apply#existing-databases) |
| 5 | [Environment promotion](https://atlasgo.io/guides/environment-promotion), [Declarative environment promotion](https://atlasgo.io/guides/environment-promotion-declarative) |
## Gotchas
- The org pin in `atlas.hcl` protects only commands that read it (`--env`). Commands that take `--url`
directly run logged out, so run `atlas whoami` before them.
- A command-line `--exclude` replaces the env's `exclude` list instead of adding to it. Keep exclusions
in the env and pass no `--exclude` with `--env`.
- Exclude patterns follow the URL scope. With a schema-scoped URL, a pattern names objects in that
schema: `audit_*` for tables, `*[type=function]` for every function. With a database-scoped URL, the
first segment names the schema: `public.audit_*`, `public.*[type=function]`, or `internal` to skip a
whole schema.
- The dev database must match the target's engine version and scope (https://atlasgo.io/concepts/dev-database).
The wrong scope fails with `modify schema "<name>" is not allowed when migration plan is scoped to one schema`, or silently drops extensions.
- The same difference on every table (collation, charset, owner) means the dev database's defaults
differ from the target's. Fix the dev database; never write a migration for it.
- A split SQL source (`schema/main.sql` plus one file per object) reads only the files `main.sql`
imports. A new file needs an `-- atlas:import <path>` line in the directive block at the top of
`main.sql`, with no blank line before it; otherwise it is ignored and `migrate diff` reports no changes.
- `Error: missing scheme` on an env that reads `getenv()` means the variable is not set in the agent's
shell, not that a stage failed.
- Without the baseline version, the first `migrate apply` on an existing database aborts with
`connected database is not clean`. Set `baseline`; never get past it with `--allow-dirty`.
- Codex runs commands in its `workspace-write` sandbox by default, with outbound network access off
until the user approves it. Atlas needs the network for Atlas Cloud and the database, so ask the user
to approve Atlas commands, or to enable network access, before Detect the Stage.
- Pull requests opened by a cloud agent may not run CI until a human approves the run. Tell the user when
a check is waiting on that.
atlas-onboard/agents/openai.yaml
interface:
display_name: "Atlas Onboarding"
short_description: "Onboard a repository onto Atlas, one verified stage at a time"
policy:
allow_implicit_invocation: false
atlas-onboard/references/stage-0-plan.md
# Stage 0: Plan
Goal: know what the repository and its databases contain, log in, and agree with the user on the units,
the pilot, and the workflow. This stage writes only `.atlas-onboarding.json`, and nothing touches a
database except reads.
## 0.1 Scan the Repository
No login or database access needed. Read configuration for names and versions only: grep for image tags,
engine names, and variable names. Never open `.env`, `*.tfvars`, or secret files, and never repeat a
password or connection string. Report the findings as a short table before doing anything else.
| Look for | Where | Feeds |
|----------|-------|-------|
| Engines and major versions | `docker-compose*.yml` image tags, ORM config, Helm values, Terraform database resources | The dev database (stage 1), and the database skill to read now (Requires). If the version is not in the repository, ask: this is a decision point |
| Schema source | ORM models (GORM, Drizzle, SQLAlchemy, Django, Ent, Sequelize, TypeORM, Prisma), `*.sql` or `*.hcl` schema files | Stage 1 |
| Current migration tool | Flyway `V1__*.sql` and `flyway.conf`, Liquibase changelogs, golang-migrate `*.up.sql`, goose `-- +goose Up`, dbmate `-- migrate:up`, ORM migration folders (Django, Alembic, Prisma Migrate) | The tool stage 4 retires, and its history table to exclude |
| Objects outside the ORM | `RunSQL` or raw `CREATE VIEW`, `CREATE TRIGGER`, `CREATE FUNCTION`, `CREATE EXTENSION` in migrations | 0.2, stage 1 |
| Atlas already present | `atlas.hcl`, `atlas.sum`, `.atlas-onboarding.json` | Resume with Detect the Stage |
| CI system | `.github/workflows/`, `.gitlab-ci.yml`, `.circleci/`, `bitbucket-pipelines.yml`, `azure-pipelines.yml`, `Jenkinsfile` | Stage 3 |
| Deploy tooling | Argo CD `Application`, Flux `Kustomization`, `*.tf`, Helm migration hooks, a release or pre-deploy command, an entrypoint that runs migrations | Stage 4. If none is found, ask where the services deploy from |
| The agent running this skill | Claude Code, Codex, Cursor, GitHub Copilot | Where to commit the skill at hand off |
## 0.2 Log In
```bash
atlas version
```
If the command is not found, ask before installing the CLI: `curl -sSf https://atlasgo.sh | sh` on macOS
and Linux, or `brew install ariga/tap/atlas`. Other methods are at https://atlasgo.io/getting-started.
Then check the login:
```bash
atlas whoami
```
If it fails, ask the user to run `atlas login` in their own terminal. It opens a browser; on the first
login the user creates an account and an organization. Explain why it comes first, naming the objects
the scan found: logged out, inspect and diff cover schemas, tables, columns, indexes, and constraints
only, and skip views, materialized views, functions, procedures, triggers, sequences, domains, and
extensions without an error. Some drivers, such as SQL Server and ClickHouse, need a login for every
command. The later stages need it too: lint, the Atlas Registry, CI, and deployment reporting.
If the user declines, stop before 0.3 for any existing database. Offer the scan report only.
## 0.3 Inventory
Read the schema from a local or development database. The recommended source is the database the
application uses in development, such as a docker compose service or a dev container, after its current
migrations ran: it has the same schema as production, and nothing the agent runs can affect a shared
environment. The agent may find it and connect on its own, with the local credentials the compose file
or the dev setup defines, and set `LOCAL_DATABASE_URL` to it. Connecting the agent to a production
database is not recommended.
Read a remote environment, such as staging, only when the user asks for it and provides the connection:
the environment variable that holds the URL, or the cloud sign-in to use. Never look for remote URLs,
credentials, or environment variables on your own. Recommend a read-only role that can read every table
in the schemas the application uses; tables the role cannot read can drop out of the inspection without
an error. Managed databases that sign in with a cloud identity instead of a password (AWS IAM,
Microsoft Entra ID, GCP IAM) get their URL from `atlas.hcl`: write that env first (stage 1, Remote
Databases) and run the commands below with `--env <name>` instead of `--url`.
The URL must use the Atlas format (`postgres://`, `mysql://`), not JDBC.
The URL's scope decides what Atlas sees (https://atlasgo.io/concepts/url#scope):
- Schema scope: the URL names one schema, with `search_path=<schema>` in PostgreSQL, the database name
in the MySQL path, or `mode=schema` in SQL Server. Atlas inspects, plans, and applies changes inside
that schema only, and writes DDL without schema qualifiers (`users`, not `public.users`).
- Database scope: the URL names no schema, such as `postgres://host:5432/app` or `mysql://host:3306/`.
Atlas covers every schema and qualifies the DDL it writes (`public.users`).
The inventory uses database scope, so it sees every schema before the units are decided; each unit picks
its own scope in stage 1. In MySQL, where a schema is a database, use the server URL only when the
application uses more than one database.
```bash
test -n "$LOCAL_DATABASE_URL" && echo set || echo missing
# Tables per schema
atlas schema inspect --url "$LOCAL_DATABASE_URL" --format '{{ json . }}' \
| jq -c '.schemas[] | {name, tables: ((.tables // []) | length)}'
```
The table count is often enough. When the database also has views, functions, triggers, or other
objects, count them by type: export the database to one file per object in a temporary directory, the
same export stage 1 uses (https://atlasgo.io/inspect/database-to-code), and count the files.
```bash
inv=$(mktemp -d)
atlas schema inspect --url "$LOCAL_DATABASE_URL" --format "{{ sql . | split | write \"$inv\" }}"
find "$inv" -name '*.sql' ! -name main.sql | sed -E "s|^$inv/||; s|/[^/]+\$||" | sort | uniq -c
rm -rf "$inv"
```
Then map tables to the code that writes them: search the repository for each table name in models and
SQL strings, and group tables by service or package. Roles and permissions are excluded from inspection
by default; ask whether the team wants Atlas to manage them, and see stage 2, Roles and Permissions.
Report the inventory in this shape:
```
Database: app (PostgreSQL 15, read through LOCAL_DATABASE_URL)
schema tables views mat. views functions triggers written by
billing 55 4 0 9 3 services/billing
identity 20 0 0 1 0 services/identity
catalog 31 0 2 0 0 services/catalog
public 1 0 0 0 0 schema_migrations (current migration tool)
extensions: pgcrypto, pg_trgm (schema public)
```
## 0.4 Units and a Pilot (decision point)
A unit is one Atlas project: its own directory, `atlas.hcl`, and registry repo (see Project Layout in
`SKILL.md`). Propose units along ownership lines:
| Situation | Unit | Mechanism |
|-----------|------|-----------|
| Each service owns its own schema | One per schema | Schema-scoped URLs: `search_path=<schema>` in PostgreSQL, the database name in MySQL, `mode=schema` in SQL Server |
| A service owns several schemas | One, covering those schemas | Database-scoped URLs and `schemas = ["a", "b"]` on the envs |
| Services share one schema | One per service, split by table | `exclude` on the envs: `["audit_*"]` with a schema-scoped URL, `["billing.audit_*"]` with a database-scoped one |
| Many databases share one schema (tenants) | One for all of them | `for_each` on the env (`atlas/references/versioned.md`, Multi-tenant apply) |
| Teams share a unit but must not change each other's tables | One, plus a lint rule | `lint { ownership "github" { ... } }` (https://atlasgo.io/guides/schema-ownership) |
Schema-scoped units can use objects in a schema no unit owns, such as extensions in `public`. Keep those
objects out of every unit, and create them in each unit's dev database with a `docker` block `baseline`
(stage 1, and https://atlasgo.io/concepts/dev-database#baseline-schema). New databases need them before
the first unit deploys. Never put the old tool's history table in a unit.
Pick a pilot: the smallest unit with recent schema changes and the fewest objects that need special
handling (extensions, partitions, triggers). Then ask one question that states the counts:
```
I found 3 schemas, 106 tables, 4 views, 2 materialized views, 10 functions, and 3 triggers, plus
pgcrypto and pg_trgm in public. I propose three units: billing, identity, and catalog, with the
extensions and the migration tool's history table left out. Pilot: identity (20 tables, one
function, changed 5 times this quarter). Does this match how your teams own the database?
```
Record the answer in `.atlas-onboarding.json`: `units` with `name`, `dir`, `repo` (default: the unit
name), `schemas`, and `"pilot": true` on one.
## 0.5 Workflow (decision point)
In both workflows, developers define the desired state of the schema as code. The difference is how
changes reach a database (https://atlasgo.io/guides/evaluation/project-structure#choose-a-workflow):
- Versioned: `atlas migrate diff` writes each change as a migration file, checked into source control,
reviewed in the PR, and applied in order. Every database replays the same files, and the history is
what gets promoted and audited.
- Declarative: `atlas schema plan` computes the change from the live schema to the desired state, the
plan is reviewed and approved, and `atlas schema apply` runs it. There are no migration files.
Default to versioned: it is the common choice for shared environments such as staging and production.
Propose declarative when the project already applies a desired state without migration files, or when
the user asks for it. Ask in one question that says what changes for developers, and link
https://atlasgo.io/concepts/declarative-vs-versioned. Record `workflow` as `versioned` or `declarative`.
When the ORM's own migration tool is in use (Django `makemigrations`, Alembic, Prisma Migrate), say what
happens to it: stage 4 retires it as the deployer, and the user decides whether developers keep running
it for other purposes, such as building test databases.
## Check
`.atlas-onboarding.json` has `workflow` and `units`, with one pilot, and the user approved both.
Nothing is committed yet: the project PR in stage 2 carries this file.
atlas-onboard/references/stage-1-schema.md
# Stage 1: Schema as Code
Goal: the unit's desired state lives in code and matches the database. This stage is the same for both
workflows. It writes `atlas.hcl` and the desired state, and changes nothing in a database. The steps
follow https://atlasgo.io/guides/evaluation/verify-atlas and
https://atlasgo.io/guides/evaluation/schema-as-code.
Create the branch `atlas-onboard/<unit>-1` (Rule 4) and work in the unit's directory.
## 1.1 Write `atlas.hcl`
```hcl
atlas {
cloud {
org = "acme" # the org atlas whoami printed
}
}
locals {
src = "file://schema" # the desired state; an ORM loader in 1.3
dev = "docker://postgres/15/dev?search_path=public" # same engine version and scope as the target
exclude = [
"atlas_schema_revisions", # Atlas's history table, created by migrate apply
"schema_migrations", # the current tool's history table (golang-migrate)
]
}
env "local" {
url = urlqueryset(getenv("LOCAL_DATABASE_URL"), "search_path", "identity") # the database from stage 0
dev = local.dev
exclude = local.exclude
schema {
src = local.src
}
}
```
- The `atlas` block pins the org. Every command that reads this file (`--env`) aborts with
`'atlas login' is required for organization acme as specified in atlas.hcl` unless someone is logged
in to that org, which protects teammates and later sessions.
- `local` reads the local or development database from stage 0, and developers apply changes to it
while they work. It derives the unit's scope from the database-scoped URL of the inventory:
`urlqueryset(..., "search_path", "<schema>")` in PostgreSQL, `urlsetpath(..., "<database>")` in MySQL.
Database-scoped units use the URL as is.
- `exclude` keeps tables that are not part of the application's schema out of every export, diff, and
plan, so Atlas never proposes to drop them:
- `atlas_schema_revisions`: Atlas's own history table, which `atlas migrate apply` creates in the
database. Without this entry, `atlas schema apply --dry-run` on a database where migrations ran
plans `drop "atlas_schema_revisions" table`.
- The current migration tool's history table, while that tool still runs: `schema_migrations` for
golang-migrate and dbmate, `flyway_schema_history`, `DATABASECHANGELOG` for Liquibase,
`goose_db_version`, `alembic_version`, `django_migrations`. Leave it out when there is no such tool.
- The unit's own patterns from stage 0.4, in the form that matches the URL scope (Gotchas in
`SKILL.md`).
The dev database is a temporary, isolated database that Atlas uses as a sandbox: it loads the desired
state and the migrations there, because every expression, default, and statement must be accepted by a
real database of the same type and version (https://atlasgo.io/concepts/dev-database#introduction). The
`docker://` driver starts it as an ephemeral container and removes it afterward. It must match the
target's engine, version, and scope. For a schema-scoped PostgreSQL unit, its URL uses
`search_path=public` whatever the target schema is called: schema-scoped DDL has no schema qualifiers,
so it runs the same in any schema.
Docker dev URLs from https://atlasgo.io/concepts/dev-database#introduction. The dev database must use the
same engine and version as the target, so replace the version with the target's, such as
`docker://postgres/16/dev` for PostgreSQL 16:
| Engine | Schema scope | Database scope |
|--------|--------------|----------------|
| PostgreSQL | `docker://postgres/15/dev?search_path=public` | `docker://postgres/15/dev` |
| PostgreSQL with PostGIS | `docker://postgis/latest/dev?search_path=public` | `docker://postgis/latest/dev` |
| PostgreSQL with pgvector | `docker://pgvector/pg17/dev?search_path=public` | `docker://pgvector/pg17/dev` |
| PostgreSQL, custom image | `docker+postgres://ghcr.io/namespace/image:tag/dev?search_path=public` | `docker+postgres://ghcr.io/namespace/image:tag/dev` |
| MySQL | `docker://mysql/8/dev` | `docker://mysql/8` |
| MariaDB | `docker://maria/latest/schema` | `docker://maria/latest` |
| SQL Server | `docker://sqlserver/2022-latest/dev?mode=schema` | `docker://sqlserver/2022-latest/dev?mode=database` |
| ClickHouse | `docker://clickhouse/23.11/dev` | `docker://clickhouse/23.11` |
| Oracle | `docker://oracle/free:latest?mode=schema` | `docker://oracle/free:latest?mode=database` |
| CockroachDB | `docker://crdb/v25.1.1/dev?search_path=public` | `docker://crdb/v25.1.1/dev` |
| YugabyteDB | `docker://ysql/latest/dev?search_path=public` | `docker://ysql/latest/dev` |
| Aurora DSQL | `docker://dsql/16/postgres?search_path=public` | `docker://dsql/16` |
| Spanner | | `docker://spanner/latest`, or `docker://spannerpg/latest` for the PostgreSQL dialect |
| SQLite | `sqlite://dev?mode=memory` | |
Redshift, Snowflake, and Databricks do not run in Docker: point `dev` at a separate, empty database (a
catalog on Databricks) on the same service, as the page shows. `docker://` dev URLs need Docker, locally
and on CI runners.
The exported schema can depend on objects the unit does not manage, such as an extension, a function in
another schema, or a provider role. A new dev database does not have them, so Atlas cannot load the
schema there until they exist. Create them in the `baseline` of a `docker` block, which runs when the
container starts, and point the envs' `dev` at it
(https://atlasgo.io/concepts/dev-database#baseline-schema):
```hcl
docker "postgres" "dev" {
image = "postgres:15"
schema = "public" # schema scope; leave it out for a database-scoped unit
baseline = <<-SQL
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SQL
}
env "local" {
url = urlqueryset(getenv("LOCAL_DATABASE_URL"), "search_path", "identity")
dev = docker.postgres.dev.url
# exclude and schema as above
}
```
A longer script can live in a file: `baseline = file("baseline.sql")`. For extensions such as PostGIS or
pgvector, use an image that ships them (`postgis/postgis`, `pgvector/pgvector`). Without Docker, a `dev`
block points at an empty database of your own and takes the same `baseline`
(https://atlasgo.io/concepts/dev-database#providing-your-own-dev-database).
### Remote Databases
When the user asks to read the schema from a remote environment and provides its connection, add an env
named after it (`staging`), and use `--env staging` where this stage says `--env local`. Use the
connection the user gives, and recommend a read-only role. Connecting to production is not recommended.
Stage 4 deploys through the same env, with a deploy role in CI instead of the read-only one.
Managed databases can sign in with a cloud identity instead of a password: Atlas requests a short-lived
token and builds the URL in `atlas.hcl`, so no password is stored
(https://atlasgo.io/atlas-schema/projects#data-sources). Each method needs the database user mapped to
the identity first; the linked sections list the steps.
AWS RDS with IAM authentication
(https://atlasgo.io/atlas-schema/projects#data-source-aws_rds_token):
```hcl
locals {
db_user = "atlas_readonly"
db_endpoint = "app-db.abc123.us-east-1.rds.amazonaws.com:5432"
}
data "aws_rds_token" "staging" {
region = "us-east-1"
endpoint = local.db_endpoint
username = local.db_user
}
env "staging" {
url = "postgres://${local.db_user}:${urlescape(data.aws_rds_token.staging)}@${local.db_endpoint}/app?search_path=identity"
# dev, exclude, and schema as in local
}
```
Azure Database for PostgreSQL or MySQL with Microsoft Entra ID, using the credentials from `az login`,
a managed identity, or a workload identity
(https://atlasgo.io/atlas-schema/projects#data-source-azure_db_token):
```hcl
data "azure_db_token" "staging" {}
env "staging" {
url = urluserinfo(
"postgres://app-db.postgres.database.azure.com:5432/app?search_path=identity&sslmode=require",
"atlas-readonly@contoso.onmicrosoft.com",
data.azure_db_token.staging,
)
}
```
Azure SQL (SQL Server) with Microsoft Entra ID: the `fedauth` parameter of the URL picks the sign-in
method (https://atlasgo.io/concepts/url, SQL Server tab):
```
azuresql://app-db.database.windows.net?fedauth=ActiveDirectoryDefault&database=app
```
GCP Cloud SQL with IAM authentication. MySQL needs `allowCleartextPasswords` and `tls` as shown;
PostgreSQL uses `sslmode=require` (https://atlasgo.io/atlas-schema/projects#data-source-gcp_cloudsql_token):
```hcl
data "gcp_cloudsql_token" "staging" {} # locals as in the AWS example
env "staging" {
url = "mysql://${local.db_user}:${urlescape(data.gcp_cloudsql_token.staging)}@${local.db_endpoint}/?allowCleartextPasswords=1&tls=skip-verify&parseTime=true"
}
```
A password kept in a secret store, such as AWS Secrets Manager or GCP Secret Manager
(https://atlasgo.io/atlas-schema/projects#data-source-runtimevar):
```hcl
data "runtimevar" "staging_password" {
url = "awssecretsmanager://staging/atlas-readonly?region=us-east-1"
}
env "staging" {
url = "postgres://atlas_readonly:${urlescape(data.runtimevar.staging_password)}@staging-db:5432/app?search_path=identity"
}
```
## 1.2 Export the Database to Code
For units without an ORM, the desired state is the database exported to code. Ask which language the
team will edit (decision point): SQL, which most teams already know, or HCL, which has editor support and
no ordering constraints (https://atlasgo.io/guides/evaluation/schema-as-code#what-should-you-choose).
Default to SQL.
```bash
# SQL: one file per object in schema/, with a main.sql entry point that imports every file
atlas schema inspect --env local --format '{{ sql . | split | write "schema" }}'
# HCL: one file; set local.src to "file://schema.hcl"
atlas schema inspect --env local > schema.hcl
```
Full reference: https://atlasgo.io/inspect/database-to-code.
## 1.3 Or Load the ORM Models
For units with an ORM, the desired state is the models. Add the `data "external_schema"` loader from
the ORM's guide (https://atlasgo.io/orms, `atlas/references/schema-sources.md`), and set `local.src` to
its URL. If the loader goes into the application's configuration (Django's `INSTALLED_APPS`), its
package must be in the requirements that production installs.
Objects the ORM cannot express (views, triggers, functions, row-level security) go in a SQL or HCL file
next to the models, combined with them through `composite_schema`
(https://atlasgo.io/guides/evaluation/schema-as-code#must-you-choose-only-one).
## 1.4 Verify Zero Diff
```bash
atlas schema apply --env local --dry-run
```
`--dry-run` only plans: it reads the database and changes nothing. When the code matches the database,
Atlas reports the schema is synced, with no changes to be made. Otherwise it prints the statements that
would make the database match the code, and each statement is a difference.
An export matches by construction; a difference there means the dev database differs from the target
(version, scope, or defaults). ORM models usually differ. Handle each kind of difference once, not each
object:
| Difference | Fix |
|------------|-----|
| Tables the ORM does not manage (unmanaged models, removed apps, other tools' tables) | Add them to `exclude`; never drop them |
| The same change on every table (collation, charset, owner) | The dev database's defaults differ from the target's: fix the dev database |
| Views, functions, triggers, extensions the ORM does not model | Add them to the `composite_schema` file from 1.3 |
| Types, defaults, index names | Ask whether the model or the database is right. Fix the model, or keep the difference: it becomes the first change in stage 2 |
## Check
`atlas schema apply --env local --dry-run` reports no changes, or only the differences the user chose
to keep. Stage 2 continues on this branch and opens the project PR.
Tell the user that the desired state now lives in the repository, and that both workflows start from it.
Link https://atlasgo.io/guides/evaluation/schema-as-code.
atlas-onboard/references/stage-2-workflow.md
# Stage 2: Migration Workflow
Goal: the unit's workflow is set up, the user has seen a change go through it, and the project PR is
open. Continue on the stage 1 branch. The steps follow
https://atlasgo.io/guides/evaluation/setup-migrations and
https://atlasgo.io/guides/evaluation/developer-workflow.
## Roles and Permissions (optional)
Atlas leaves roles, users, and permissions (`GRANT` and `REVOKE`) out of inspection and management by
default. If the team wants to manage them as code, enable them with a `mode` block in the env's `schema`
block before the baseline, and repeat the stage 1 export so the desired state includes them
(https://atlasgo.io/atlas-schema/hcl#enabling-roles-and-permissions,
https://atlasgo.io/guides/security-as-code).
Roles and users belong to the database instance, not to one schema, so they cannot be managed in schema
scope. A unit that manages roles uses database scope: its URL and its dev URL name no schema (no
`search_path` in PostgreSQL), and its DDL is schema-qualified (stage 0.3).
The exported schema also refers to roles that exist on the target but not in a new dev database, such
as the provider roles of a managed database. Create them in the dev database's `baseline`, so Atlas can
load the schema there (https://atlasgo.io/concepts/dev-database#baseline-schema, and for managed
PostgreSQL, https://atlasgo.io/guides/postgres/security-declarative#dev-database-for-cloud-environments):
```hcl
docker "postgres" "dev" {
image = "postgres:15" # the target's engine and version; no schema, so database scope
baseline = <<-SQL
CREATE ROLE rds_superuser;
CREATE ROLE rds_iam;
GRANT rds_superuser TO postgres;
SQL
}
env "local" {
url = getenv("LOCAL_DATABASE_URL") # database scope: no search_path
dev = docker.postgres.dev.url
exclude = local.exclude
schema {
src = local.src
mode {
roles = true # roles and users
permissions = true # GRANT and REVOKE
}
}
}
```
- With `roles = true`, the desired state is authoritative for every role Atlas inspects on the instance:
Atlas plans to drop any role the schema does not declare. Declare the roles Atlas must not touch with
`external = true`: the role Atlas connects with, the admin, replication roles, provider roles such as
`rds_superuser`, and other teams' roles.
- The dev database must be an instance of its own, such as the container above. On the target's
instance, its roles would collide with the live ones.
- Declarative units that manage passwords also set `sensitive = ALLOW`
(https://atlasgo.io/guides/postgres/security-declarative).
- Use the same `mode` block and dev database in every env of the unit, so local development, CI, and
deployments plan roles and permissions the same way.
## Versioned
### 2.1 Generate the baseline
Add the migration directory to the `local` env:
```hcl
env "local" {
# url, dev, exclude, and schema as in stage 1
migration {
dir = "file://migrations"
}
}
```
Generate the baseline, the first migration: it captures the schema as it exists today, and every later
migration is a diff on top of it (https://atlasgo.io/versioned/import#generate-a-baseline-migration).
`migrate diff` compares the migration directory, empty so far, with the desired state from stage 1:
```bash
atlas migrate diff --env local baseline
```
This writes `migrations/<version>_baseline.sql` and `migrations/atlas.sum`. The version is the
timestamp at the start of the file name, such as `20250811074144` for `20250811074144_baseline.sql`.
If the user kept differences in stage 1, the baseline must describe the database as it is. Generate it
from an export of the database instead (`--to file://<export dir>`, exported as in stage 1.2 to a
temporary directory), then run `atlas migrate diff --env local <name>`: it writes the kept differences
as the second migration, reviewed like any other.
Verify the directory matches the desired state:
```bash
atlas migrate diff --env local
```
It prints `The migration directory is synced with the desired state, no changes to be made`.
### 2.2 Apply the baseline
How a database starts depends on whether it already has the schema
(https://atlasgo.io/versioned/import#apply-the-baseline-migration):
- New databases run the baseline in full, which creates the schema. Every new local database starts
this way from now on, so the `local` env never sets a baseline. Show it on a new local database, such
as a recreated compose database:
```bash
atlas migrate apply --env local
```
A local database that already has the schema, such as the one stage 1 exported, is either recreated
this way or marked once with `atlas migrate apply --env local --baseline <version>`.
- Existing databases in real environments, such as staging and production, already have the schema and
must not run the baseline. They start after it, in one of two ways, as the user prefers:
- `baseline = "<version>"` in the `migration` block of that environment's env, which stages 4 and 5
write. Every deployment through the env then starts after the baseline, whatever tool runs it.
- A one-time `atlas migrate apply --env <env> --baseline <version>` on each database, before its first
deployment. It writes Atlas's history table to the database, so the user runs it, not the agent.
Never set `baseline` in an env that creates new databases: there, Atlas skips the baseline file, and the
new database misses the schema it describes. Without a baseline, the first `migrate apply` on an
existing database stops with `connected database is not clean`.
### 2.3 Show the change workflow
Every schema change from now on is: edit the desired state, `atlas migrate diff --env local <name>`,
review the file, `atlas migrate lint --env local --latest 1`, then open a PR. Developers apply to their
local database with `atlas migrate apply --env local`.
Show what lint catches on a copy of the directory, so the repository is untouched. Pick a real table and
a column that no view, trigger, or index uses; otherwise the replay fails on the dependent object instead
of showing the finding. Say up front that the copy is thrown away:
```bash
tmp=$(mktemp -d) && cp -R migrations "$tmp/dir"
printf 'ALTER TABLE <table> DROP COLUMN <column>;\n' > "$tmp/dir/99999999999999_lint_demo.sql"
atlas migrate hash --env local --dir "file://$tmp/dir"
atlas migrate lint --env local --dir "file://$tmp/dir" --latest 1
rm -rf "$tmp"
```
Lint exits 1 with `DS103` (dropping a column) and `BC104` (clients using it will fail), plus a suggested
fix. Explain each finding in one sentence with its code (https://atlasgo.io/lint/analyzers): CI stops a
PR like this until someone approves it.
### Keeping an existing migration history (only on request)
The baseline is the default. Import a golang-migrate, goose, Flyway, Liquibase, or dbmate history
instead only when the user asks to keep it, and only when it covers exactly this unit's schemas
(https://atlasgo.io/versioned/import):
```bash
atlas migrate import --from "file://<old-dir>?format=flyway" --to "file://migrations"
```
Down and undo files are not imported, and comments that do not directly precede a statement are lost.
Fix the directory before anything else:
1. Order. Atlas runs files in file-name order, and imported versions are not zero-padded: `10_c.sql`
sorts before `1_a.sql`, and a converted Flyway repeatable `10R_va.sql` sorts before `10_c.sql`.
Compare `atlas migrate ls --env local` with the old tool's order (`flyway info`). If they differ,
rename the files to zero-padded versions that keep the old order, and give each repeatable file the
next free version after the last versioned file.
2. Repeatables run once. Flyway re-runs `R__` files when they change; after the import they are ordinary
files. Tell the user about any statement that must run on every deploy.
3. Replace `${placeholders}` with their values, and add `-- atlas:txmode none` as the first line of files
that ran outside a transaction (`CREATE INDEX CONCURRENTLY`).
4. `atlas migrate hash --env local`, then the same `atlas migrate diff --env local` check as above.
## Declarative
### 2.1 No migration directory
Declarative units have no migration files. `atlas.hcl` from stage 1 is complete: Atlas plans each change
against the target database when it is applied.
### 2.2 Show the change workflow
Every schema change from now on is: edit the desired state, then `atlas schema apply --env local`
against the local database. Atlas prints the planned statements, runs the lint analyzers on them, and
asks for approval (https://atlasgo.io/guides/evaluation/developer-workflow). `--dry-run` shows the plan
without applying it. On shared databases, changes go through a reviewed plan instead: CI creates it
with `atlas schema plan` on the PR (stage 3).
## Project PR
Before committing, look for secrets in the exported files and stop on any real one:
```bash
grep -rniE 'password|secret|token|api_?key' schema migrations
```
Open a pull request with the unit's directory and `.atlas-onboarding.json`, staged by path, titled
`Manage the <unit> schema with Atlas`. In the description, paste the stage 1 dry run, the stage 2 checks,
and the lint findings, and say that the current migration tool still deploys until stage 4. Merging is
the user's call.
## Check
1. Versioned: `atlas migrate validate --env local` passes, and `atlas migrate diff --env local` prints
`The migration directory is synced with the desired state, no changes to be made`.
2. The project PR is open.
Tell the user how a schema change works from now on, in their workflow's terms. Link
https://atlasgo.io/guides/evaluation/developer-workflow.
atlas-onboard/references/stage-3-ci.md
# Stage 3: CI
Goal: every pull request that touches the unit gets an Atlas lint report (or a plan, for declarative
units), and every merge to the default branch pushes a new version to the Atlas Registry. Start after
the project PR from stage 2 is merged. Nothing deploys in this stage.
This stage decides which workflows to add, handles the manual steps, and checks the result. Each CI
system has a step-by-step guide for each workflow; follow the one that matches the scan and the
workflow from stage 0. `atlas/references/cicd.md` summarizes the GitHub Actions pipelines, and the
`atlas-action` skill has each step's inputs on every platform and the fix for each failed step.
| CI system | Versioned | Declarative |
|-----------|-----------|-------------|
| GitHub Actions | https://atlasgo.io/guides/ci-platforms/github-versioned | https://atlasgo.io/guides/ci-platforms/github-declarative |
| GitLab CI | https://atlasgo.io/guides/ci-platforms/gitlab-versioned | https://atlasgo.io/guides/ci-platforms/gitlab-declarative |
| Bitbucket Pipelines | https://atlasgo.io/guides/ci-platforms/bitbucket-versioned | https://atlasgo.io/guides/ci-platforms/bitbucket-declarative |
| CircleCI | https://atlasgo.io/guides/ci-platforms/circleci-versioned | https://atlasgo.io/guides/ci-platforms/circleci-declarative |
| Azure DevOps, code on GitHub | https://atlasgo.io/guides/ci-platforms/azure-devops-github#versioned-migrations-workflow | https://atlasgo.io/guides/ci-platforms/azure-devops-github#declarative-workflow |
| Azure DevOps, code in Azure Repos | https://atlasgo.io/guides/ci-platforms/azure-devops-repos#versioned-migrations-workflow | https://atlasgo.io/guides/ci-platforms/azure-devops-repos#declarative-workflow |
All the CI guides are listed at https://atlasgo.io/guides.
## 3.1 Decide
- CI system: from the scan, with its guide from the table above. Anything else, such as Jenkins or
TeamCity, installs the CLI (`curl -sSf https://atlasgo.sh | sh`), runs
`atlas login --token "$ATLAS_TOKEN"`, then the same commands with `--env ci`.
- Optional jobs, off by default for the pilot: automatic migration generation and automatic rebase of
`atlas.sum` conflicts. Which platforms have them, and the extra token they need so their commits
trigger lint, is in `atlas/references/cicd.md`. Offer them once several developers change the schema.
For ORM units, the migration generation job also fails a PR whose models and directory disagree.
## 3.2 Credentials (manual step)
CI authenticates with a bot token. Only an organization admin can create one, and only in the Atlas
Cloud UI: ☰ > Settings > Bots > Create Bot. Create it in the organization the `atlas` block of
`atlas.hcl` pins; a token from another organization fails every command that reads the file. The token
is shown once. Ask the user to create it and store it as a CI secret themselves; it never passes
through the conversation.
| CI | Where | Name |
|----|-------|------|
| GitHub Actions | Settings > Secrets and variables > Actions, or `gh secret set ATLAS_CLOUD_TOKEN` in their own terminal | `ATLAS_CLOUD_TOKEN` |
| GitLab CI | Settings > CI/CD > Variables, masked, with Protect variable unchecked so merge request pipelines can read it | `ATLAS_CLOUD_TOKEN` |
| CircleCI | Organization Settings > Contexts | `ATLAS_TOKEN` |
| Bitbucket | Repository settings > Repository variables, secured | `ATLAS_TOKEN` |
| Azure DevOps | Pipelines > Library, variable group, marked secret | `ATLAS_TOKEN` |
If the repository already has an Atlas token secret under another name, reuse it and write that name
into the workflows. When the registry repo has protected flows enabled, the bot needs the Writer role
on it (https://atlasgo.io/cloud/roles-and-permissions).
Atlas posts its lint report, or the plan, as a comment on the pull request. Each CI system needs its
own credential for that, as its guide describes:
| CI | Comment credential |
|----|--------------------|
| GitHub Actions | None to create: the workflow sets `GITHUB_TOKEN: ${{ github.token }}` and `permissions: pull-requests: write` (both in the `atlas/references/cicd.md` templates) |
| GitLab CI | `GITLAB_TOKEN`: a project access token with the Reporter role and the `api` scope, stored as a CI/CD variable |
| Bitbucket | `BITBUCKET_ACCESS_TOKEN`: an app password with `pullrequest:write`, stored as a secured variable |
| CircleCI | `GITHUB_TOKEN`: a GitHub personal access token with the `repo` scope, and `GITHUB_REPOSITORY` (`owner/repo`), in the context |
| Azure DevOps, code on GitHub | A GitHub service connection, with OAuth or a personal access token |
| Azure DevOps, code in Azure Repos | The Build Service account allowed to Contribute and to Contribute to pull requests on the repository |
## 3.3 First Push (ask first)
The first push creates the registry repo, so lint on the CI PR has a version to compare with. Push from
an up-to-date default branch, and make sure the name is free:
```bash
git switch <default> && git pull --ff-only
atlas cloud repo describe --slug <repo>
```
If the repo exists and is not this unit's, agree on another slug with the user and record it as the
unit's `repo` in `.atlas-onboarding.json`. Then ask "Create registry repo `<repo>` in `<org>` with this
push?" and push:
```bash
atlas migrate push --env local <repo> # versioned
atlas schema push --env local <repo> # declarative
```
For ORM units, `--env local` runs the loader. If the loader's dependencies are not installed in this
shell, pass `--dir file://migrations --dev-url "<dev URL>"` instead of `--env local`.
Teams that keep every Atlas Cloud change in CI can create the repo with the `create-repo` action instead,
in a workflow run once by hand (`atlas/references/cicd.md`). The repo then starts empty, and its first
version arrives with the first merge.
## 3.4 Workflows (one PR)
Create the branch `atlas-onboard/<unit>-3` (Rule 4), then:
1. Add `env "ci"` to the unit's `atlas.hcl`. It needs no database URL: lint and plans run on the dev
database and compare with the registry (`atlas/references/cicd.md`, case 1).
```hcl
env "ci" { # versioned
dev = local.dev
exclude = local.exclude
migration {
dir = "file://migrations"
repo {
name = "<repo>"
}
}
}
env "ci" { # declarative
dev = local.dev
exclude = local.exclude
schema {
src = local.src
repo {
name = "<repo>"
}
}
}
```
Units that manage roles and permissions copy the `mode` block and the dev database from stage 2.
2. Add the PR and merge pipeline from the platform's guide in the table above, with the secret names
from 3.2. On GitHub Actions, `atlas/references/cicd.md` has the same pipelines: Versioned Pipeline
(`atlas-ci.yaml`) or Declarative Pipeline (`atlas-plan.yaml`). Remove every apply step, such as the
declarative `schema/apply` and the commented `migrate/apply`: stage 4 adds deployment.
3. For ORM units, every job whose env loads the models (the declarative plan, and the optional migration
generation job) installs the ORM's runtime and its Atlas provider before Atlas runs, as the ORM's
guide shows. For Django: `actions/setup-python`, then `pip install django atlas-provider-django`
(https://atlasgo.io/guides/orms/django/linting).
4. Keep test steps only for tests that exist: `schema/test` when the unit has schema tests,
`migrate/test` when it has migration tests.
5. Use the real default branch (`git symbolic-ref --short refs/remotes/origin/HEAD`), not `master`.
Filter paths with `**` globs under the unit's directory, and set `working-directory` to that
directory. A repository with several units runs one job per unit, each with its own directory and
paths.
6. Lint runs only on pull requests, and push runs only on the default branch. Check every generated job
for this, including jobs included from templates or components: on GitLab, give the included push job
`rules` that limit it to the default branch.
7. Keep the push job's `latest` push on (the default of `migrate/push`). The stage 4 drift check reads
the state that untagged pushes store; a pipeline that pushes only commit tags silently skips it.
The dev database in CI: hosted GitHub runners have Docker, so the `docker://` URL works. On runners
without a Docker daemon (GitLab's Docker executor, many self-hosted runners), point `dev` at a service
container attached to the Atlas jobs only, with the target's version and scope, or at an empty database
the runner can reach (`dev = getenv("DEV_URL")` from a CI secret,
https://atlasgo.io/concepts/dev-database#providing-your-own-dev-database). Never add services, images, or
variables at the top level of a pipeline that other jobs share.
Open the PR, titled `Add Atlas CI for <unit>`.
## Check
1. The PR's Atlas job passes. The PR adds no migration files, so lint has nothing to analyze and may post
no report; the first report appears on the next schema PR. Read the result with
`gh pr checks <number>`, `glab ci status`, or ask the user.
2. After the user merges, the push run on the default branch passed
(`gh run list --workflow <file> --branch <default> --limit 1`, the GitLab pipeline page, or ask),
and the registry has the merge commit's version:
```bash
atlas migrate ls --dir "atlas://<repo>?tag=<merge commit sha>" --latest
```
For declarative units, `atlas cloud repo describe --slug <repo>` shows the repo.
3. The Atlas check is required for merging, so a failing lint blocks the merge. The user sets this in
the repository host's branch protection; on Azure Repos, the guide's branch policy with build
validation set to Required does it.
4. Optionally, the user opens a draft PR with a sample change, such as the destructive change from stage
2.3, sees the report as a PR comment, and closes it without merging.
Tell the user what CI now does on every PR and every merge. Link https://atlasgo.io/versioned/setup-cicd
(or https://atlasgo.io/declarative/setup-cicd).
atlas-onboard/references/stage-4-deploy.md
# Stage 4: Deploy to Staging
Goal: staging deployments of the unit apply versions from the Atlas Registry, every run is recorded in
Atlas Cloud, and the old tool no longer deploys the unit to staging. This stage writes staging
configuration only: no production job, manifest, overlay, or module. Production is stage 5, in a
separate PR when the user asks for it.
## 4.1 Pick the Target
Detect it from the scan; ask only when several rows apply or none does. Record the answer as `deploy`
in `.atlas-onboarding.json`.
| Found | Target |
|-------|--------|
| CI deploys the application (a deploy job, a release workflow) | CI deploy job (4.4) |
| A release or pre-deploy command, or an entrypoint that runs migrations (Render, Heroku, Fly.io, ECS) | Platform release step (4.5) |
| Argo CD `Application` | Atlas Operator with Argo CD (4.6) |
| Flux `Kustomization` | Atlas Operator with Flux (4.7) |
| Terraform with the `ariga/atlas` provider, or the user wants schema changes in Terraform plans | Terraform (4.8) |
| Terraform that only provisions the database instance | Not a migration target: use the row for how the application deploys |
| A Helm `pre-upgrade` migration hook | The Atlas Operator (4.6 or 4.7); Helm hooks for migrations are deprecated |
## 4.2 Plan the Cutover
The old tool stops deploying the unit to an environment in the same change that starts Atlas for it.
1. Bring the code up to date. If the old tool applied changes since stage 1, add them to the desired
state (versioned units: generate them with `atlas migrate diff --env local <name>`), and rerun the
stage 1 and stage 2 checks.
2. Find each existing database's baseline: the version of the baseline file. For an imported history,
it is the Atlas version of the newest file the old tool's history table records as successful in
that environment. Ask the user for the tool's status output, such as `flyway info` or
`goose status`, or for a query on its history table. Environments can differ: never copy staging's
baseline to production. Set it as stage 2.2 describes: `baseline` in the env (4.3), or a one-time
`--baseline` run by the user.
3. Check each existing database against its baseline before its first deploy, with the `staging` env
from 4.3 and `STAGING_DATABASE_URL` set to a read-only role:
```bash
atlas schema apply --env staging --to "file://migrations?version=<baseline>" --dry-run
```
`Schema is synced, no changes to be made` means the database is at the baseline. Planned statements
mean the environment drifted from it: show them and let the user decide.
4. Retire the old tool for staging:
- If it deploys only this unit, remove its staging step in the deploy PR. When environments share
one artifact (one image, one entrypoint), switch by environment instead, with a variable only
staging sets. Never remove production's step before production deploys with Atlas.
- If it also deploys other units, keep it, and stop new files for this unit in the old directory
(a CODEOWNERS entry, or a CI check). Two tools may deploy to one database only when they change
different schemas.
- When the Atlas deploy and the removal are in different repositories, the user merges the Atlas
deploy first, then the removal, with no schema change in between.
5. ORM migration tools (Django, Alembic, Prisma Migrate) stop deploying too. Ask the user what
developers keep running them for, such as building test databases.
## 4.3 The Staging Env
Add a `staging` env to the unit's `atlas.hcl`, or extend the one stage 1 added for a remote database.
For a cloud sign-in (AWS IAM, Microsoft Entra ID, GCP IAM) or a password from a secret store, build
`url` as in stage 1, Remote Databases:
```hcl
env "staging" {
url = urlqueryset(getenv("STAGING_DATABASE_URL"), "search_path", "identity")
dev = local.dev
exclude = local.exclude
schema {
src = local.src
}
migration {
dir = "atlas://identity"
baseline = "<baseline version>" # only when the staging database existed before Atlas
}
check "migrate_apply" {
drift {
on_error = CONTINUE # report drift; switch to FAIL once staging is clean
}
}
}
```
- Declarative units drop the `migration` block and deploy the desired state from the registry: in
this env, `src = "atlas://<repo>"` replaces `src = local.src`.
- The env name is the environment name in Atlas Cloud. Deployments from `atlas://` directories are
reported automatically.
- Two roles, one env: locally, `STAGING_DATABASE_URL` holds a read-only role, enough for the checks and
dry runs in this stage. Deployments use a deploy role: the one the old tool deployed with, or one that
owns the unit's objects. Only the CI secret or the cluster Secret holds it, never the agent's shell.
- Database-scoped units that share a database with other units set `revisions_schema` and `lock_name`
in the `migration` block to `atlas_<unit>`, so each unit keeps its own history and lock. Schema-scoped
units keep the history table in their own schema.
- The drift check compares the database with the registry's state for its last applied version
before anything runs; it starts after the first deploy (`atlas/references/drift.md`). Only untagged
pushes store that state, such as the `latest` push that `migrate/push` makes by default. For a
version pushed only with a tag, the check prints `no state found for version ...` and is skipped,
even with `FAIL`. Keep `latest` on in the push job; a CLI pipeline pushes `<repo>` as well as
`<repo>:<sha>`.
- Versioned deploys have no approval step of their own. Review happens on the PR, and
`check "migrate_apply"` rules, the drift check, and promotion (stage 5) guard the apply.
## 4.4 CI Deploy Job
GitHub Actions, added to the stage 3 workflow after the push job:
```yaml
deploy-staging:
needs: push
runs-on: ubuntu-latest # copy runs-on and network setup from the old migration job
environment: staging # deployment branches: the default branch
env:
STAGING_DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN }}
- uses: ariga/atlas-action/migrate/apply@v1
with:
working-directory: <unit dir>
env: staging
dir: atlas://<repo>?tag=${{ github.sha }} # the version this merge pushed
```
- The runner must reach the staging database. Hosted runners cannot reach a private database: copy
the runner and network setup of the job that runs the old tool today.
- The job must finish before the application deploys. Add it to the application deploy job's `needs`.
- The user stores `STAGING_DATABASE_URL`, with the deploy user, as a secret of the `staging`
environment, whose deployment branches are the default branch only, so pull request jobs never get
it.
- GitLab CI: the `migrate-apply` component, as its own job with `rules` that limit it to the default
branch, `env: staging`, the URL from a protected variable, and the runner tags of the application's
staging deploy.
- Other CI systems: install the CLI, export the bot token as `ATLAS_TOKEN`, and run
`atlas migrate apply --env staging`.
- Declarative units: `ariga/atlas-action/schema/apply@v1` after the approve and push steps of
`atlas-plan.yaml`. Plan, approve, and apply must see the same starting state
(`atlas/references/cicd.md`, Declarative Pipeline).
## 4.5 Platform Release Step
On platforms that run a command before each release (Render's pre-deploy command, Heroku's release
phase, Fly.io's `release_command`, an ECS task before the service update), run
`atlas migrate apply --env staging` there instead of the old tool, not in the container entrypoint
(https://atlasgo.io/guides/deploying/fly-io). The image needs the `atlas` binary and the unit's
`atlas.hcl`. The user sets `ATLAS_TOKEN` and `STAGING_DATABASE_URL` in the platform's staging
environment. Release only after the stage 3 push job passed, so the registry has the version.
## 4.6 Atlas Operator with Argo CD
Read the `atlas-operator` skill first (Requires): it covers the operator's install, the
bot token Secret, the resource policies, and the errors a first rollout hits. The Operator must be
installed in the cluster (https://atlasgo.io/integrations/kubernetes/install).
That is usually the platform team's change; ask before adding it. Ask for a local checkout of the GitOps
repository, and write to its staging overlay only, never to a shared base. Reference Secrets that the
cluster's secret manager creates, by name, and never write secret values to Git:
```yaml
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasMigration
metadata:
name: <unit>
annotations:
argocd.argoproj.io/sync-wave: "1" # before the application's resources (wave "2")
spec:
envName: staging # environment name in Atlas Cloud
urlFrom:
secretKeyRef:
name: <unit>-db-credentials # deploy user; the URL carries the unit's scope (search_path)
key: url
cloud:
tokenFrom:
secretKeyRef:
name: atlas-token
key: token
dir:
remote:
name: <repo>
tag: "<commit sha>"
baseline: "<baseline version>" # only for databases that existed before Atlas
```
- Health: Argo CD 2.10 and later ship health checks for `AtlasMigration` and `AtlasSchema`. Add the
custom check from https://atlasgo.io/guides/deploying/k8s-argo only on older versions. A failed
migration, or a plan waiting for approval, reports `Degraded` and holds back later waves.
- Updating `tag` is the deployment; a new registry version does not trigger the Operator. Argo CD Image
Updater changes only container images, so add a CI job that commits the new tag to the GitOps
repository after the push job, with a token the user stores, limited to that repository.
- Sync waves order resources within one sync of one Application. If the application's image is bumped
in a separate commit or sync, the app can run before its migration: bump both in the same commit, or
require backward-compatible migrations (expand, deploy, contract) and add that rule to `AGENTS.md`.
- Database-scoped units that share a database set `revisionsSchema: atlas_<unit>`.
- Declarative units use `AtlasSchema` with `schema.url: atlas://<repo>?tag=<tag>` and a review policy
(`policy.lint.review: ERROR`, `WARNING`, or `ALWAYS`). A plan that needs review waits for a human to
approve it in Atlas Cloud (https://atlasgo.io/integrations/kubernetes/declarative).
- Optional drift monitoring: an `AtlasDriftCheck` next to the `AtlasMigration`, with `onDrift: Report`
first. Registry versions pushed with a tag need a dev database on the `AtlasMigration` (`devURLFrom`)
for the check to work (https://atlasgo.io/integrations/kubernetes/versioned).
## 4.7 Atlas Operator with Flux
The same Atlas resources as 4.6, ordered through Kustomizations instead of sync waves: the
Kustomization that holds the Atlas resource lists it under `healthChecks`, and the application's
Kustomization lists that one under `dependsOn` (https://atlasgo.io/guides/deploying/k8s-flux).
## 4.8 Terraform (decision point)
Offer this only when Terraform already deploys the application, or the user asks for schema changes in
Terraform plans. Use a provider `cloud` block plus `data "atlas_migration"` and
`resource "atlas_migration"` reading `atlas://<repo>`
(https://atlasgo.io/guides/terraform/opentaco), in the staging root module or workspace only. The
Terraform runner then needs network access to the database and the bot token. Pin the `ariga/atlas`
provider to the latest release on the Terraform Registry; version pins in older examples lag behind.
## 4.9 Run and Check (staging only)
1. Readiness: `atlas cloud migration list --repo <repo> --env-name staging --status FAILED` prints
`No migrations found.`
2. Approval with a named target. Ask the user for the staging database's name instead of reading the
URL: "Deploy `<repo>` version `<version>` to staging (`<database>`)?"
3. Dry run, with `STAGING_DATABASE_URL` in the agent's shell:
`atlas migrate apply --env staging --dry-run` (declarative:
`atlas schema apply --env staging --dry-run`). Atlas Cloud records it as `DRY_RUN`.
4. The user merges the deploy PR, or merges the GitOps PR, or runs the release.
5. Check:
```bash
atlas migrate ls --dir atlas://<repo> --latest --short
atlas cloud migration list --repo <repo> --env-name staging
atlas cloud database list --env-name staging
```
Done when the newest staging event is `PASSED` for that version and the database list shows the
unit's repo `SYNCED` at it. A first deploy that only records the baseline runs no file and is still
recorded as `PASSED`. On `FAILED`, follow `atlas/references/cloud.md`, Investigate a failed
deployment.
Tell the user what runs on each merge now, and where to see it (https://atlasgo.io/cloud/deployment).
Production is stage 5 (`references/stage-5-promote.md`): environment promotion from staging, in its own
PR when the user asks for it.
atlas-onboard/references/stage-5-promote.md
# Stage 5: Promote to Production
Goal: production applies only the version already deployed to staging. This is environment promotion,
and it builds on the earlier stages (https://atlasgo.io/guides/environment-promotion):
1. Push once: CI pushes every merge to the Atlas Registry (stage 3).
2. Validate in staging: staging deploys the pushed version, and Atlas Cloud records it (stage 4).
3. Promote: production reads, from Atlas Cloud, the version deployed to staging, and applies that
version.
The agent writes the production configuration in its own PR, only when the user asks for it. The user
approves that PR and runs the first production deployment; the agent never applies to production.
## 5.1 The Production Env
The `cloud_databases` data source reads the version deployed to staging. Use it to pin production to
that version (the default), or to gate production with a pre-execution check that fails the deployment
when the planned version is not staging's.
Versioned units pin with `to_version`:
```hcl
data "cloud_databases" "staging" {
repo = "<repo>"
env = "staging"
}
env "prod" {
url = urlqueryset(getenv("PROD_DATABASE_URL"), "search_path", "identity")
dev = local.dev
exclude = local.exclude
migration {
dir = "atlas://<repo>"
baseline = "<production baseline>" # only if production existed before Atlas (stage 4.2)
to_version = data.cloud_databases.staging.targets[0].current_version
}
}
```
To gate instead, keep `dir` unpinned and add the guide's `check "migrate_apply"` with a `deny` rule on
`self.planned_migration.target_version > local.staging_version`
(https://atlasgo.io/guides/environment-promotion#example-pre-execution-check-for-promotion).
Declarative units pin the desired state to staging's version
(https://atlasgo.io/guides/environment-promotion-declarative):
```hcl
env "prod" {
url = urlqueryset(getenv("PROD_DATABASE_URL"), "search_path", "identity")
dev = local.dev
exclude = local.exclude
schema {
src = "atlas://<repo>?version=${data.cloud_databases.staging.targets[0].current_version}"
}
}
```
To gate instead, keep `src = "atlas://<repo>"` and add the guide's `check "schema_apply"` with a
`deny "version_mismatch"` rule. Checks also run on `--dry-run`, so the gate can be tested without
touching production.
`targets[0]` assumes one staging database for the unit.
## 5.2 The Production Deployment
Production deploys with the same target as staging (stage 4), with production values:
- CI: a separate job or workflow with `env: prod` that runs only after the staging deployment succeeded,
in a protected environment that requires approval. It never runs on the merge alone.
- Atlas Operator: an `AtlasMigration` in the production overlay with `envName: prod` and the tag
staging runs. With the Operator, promotion is the tag.
- Platform release step: the production environment's release command, with `PROD_DATABASE_URL`.
The same PR retires the old tool for production (stage 4.2). Before the first production deployment of
a versioned unit, the user checks production against its baseline, since it needs production access:
```bash
atlas schema apply --env prod --to "file://migrations?version=<production baseline>" --dry-run
```
## Check
After the user runs the first production deployment:
```bash
atlas cloud migration list --repo <repo> --env-name prod --status PASSED
atlas cloud database list --env-name prod
atlas cloud database list --env-name staging
```
Done when the production deployment passed, and production's `CURRENT VERSION` is staging's.
Tell the user how a schema change reaches production now: pull request, CI, merge, staging, promotion.
Link https://atlasgo.io/guides/environment-promotion (or the declarative guide).