Skip to main content
Back up and restore a server’s stored state. The runtime exports its stored state as a backup document and imports it back. Each backup is composed of named sections — access-control policies, presets, and whatever else the host and its plugins register. You choose which sections to export and which to import; nothing is all-or-nothing.

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.
A section that fails to export is left out of the document and named with its message under the document’s 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.
Importing a section applies its payload over the live state. Export the current state first so you have a document to roll back to, and import only the sections you mean to restore.

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.
The natural key is per section: a hook by its 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:
  • manifest restores as a whole document — a single manifest always replaces the live one, through the mutation pipeline below.
  • env merges: 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 in new_api_keys to 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 an access_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 own accounts 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 in new_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/setup answers 409 even when every key is orphaned. To recover the owner’s access, re-import an access_control export — it re-mints every orphaned key, including the owner’s. Without an export, run tai 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 through tai keys afterward.

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 channel row’s our_identity is stored canonical and must be unclaimed.
  • A preset is judged by its document’s rules only: its name, its output_schema and input_schema bodies, and its binding.
A 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.
A 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 the tai_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