Atlas Scripts run data operations against your database as declarative code: transactional mutations, reports, and batched loops, with transactions, assertions, and output masking built in.
Schema migrations change the shape of your database: tables, columns, and indexes. Atlas Scripts change or read the data. They are a declarative language for running data operations against a database: one-off or planned mutations, reads and reports, and batched or repeated loops. You describe the work; Atlas runs it with transactions, assertions, and output masking built in.
If you use Terraform, Atlas Scripts are Actions for your database: the Day 2 work that comes after the schema is in place. Much like terraform apply -invoke, you run one by name, and it leaves no trace in your migration history.
Use an Atlas Script when ad-hoc or planned data work needs a transaction, a pre-flight guard, or a controlled loop: backfills, GDPR and privacy purges, reports with masked columns, and scheduled monitors and probes.
Wiring a Script Source
Point an environment at your scripts with a script block, so the atlas script commands pick up the source from the selected environment:
env "prod" {script {src = "file://scripts"}}
A Transactional Mutation
A script "exec" runs a set of writes as one transactional unit with guardrails. It gates on a condition, reads the target rows once, and asserts each write touched exactly the rows it expected. Any failure rolls the whole unit back:
script "exec" "archive_dormant" {# Stop cleanly if there is nothing to archive.condition "have_dormant" {sql = "SELECT count(*) > 0 FROM accounts WHERE last_seen < '2020-01-01'"}# Read the dormant rows once and bind them for the writes below.query "dormant" {sql = "SELECT id FROM accounts WHERE last_seen < '2020-01-01' ORDER BY id"rows { id = int }}# Delete exactly the rows the read found, or roll the whole unit back.exec "purge" {sql = "DELETE FROM accounts WHERE id IN (SELECT value FROM json_each(?))"args = [jsonencode(query.dormant.rows[*].id)]expect_rows = length(query.dormant.rows)}}
Run it against your database with atlas script exec:
atlas script exec --url "$URL" --file "file://archive.hcl" --run archive_dormant
Reports with Masked Columns
A script "query" reads and prints result sets as CSV or an aligned table. A mask block redacts columns before the results leave Atlas, so a report is safe to share: REDACT hides the value, PARTIAL keeps the edges, HASH tokenizes it with a keyed digest so it stays joinable across reports, and REPLACE rewrites matched characters.
script "query" "customer_export" {query "rows" {sql = "SELECT id, email, card FROM customers ORDER BY id"format = CSV# Hash the identity column with a keyed digest, keep the card's last four.mask {columns = ["email"]method = HASHsalt = "prod-secret"}mask {columns = ["card"]method = PARTIALkeep_right = 4}}}
Batched Loops
A script "loop" runs a do body once per keyset or range batch, each iteration in its own transaction, so a large backfill or purge runs in bounded units instead of one long statement. Pacing, staged ramp-up, and back-pressure are declared in a policy block.
script "loop" "purge_inactive" {# Page through the table with a cursor, 500 rows at a time.iterator "keyset" {cursor {id = int}init {sql = "SELECT id FROM users WHERE active = 0 ORDER BY id LIMIT 500"}next {sql = "SELECT id FROM users WHERE active = 0 AND id > ? ORDER BY id LIMIT 500"args = [cursor.id]}}# Runs once per page, each iteration in its own transaction.do {exec {sql = "DELETE FROM users WHERE id IN (SELECT value FROM json_each(?))"args = [jsonencode(iterator.keyset.batch[*].id)]}}}
Calling Services from a Batch
A purge rarely ends at the database. An http command in the do body sends the batch to a service, so one reviewed script drives the whole cascade: blob storage, caches, and search indexes. A request cannot be rolled back, so deliver first and mark the rows done after, leaving a failed batch pending for the next run:
do {# Drop the batch from the search index first.http "search" {url = "https://search.internal/documents/delete"method = POSTbody = jsonencode({ ids = iterator.keyset.batch[*].id })expect_status = 200}# Mark the rows purged only after the call succeeds.exec {sql = "UPDATE users SET purged = 1 WHERE id IN (SELECT value FROM json_each(?))"args = [jsonencode(iterator.keyset.batch[*].id)]}}policy {tx {mode = MANUAL}}
Tested Like Code
Because a script is code rather than an ad-hoc SQL session, it is versioned, reviewed, and verified in CI before it runs in production. A script command inside a test case runs it against a real database and asserts on the result. An as block runs the same script under a reduced role or login, so a passing test is also a guarantee about the privileges it will run with:
test "schema" "cancel_pending" {exec {sql = "INSERT INTO orders (id, status) VALUES (1, 'pending'), (2, 'shipped')"}script "exec" {file = "scripts.hcl"run = "^cancel_pending$"}script "query" {file = "scripts.hcl"run = "^pending_count$"output = "0"}}
Atlas Scripts are available to Atlas Pro users that purchased Atlas Pipelines; run atlas login to get started. See the Atlas Scripts documentation for the full reference, including masking, batched loops, and testing.