Skip to content

tide CLI

The caller-side CLI operates on a service repo containing one or more .atl files and a tide.yaml.

tide <command> [flags]

The caller-side CLI. It operates on a service repo containing one or more .atl files and a tide.yaml.

-h and --help are accepted by leaf commands — those that take flags. Bare tide, and a group name (job, workflow, caller, backfill, sandbox) without a subcommand, print usage and exit 2.

Configuration file

tide reads ./tide.yaml from the current directory (overridable per command with --config <path>). The file carries:

caller: <name>                # required
org: <organisation>           # only when more than one organisation is enrolled
schema_paths:                 # required — at least one directory
  - internal/foo
  - internal/bar
output_dir: internal/atlantis # required for `tide generate`
generate:                     # required for `tide generate` — namespaces to emit
  - consumer
  - vendor

output_dir is the directory inside the caller’s own Go module where tide generate writes the typed client. generate lists the namespaces the caller consumes (its own plus any it reads cross-namespace). Both are only required for tide generate; the other commands ignore them.

The server address and the TLS material are not held in tide.yaml. tide login collects both into the credential store at ~/.atlantis/<org>/<caller>/, and tide renews the certificate on its own at two thirds of its life. When org: is unset and exactly one organisation is enrolled, that one is used; with several enrolled, an unset org: is refused rather than guessed.

schema_paths semantics

Each path is walked recursively; every file with extension .atl is included, and dot-directories are skipped — .tide-cache/ and .git/ are never submitted. Order does not affect schema resolution. Paths are recorded relative to the caller’s repo root so server-side error messages stay useful in the caller’s context.

Environment variables

Variable Overrides Notes
ATL_CALLER caller
ATL_ORG org Which organisation this repository’s caller belongs to. Needed only when more than one is enrolled.
ATL_GENERATE generate Comma-separated namespace list; replaces the generate: field for tide generate.
ATLANTIS_HOME Where the credential store lives. Defaults to ~/.atlantis.
ATL_CLOUD_URL Where Atlantis Cloud is, for tide login’s browser flow. Release builds carry the address; a development build needs this.
ATL_ENROLL_URL The organisation’s enrolment listener, for tide login --oidc in CI. The browser flow discovers it and never needs this.

TIDE_CALLER is not consulted. The equivalent variable is ATL_CALLER.

tide job and tide workflow read $USER to stamp the submitting principal on jobs and workflow runs.

Commands

Command Purpose --timeout default
tide init Write tide.yaml
tide login Enrol this machine
tide apply Submit .atl files and run the migration 30s
tide plan Report what an apply would do 30s
tide rehearse Execute the migration against a clone of the real database 20m
tide inspect Report drift between the database and the declarations 2m
tide pull Refresh the local .tide-cache/ mirror 30s
tide generate Write the typed Go client (--check verifies it) 30s
tide list / tide show Print the merged schema’s files 30s
tide history / tide diff / tide blame / tide owners Version registry reads 10s
tide rollback Preview a rollback (--dry-run) 30s
tide parked List parked objects 10s
tide backfill status Report backfill progress 10s
tide job … / tide workflow … Submit and inspect background work 10s
tide caller alias … Manage caller aliases (operator) 10s
tide sandbox … Local sandbox (source builds only)
tide version Print the version banner

Commands that dial the server accept --config <path> (default tide.yaml) and --timeout <duration>, with the defaults above. Duration uses Go’s time.ParseDuration format (e.g. 30s, 1m, 500ms). tide init, tide login, tide sandbox, and tide version take neither flag.

tide init

tide init --caller NAME [--org ORG] [--schema-path DIR]
          [--output-dir DIR] [--generate ns1,ns2]

Writes tide.yaml — the caller name, where the .atl files live, and optionally where a generated client goes. Refuses to overwrite one that exists. The optional half is written as commented guidance until asked for, so the file says what else it can carry.

The first command in a new repository; tide login is the second.

tide apply

Submits the local .atl files to the server, runs the migration, and prints a hint for the caller to regenerate the typed Go client. There is no endpoint override — apply targets the address the credential store holds for this caller.

tide apply [--backfill] [--dry-run] [--no-pull] [--wait-for-approval <duration>]
Flag Description
--backfill Boolean. Starts the declarative backfill flow for a backfill_required plan, calling BeginBackfillPlan. Progress is reported by tide backfill status.
--dry-run Plan only; do not apply. Exits with tide plan’s code map — a destructive class exits 4. Overrides --backfill: with both set, the backfill that would run is listed and nothing is started.
--no-pull Skip the automatic tide pull before the apply, for an offline run or a known-current cache.
--timeout Bounds a single RPC. Default 30s.
--wait-for-approval Default 0 (off). Hold the process open this long, retrying the apply every 15 seconds until a person decides. Independent of --timeout: each retry gets its own --timeout.

The default flow runs tide pull first so cross-caller references resolve against the freshest merged schema.

apply also checks the live database for drift inside its locked transaction, and refuses when it finds any of three kinds: a bare unique index the schema doesn’t declare (the refusal carries a DROP INDEX remediation), a check constraint that diverges from the declaration, or a column whose type or width has moved. The refusal names the object and the fix, and also a server-side override — on Atlantis Cloud, set by platform operators, not by callers. Drift refusals don’t change the plan class or exit code.

tide plan

Validates the local schema against the server and reports what would change. Performs no server-side writes.

tide plan [--format {table|json}] [--no-pull]
Flag Description
--format {table|json} Default table. json emits the raw planning response for downstream tools.
--no-pull Skip the pre-plan refresh of .tide-cache/.

A bare unique index the schema doesn’t declare surfaces as an index-drift warning, but only under --format=json — in the index_drift, index_drift_notes, and index_drift_error fields. The table output does not render it. Drift never blocks plan or changes its exit code; tide apply is where it refuses.

tide rehearse

Executes the working tree’s migration against a disposable clone of the managed database — schema and real rows, copied under one snapshot — and reports what Postgres did. The clone is destroyed when the rehearsal ends; nothing it runs writes to the managed database.

tide rehearse [--format {table|json}] [--timeout <duration>]
Flag Description
--format {table|json} Default table. json carries verdict, SQLSTATE, redacted error, per-constraint violation counts, remediation, and timings.
--timeout <duration> Default 20m.

Verdicts, and the exit code each maps to:

Verdict Exit Meaning
pass 0 The migration completed against the cloned rows.
pass_with_warnings 0 Completed, with something worth reading in the output.
fail_data 2 Existing rows violate the change — the real apply would fail the same way. The output carries violation counts and, where the server knows a template, a suggested fix.
fail_structural 2 The SQL itself does not run.
unverified 4 The rehearsal could not answer — one of the limits below was hit.

Rehearsal limits:

Limit Value
Database size the clone accepts 25 GiB
Clone timeout 15 minutes
Execution timeout 5 minutes
Concurrent rehearsals per deployment 2

Errors are redacted before they are stored or returned: SQLSTATE, message, and the constraint, table, and column names are kept; the detail lines that embed row values are not.

A verdict binds to the exact content it rehearsed — the same identity an approval binds to — and the apply gate consumes it only within an hour, so the auto_verified and auto_all tiers act on fresh passes and nothing else. unverified never satisfies a tier. See change approval.

Rehearsing needs the caller rehearsal-enabled — an admin’s per-caller grant on the console’s Callers page. A clone holds every caller’s rows.

tide inspect

Reports how the live database differs from your .atl files. Writes nothing.

tide inspect [--format {table|json}] [--timeout <duration>]
             [--generate <dir>] [--schemas <list>]
Flag Description
--format {table|json} Default table. json emits the raw drift report.
--timeout <duration> Default 2m.
--generate <dir> Write .atl for tables no declaration mentions into <dir>, one file per entity. Never overwrites an existing file.
--schemas <list> With --generate: comma-separated Postgres schemas to search. Default is every non-system schema.

plan compares the local files against the recorded checkpoint and reports what an apply would do. inspect compares them against the live database and reports where the two differ: a column added by hand, a dropped policy, a table that no longer matches its declaration.

Findings are grouped by severity:

Severity Meaning Exit code
addition Declared, not in the database yet 1
removal In the database, no longer declared 1
mismatch Both exist and disagree 2

An addition is resolved by tide apply; a mismatch means the database was changed outside atlantis, and no apply reconciles it.

The report ends with a not checked section. Introspection does not read indexes, uniques or check predicates back from the catalogue, so a clean run means the columns, types, keys and tenant isolation agree — not that everything agrees.

The server runs inspect inside a read-only transaction, so it is safe to point at production from CI.

Generating declarations for a database you already have

tide inspect --generate=schema/

Discovers tables nothing declares, reads their columns, types, keys, defaults and foreign keys from the catalogue, and writes a .atl file per table — the route into adopting a database without hand-writing every declaration.

--generate is the one form of inspect that does not require an existing declaration — it runs in a repository with no .atl files and tolerates a schema_paths directory that does not exist yet. Every other form refuses, because there is nothing to compare the database against.

Entities are named from the table — user_accounts becomes UserAccounts, with no attempt to singularise. That name becomes a generated Go type, a proto message, and part of the entity ID other callers reference, so renaming it after adoption is a breaking change.

Skipped tables are reported with a reason. Three cases produce a skip:

  • the entity name is already declared in this namespace — rename one, or declare the table by hand;
  • two discovered tables in different Postgres schemas want the same entity name — generate one of them into its own namespace;
  • the name is not usable as a filename: atlantis does not write a filename derived from a table name unchecked, because a Postgres identifier can contain / and ...

Existing files are never overwritten — deleting one regenerates it on the next run.

Partition children, views, the platform’s own tables and TimescaleDB chunk storage are not offered.

Warnings about what introspection could not verify print with the run. The same limits described above apply, and a generated file understates your schema.

tide pull

Downloads the merged schema into .tide-cache/schema/ and records the server’s schema version in .tide-cache/version.json. Subsequent pulls short-circuit when the version matches.

tide pull [--force]
Flag Description
--force Pull even if the local cache version equals the server’s.

.tide-cache/ mirrors every caller’s currently-registered .atl files. It is a local cache, distinct from the generated Go client, and belongs in .gitignore.

tide generate

Generates the typed Go client SDK into the caller’s own repo, scoped to the namespaces in generate:.

tide generate

The flow:

  1. Fetch the canonical IR from the server (the GetCanonicalIR admin RPC). The canonical IR is the server’s persisted schema checkpoint, with proto field numbers already assigned. Pulling those numbers from the server means the generated wire format matches it exactly — the caller never re-derives numbers locally.
  2. Filter the IR to the generate: namespaces. The caller gets typed clients only for what it consumes, not every caller’s entities.
  3. Read the caller’s go.mod to compute the package prefix <module>/<output_dir>.
  4. Emit proto sources (the scoped namespaces plus the embedded atlantis/common/v1 protos) and the typed Go wrappers into output_dir, then shell out to buf generate for the .pb.go wire types and format the Go sources.

Requirements:

  • output_dir and a non-empty generate: list in tide.yaml.
  • buf on PATH.
  • A readable go.mod at the caller repo root, which is where generate runs.

The generated tree lives in the caller’s module and is imported with the caller’s own import path — the caller’s source, committed like any other generated code, not a shared artifact. There is no dependency on a shared SDK for the generated types; the hand-written clients/go/jobs runtime is a normal library dependency for callers that run job workers.

Generate rewrites atlantis/, pb/ and client/ under output_dir, plus buf.gen.yaml and buf.yaml at its top, and leaves anything else there alone, so an entity dropped from the schema does not leave its client behind. output_dir must name a directory inside the repository; ., an absolute path, and anything reaching upward through .. are refused, because that value decides what is removed.

Beside the code it writes tide.manifest.json, recording the caller, the namespaces, the module prefix and a hash per generated file. It is committed with them; --check compares against it.

tide generate --check

Verifies the committed client against its manifest and writes nothing.

tide generate --check

It requires no credentials, no network and no buf, and runs in a pipeline with no enrolled identity. It detects a hand-edited file, a missing file, and any file in the generated tree that the manifest does not record.

It does not detect a change in the server’s schema. --against-server does:

tide generate --check --against-server

which additionally fetches the canonical IR, re-runs the emitters in memory and compares them against the manifest. It never invokes buf: a .pb.go is a function of its .proto and the plugin versions pinned in buf.gen.yaml, so comparing the protos settles it. This form dials the server and therefore needs credentials.

Exit Meaning
0 the client matches
1 it does not, or there is no manifest — run tide generate
3 the question could not be answered: unreadable manifest, bad config, or the server was unreachable under --against-server

tide apply prints a regeneration reminder when it changes a namespace the caller consumes.

tide list

Fetches the merged schema and prints the path of every .atl file, sorted lexically.

tide list

tide show <substring>

Fetches the merged schema and prints the canonical .atl text of every file whose full path contains the substring. Case-sensitive match.

tide show <substring>

Exits non-zero if no file matches.

tide backfill status [<plan-hash>]

Reports the progress of a declarative backfill started by tide apply --backfill. With no argument, it reports the latest backfill plan for the configured caller. With a plan hash, it reports that plan.

tide backfill status [<plan-hash>] [--format {table|json}]

tide job submit|status|dead|retry

Submits and inspects background jobs.

tide job submit <job-name> [--args=JSON] [--scheduled-at=RFC3339]
tide job status <job-id> [--format {table|json}]
tide job dead   [--job-name=...] [--limit=N] [--format {table|json}]
tide job retry  <dead-job-id>

submit enqueues a job (optionally scheduled for a future time); status reports one job’s state; dead lists jobs in the dead-letter queue; retry re-enqueues a dead job. The submitting principal is stamped from $USER.

tide workflow start|status

Starts and inspects multi-step workflows.

tide workflow start  <workflow-name> [--state=JSON]
tide workflow status <workflow-id> [--format {table|json}]

The submitting principal is stamped from $USER.

tide history

Prints schema versions newest-first: version number, caller, event type, change count, and timestamp.

tide history [--limit N] [--caller X] [--format json]

tide diff <from-version> <to-version>

Computes the structural diff between two historical schema versions. The server loads both IR snapshots and runs the diff.

tide diff <from-version> <to-version> [--format {table|json}]

tide blame <entity-id>

Shows per-field provenance for an entity: who introduced each field, who last modified it, and the schema versions those events map to.

tide blame <entity-id> [--format {table|json}]

tide owners

Prints every active entity and the caller that introduced it, without reading version history.

tide owners [--format {table|json}]

tide rollback

Reverts the live schema to the state captured by a prior version. The server diffs current → target and emits the migration.

tide rollback --to=<version> [--dry-run] [--yes]
Flag Description
--to=<version> Target schema version to revert to.
--dry-run Emit the rollback plan without applying it.
--yes Skip the interactive confirmation.

Without --yes, the command prints Continue? [y/N] and aborts with exit 1 on anything but y. Executing a rollback needs an operator capability callers do not hold, so from a caller enrolment only --dry-run succeeds; the console’s Operations page performs rollbacks (admin role).

tide parked

Lists the objects destructive migrations kept instead of dropping, and how long each remains recoverable.

tide parked [--all] [--limit N] [--format {table|json}]
Flag Description
--all Include objects already reaped. Default shows only recoverable ones.
--limit N Default 100.
--format {table|json} Default table.

Columns: kind, object, recoverable-for, state. An object under 7 days from reaping renders highlighted; one whose reap has failed twice or more shows as stuck. Restoring is a schema change: revert the .atl that dropped the object and apply, and the object comes back with its rows — see Recover a dropped table.

tide sandbox boot|shell|spawn

Drives a local, schema-true sandbox bound to a compiled .atl tree.

tide sandbox boot  <path> [--addr ADDR] [--seed N] [--strict] [--backend {sim|embedded|auto}]
tide sandbox shell <path>
tide sandbox spawn <path> [-n N] [--seed N] [--strict]

boot starts the sandbox HTTP control plane on 127.0.0.1:0 (kernel-chosen port) unless --addr pins one, and prints the URL and route list. shell is an interactive SQL REPL (.tables, .describe, .sample, .quit). spawn forks N children from one parent (default 100) and prints timings; it runs on the sim backend only.

The prebuilt tide binaries do not include this command: it needs cgo, and the released binaries are cgo-free so they run without a C toolchain. Running it means building tide from source with CGO_ENABLED=1. The hosted product’s sandbox is the console’s Sandbox page, which needs no local build — see Use the sandbox and the sandbox HTTP API.

tide caller alias list|add|rm

Manages caller identity aliases. Requires CAPABILITY_OPERATOR.

tide caller alias list <caller>
tide caller alias add  <caller> <alias>
tide caller alias rm   <caller> <alias>

Exit codes here differ from the rest of tide: 1 covers config, dial, and RPC failures (elsewhere 3); 2 is a usage error.

tide login

tide login

Opens the browser: sign in at Atlantis Cloud, type the code the terminal is showing, approve. The caller comes from tide.yaml’s caller:, or --caller. The credential lands in ~/.atlantis/<org>/<caller>/ and renews on its own.

The code is typed at the approval page, never carried in a link — a link with the code embedded would let a mailed click approve someone else’s machine. Approving requires being a member of the organisation; enrolling as a caller requires the admin role, or the caller’s “developers may enrol” flag for every role below admin — developers included.

tide login --oidc [--url URL] [--audience AUD]

CI: exchanges the runner’s workload identity (GitHub Actions’ OIDC token) for a one-hour certificate, under a federation rule configured on the console’s Callers page. Needs permissions: id-token: write in the workflow, the enrolment address via --url or ATL_ENROLL_URL, and the organisation via tide.yaml’s org: or --org. A complete workflow is in Set up CI.

tide login --url URL --org ORG --token TOKEN [--ca FILE]

The alternate method: a single-use token an admin minted on the Callers page, for enrolling a machine on someone’s behalf. --ca is local development only, and also stores the authority so renewal keeps verifying against it.

tide version

Prints the tide logo banner followed by the version. Does not contact the server.

tide version

Exit codes

Code Meaning
0 Success, or no-op (e.g., tide pull with the local cache already current); rehearse verdict pass or pass_with_warnings
1 Backfill required — tide apply or tide plan returned a backfill-required class; inspect found outstanding work (additions/removals); generate --check found the committed client out of date; or caller alias hit any failure (its one exception to the map)
2 Unknown subcommand passed to tide itself; cross-caller breaking change from plan; apply blocked on an approval that has not been given; inspect found a mismatch; or rehearse verdict fail_data / fail_structural
3 Operational error: parse/validation failure, network error, config error, or unknown plan class
4 Destructive change from plan; or rehearse verdict unverified

tide plan exits on the plan class it computed. tide apply exits on the server’s decision. A destructive change exits 4 from plan; from apply the same change exits 0 when policy permits it unattended and 2 when it requires approval. Code 2 from apply means the gate held the change for a decision.

Code 2 also covers usage errors: an unknown subcommand, a group name without a subcommand, tide init over an existing tide.yaml, or a missing required flag. A CI script that needs to tell them apart parses stderr. Set up CI carries a full exit-code branch table.

Dropped objects are parked rather than deleted, and reaped after the retention window. tide parked lists what is held and until when.

Cache layout

tide pull writes to a local cache at .tide-cache/:

.tide-cache/
├── schema/
│   └── <submitted path>.atl   # every caller's files, at their registered paths
├── version.json
└── generate.json

generate.json records the checkpoint content hash the last tide generate ran against, with a timestamp. Nothing reads it back, and it is not the value --check compares.

tide list and tide show fetch from the server on every invocation; they do not read the cache. Deleting .tide-cache/ only affects the next tide pull (and the automatic pre-pull inside tide apply/plan).

Output

Errors print to stderr prefixed tide:. Success, progress, and informational lines print with glyph prefixes (, , ); table output prints bare rows.

--format=json

JSON output is the proto3 canonical JSON mapping of the admin API messages defined in atlantis/admin/v1/admin.proto.

Four properties of the encoding:

  • 64-bit integers are strings. "version": "7", not "version": 7. JSON numbers are IEEE-754 doubles and lose precision above 2^53; every 64-bit field here — schema versions, timestamps, row counts — is one you may compare for equality, so the mapping quotes them.
  • Enums are their full names. A plan class is "PLAN_CLASS_ADDITIVE", not "additive"; a check-drift kind is "CHECK_DRIFT_KIND_LIVE_NOT_DECLARED". The human-readable table output still prints the short form.
  • Empty lists and maps are [] and {}, never null or an absent key, so a consumer can iterate without a nil check. Fields declared optional in the proto are still omitted when unset, which is how “not set” stays distinguishable from “set to empty”.
  • Field names are snake_case — the proto field names, matching the .atl grammar and the SQL columns rather than protojson’s default lowerCamelCase.

Whitespace is not stable between builds; parsed values are the comparable form, not bytes.

Fields that carry a JSON document — args, state, diff, from_ir, to_ir, ir_snapshot — are inlined as JSON, not base64. They are bytes on the wire because they are content-hash inputs and must stay byte-exact, but rendering them base64 in a terminal would make tide diff --format=json | jq '.diff.additive' useless. A payload that is not valid JSON is left as the base64 string rather than silently nulled.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close