List the sections
See which sections are registered and can be exported or imported.Export
Export the named sections as a backup document. The document prints to stdout — redirect it to a file to save it.--section is repeatable.
errors map: tai backup export still prints the document, writes each failed section and its message to stderr, and exits 1, and the Studio’s Backup tab keeps the download and shows a warning naming each failed section and its message.
Import
Import selected sections from a backup document file. Name each section to apply; a section in the file that you do not name is left untouched.Skip or overwrite
An import runs under one mode, applied per record:skip (the default) or overwrite. The mode is decided against each record’s natural key — the field that identifies it within its section:
- Under
skip, a record whose key already exists on the server is left exactly as it stands — its live value is never replaced — and only records with a new key are created. - Under
overwrite, an existing record is upserted from the backup; new records are still created.
name, a topic verifier by its topic, a stored object by its path, a sub-MCP app by its slug, a stored connection by its id, an access-control route by its url. New records are created under both modes; the mode only decides what happens when a key collides with a live one.
Mode is a property of the HTTP import door, not the CLI: POST /api/backup/import takes a "mode" field in its body (an unrecognized value is a 400). The tai backup import command sends no mode, so the CLI always restores in the default skip — the non-destructive choice. To overwrite, call the door with "mode": "overwrite".
The sections that ignore mode
A few sections do not key per record, so mode does not apply:manifestrestores as a whole document — a single manifest always replaces the live one, through the mutation pipeline below.envmerges: every key in the backup is applied, updating a key that exists and adding one that does not. It never removes a live key the backup omits.- API-key tokens (inside
access_control) are never overwritten, under either mode. A restore mints brand-new keys for absent user ids and surfaces each plaintext innew_api_keysto redistribute; a user id already provisioned keeps its live key and is reported as skipped. To replace one, revoke that user id’s key first, then re-import — the mint refuses to overwrite an existing identity.
Access-control routes are canonical
The route table holds every url in its canonical form (each segment decoded once, slashes collapsed, dot segments resolved, no trailing slash), and anaccess_control import matches an archive url against the live table by that form. Two archive urls that reduce to one canonical route (/api/x and /api//x/) would overwrite each other, so the whole section is refused before its first write, the collision named in the section’s errors; an archive url with no canonical form is refused the same way. Correct the archive and import again.
A deployment refuses to boot while its route table holds a row that is not in canonical form, naming every such row. Every route door and the import write the canonical form, so such a row comes from a write made directly to the database, and no door can reach it while the boot refuses. Correct it there: in the skeleton database’s access_control_routes table, set each named row’s url to its canonical form — or delete the row when its canonical url is already in the table, or when the url has no canonical form at all — then start the deployment again.
An access_control import invalidates every worker’s cached policy decisions once when it ends, and a sub_mcp import invalidates them after each restored app, so a restored route, policy or mount is enforced on the next request.
The accounts archive
The Postgres accounts provider registers its ownaccounts section: the account
roster (email, password hash, disabled flag, creation time), never sessions or
pending invites. Its archive format is version 2, which carries no role — a
member’s role lives in their access-control policy, restored by the
access_control section. An archive of version 1 is refused before anything is
written, and the section’s report names the version: recreate the archive from a
deployment on the current baseline, then import it.
API keys and the identity store
An api key spans two stores: its policy — scopes, condition, fingerprint, and owner — lives in Postgres, while the secret exists only as a hash on the key’s identity record, which the identity provider keeps in Redis. An export carries the policy alone and never any key material, which is why a restore re-mints keys and returns each new plaintext once innew_api_keys.
Restore the two stores together: bring Redis and Postgres back from snapshots of the same moment, or import access_control onto an empty Postgres so every key is minted fresh. A restore that lands Postgres rows on a flushed identity store leaves those keys with a policy row but no identity — an orphaned state the platform names and clears rather than stranding:
- The key listing marks each orphaned row
orphaned: true, so you can find them. - A restore re-mints an identity for each orphaned row under the same user id, leaves its policy — scopes, condition, fingerprint, and owner — untouched, and returns the new plaintext once in
new_api_keys. A user id whose identity is still present is skipped, as above. - Revoking an orphaned key clears its policy row.
- Minting a key on an orphaned user id is refused until you re-import the backup to re-mint it, or revoke it.
- The setup door does not re-open: once the owner principal exists in Postgres,
POST /api/setupanswers409even when every key is orphaned. To recover the owner’s access, re-import anaccess_controlexport — it re-mints every orphaned key, including the owner’s. Without an export, runtai setup --recover --token <setup token>on the deployment host: it re-mints only the owner’s key onto the surviving policy row (same fingerprint, scopes, and owner; a fresh identity record; the plaintext printed once), and refuses while an owner key still authenticates. Revoke or re-mint the other orphaned keys throughtai keysafterward.
The import report
Each section returns a report:created, updated, and two distinct skip counts. skipped_existing counts records left untouched because their key already existed under skip — a clean, expected skip, not a failure. errors (with a matching skipped) counts records rejected — a malformed entry, an unsafe path, a token bound to a scope that never materialized, a token whose policy data carries disabled (a key has no disabled state; it is revoked), a token minted fresh with neither a scope nor a condition (it would grant nothing), a route mapping a URL that a route declared public serves — each named in the report so one bad record never silently drops or aborts the rest.
Restoring the manifest or env section applies live and fleet-wide: the import runs the same mutation pipeline a config change does — validate, persist, reload the calling process, then broadcast on the worker bus. The import report carries the per-origin fleet report of that reload, so a worker a restore did not reach is named rather than hidden.
A restore that writes the connector_connections section also moves each restored connection’s token cache entry to the restored version once the database write commits, so no worker keeps serving the pre-restore token. A connection restored with an already-lapsed session leaves no cached token behind. If that cache write fails — the connector-store Redis is unreachable — the section reports the error and the import answers ok: false, although the restored rows are already committed; once Redis is back, re-import the section with "mode": "overwrite" to rewrite the cache.
The storage section carries every object of the deployment’s content store, whatever its bytes are, except the objects under a prefix marked not backed up (inbound media is one; see inbound media). An object whose bytes are UTF-8 text travels as content_text, any other object as content_base64, and every entry carries the content_type the store reports. A restore writes each entry back byte for byte; a storage provider that records content types keeps the entry’s content_type, and one that infers the type from the path infers it again on the restored object. A storage provider that holds text only cannot keep a binary object, so it rejects such an entry, naming its path in the section’s errors; the rest of the section still restores. A restore refuses an entry under a prefix marked not backed up the same way, naming its path and the prefix in errors. An archive whose store section is named templates restores nothing from it: selecting templates reports unknown section: 'templates'.
A restore that writes the storage section clears the compiled-template cache fleet-wide over the same bus and cycles the prefork pool once, so no worker — and no forked pool child — keeps serving a template compiled from the pre-restore content.
Restore order
An import replays the selected sections in their registration order, never in the order you name them:manifest, env, storage, access_control, sub_mcp, states,
webhooks, conversations, conversation_target_config, schedules,
connector_categories, connector_connections, connector_oauth_clients,
versioned_documents, tool_meta, then the sections plugins register. A section that decides
its records against state another section restores replays after it: storage replays before
every section whose records name a stored template by id, which the restore checks, and
states replays before every section that restores a state binding.
Inside access_control, the principals restore first, then the route mappings, then the API
keys. A principal the restore creates gets its archived disabled flag with the principal
itself, before any route or key, so a failure later in the section never leaves a principal the
archive holds disabled enabled. A principal the server already holds is skipped under both
modes and keeps its live state, whatever its archived flag. A key is re-minted onto its owner,
enabled or disabled, and follows the owner’s live state: while the owner is disabled the key
is denied at every door, and while the owner is enabled it authenticates — so a key restored
onto an existing, enabled owner authenticates at once, even where the archive holds that owner
disabled.
Restoring stored definitions
Hooks, schedules, presets, per-target conversation configs, and conversation routes restore through the same write service their HTTP door runs, so a restored definition passes the same save rules a new one does:- A state binding is validated and its named templates are attached — see saving a binding.
- A route’s target bind check runs, its jq programs compile, and a
channelrow’sour_identityis stored canonical and must be unclaimed. - A preset is judged by its document’s rules only: its name, its
output_schemaandinput_schemabodies, and its binding.
schedules export holds the backend’s schedules as ScheduleRecord rows (name, the tool
the schedule fires, arguments, cadence, enabled), the same shape on every scheduling backend,
and the backend’s import answers a ScheduleImportReport; a report of any other shape fails
the section loudly. An archive row without a tool or with a backend-specific schedule
shape is refused per record.
A record that fails a rule is refused per record — named in errors with its reason and
counted in skipped — and the rest of the section still restores. A store or transport
failure is never a per-record refusal: it stops the section there, and the section’s report
carries the failure’s message with zero counts and makes ok false. A record the section
committed before the failure stays written although the report counts none, so once the store
is reachable import the section again — under skip those records count as skipped_existing.
Two kinds of check stay at the HTTP door and do not run on a restore, because their verdict
depends on what the running server has registered rather than on the backup itself:
- Target existence. A restored conversation config or route may name a target the backup
restores later (a preset in
versioned_documents) or a plugin registers later. The door still refuses a target that does not exist. - A preset’s live-registry rules — its base tool, agent, extension, and write-validator
checks. A restored preset whose base cannot bind is stored as a row; when the server next
loads its presets (a reload or a restart) it is quarantined and listed as
conflicted, with the reason logged at error level.
versioned_documents restore holds one connection of the skeleton database while its preset
checks read the template and states stores, so it needs two connections of that
database’s pool at once (the default pool holds ten).
Recovering from a bad migration
The backup document above captures platform state — policies, presets, and the other registered sections. It does not capture the database schema, which is a separate layer: versioned as migration chains and recorded, one row per applied file, in thetai_schema_history table. That
table is the source of truth for where each component’s schema stands; read it
with tai db status.
Migration chains roll forward only — there are no down-migrations, so a
migration is never “un-applied”. Recovering from a migration that went wrong is a
restore-then-re-migrate, not a rollback:
1
Restore the datastore snapshot
Restore Postgres to a snapshot taken before the bad migration (a managed
provider’s point-in-time restore, or a
pg_dump you captured). This brings
the schema, the data, and tai_schema_history back to that point together —
which is why the upgrade flow tells you to snapshot
before a schema-carrying upgrade.2
Re-migrate forward
Point the image you mean to run at the restored database and run
tai db migrate. The runner reads tai_schema_history, sees which files the
snapshot already carries, and applies only what is still pending — bringing the
chain forward to the running image. Preview it first with tai db migrate --plan.Because history and data restore as one snapshot, keep the datastore backup and
the
tai backup export document on the same cadence — a restored database and a
stale platform-state document drift apart otherwise.See also
- Ops notes —
tai db statusand the migration model. - Access control — one of the sections you back up.
- Create and version a preset — presets are a backed-up section.
- CLI reference — the full
tai backupsurface.

