Back to changelog
New
4 minute read

Atlas Scripts: Run Data Operations as Code

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:

atlas.hcl
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:

archive.hcl
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.

export.hcl
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 = HASH
salt = "prod-secret"
}
mask {
columns = ["card"]
method = PARTIAL
keep_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.

purge.hcl
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:

purge.hcl
do {
# Drop the batch from the search index first.
http "search" {
url = "https://search.internal/documents/delete"
method = POST
body = 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:

scripts.test.hcl
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.

featuredataatlas scriptsatlas pro