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

> Documentation Index
> Fetch the complete documentation index at: https://docs.tryatlantis.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# tide CLI

```
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:

```yaml
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](/concepts/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](/guides/adopt-an-existing-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`](https://buf.build/docs/installation) 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](/guides/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](/guides/use-the-sandbox/) and the
[sandbox HTTP API](/reference/sandbox-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](/guides/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](/guides/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](https://protobuf.dev/programming-guides/json/) 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.

Source: https://docs.tryatlantis.dev/reference/cli-tide/index.mdx
