Import and Restore
ajat import is the inverse of ajat export. It reads a local on-disk rule tree and writes those rules back into a Jira Cloud tenant through the Automation REST API. Export and import together close the round-trip and give you three capabilities Jira does not:
- Disaster recovery — a wrecked rule, an accidental deletion, or a botched manual edit can be restored from the most recent export without rebuilding by hand.
- Sandbox-to-production promotion — develop and test rules in a sandbox tenant, then import them into production.
- Tenant migrations — copy an automation estate from one Jira Cloud site to another with fresh UUIDs.
ajat import is the only ajat command that writes to a live Jira tenant. It
is deliberately conservative: it always computes and prints a plan first, and
no API call mutates anything until you confirm interactively or pass --yes.
Always run --dry-run first, and import against a snapshot you trust.
ajat operates on Atlassian Jira Cloud Automation only. Import writes through the Jira Cloud Automation REST API and does not support Jira Server or Jira Data Center.
The safety model
Because import mutates a live tenant, the contract is intentionally strict:
- Plan first, mutate second. Every run starts by listing target-tenant rule summaries, scanning the local tree, and printing a plan (
create / update / skip / prune / errors). No API call mutates state until you confirm. - Default: interactive confirmation. When stdin is a TTY, import waits for
y/yesbefore applying the plan. --dry-runprints the plan and exits without prompting — safe to wire into scheduled jobs that only need to detect drift.--yesskips the prompt for CI use, and is required when stdin is not a TTY.- State-based skip. A sibling
.ajat_import_state.jsonrecords the sha256 of each pushed payload. Later runs skip rules whose local file is byte-for-byte identical to the last push and whose UUID is still present in the target tenant.
The two operations import performs against the live tenant are:
| Operation | Request | When |
|---|---|---|
| CREATE | POST /rest/v1/rule | The rule UUID is not present in the target tenant. |
| UPDATE | PUT /rest/v1/rule/{uuid} | The rule UUID already exists in the target tenant. |
Quickstart
# 1. Preview what would change — no writes.
ajat import --input-dir ./jira-automations-backup --dry-run
# 2. Apply the full plan with an interactive prompt.
ajat import --input-dir ./jira-automations-backup
# 3. Same, but non-interactive for CI.
ajat import --input-dir ./jira-automations-backup --yesThe import plan
Every run prints a plan before anything is written. It is the single most important artifact of a safe import: read it, confirm the counts match your intent, and only then apply.
Import plan
input dir : ./jira-automations-backup
create : 3
update : 12
skip : 0
prune : 5
errors : 0| Count | Meaning |
|---|---|
create | Rules whose UUID is missing from the target — they will be POSTed. |
update | Rules whose UUID already exists — they will be PUT (overwritten). |
skip | Rules unchanged since the last successful push and still present in the target. |
prune | DISABLED target rules absent from the source that --prune will delete. |
errors | Files that could not be parsed or lacked a recognisable UUID prefix. |
Files whose names do not start with a recognisable UUID prefix, files that fail to parse as JSON, and hidden directories are listed in the plan’s errors section and skipped rather than aborting the run.
What gets imported
ajat import consumes the on-disk layout produced by ajat export:
<input-dir>/
├── .ajat_import_state.json # written by import (skip-if-unchanged)
├── global/<uuid>__<name>.json
├── projects/<KEY>__<Project>/<uuid>__<name>.json
├── multi-project/<scope-dir>/<uuid>__<name>.json
└── other-scope/<uuid>__<name>.jsonThe UUID parsed from each filename is the unit of identity. Each file’s contents — the full JSON returned by the export — are sent back verbatim, except for the optional UUID-stripping step described below.
UUID strategy
The Automation REST API lets callers supply a UUID when creating a rule. That gives import two modes, controlled by --uuid-strategy:
| Strategy | Behaviour on CREATE | Use case |
|---|---|---|
preserve (default) | The payload is sent verbatim; the server honors the supplied UUID when possible. | Same-tenant restore. External systems (audit logs, links, dashboards) referencing rule UUIDs keep working. |
new | The top-level "uuid" field is removed before POST; the server allocates a fresh UUID. | Cross-tenant promotion — sandbox to production, or copying rules to a different Atlassian site. |
Updates (PUT /rule/{uuid}) always use the UUID parsed from the filename; --uuid-strategy only affects creates.
Rewriting project scope with --scope-map
When you move rules between tenants, two tenant-specific fields need rewriting on the way in:
| Field | Why it needs rewriting | Flag |
|---|---|---|
| Rule UUID | Cross-tenant UUID collisions are not desired. | --uuid-strategy=new |
ruleScopeARIs | Each ARI embeds the source cloudId and project ID, both of which differ in the target tenant. | --scope-map OLD=NEW |
--scope-map is repeatable. Each entry rewrites the matching ARI in every rule’s ruleScopeARIs array before the rule is pushed; ARIs not listed in the mapping are left alone. The plan preview reports the active entry count so you can confirm the right number of mappings loaded before applying:
Import plan
input dir : ./sandbox-export
create : 42
update : 0
skip : 0
errors : 0
scope-map : 2 entriesNotes:
--scope-mapapplies to both create and update — the intent of the flag is “the source ARIs are stale, rewrite them everywhere”.- An entry whose source ARI matches no rule’s
ruleScopeARIsis a silent no-op. --scope-mapis independent of--uuid-strategy. You can use it withpreservewhen the same tenant’s project ARIs changed, for example after a project key rename.
Selective imports
Two flags narrow the run without touching the local files:
--create=false— never POST new rules. Useful when you want to bring rules that already exist in the target up to date, but not reintroduce ones that were removed from the target on purpose.--update=false— never overwrite rules that already exist. Useful for a “first-time seed” of a fresh tenant.
Passing both --create=false --update=false is an error: there would be no work to do.
Pruning orphan rules with --prune
--prune opts the import into “make the target match the source” mode. After the create and update phase, every DISABLED rule in the target tenant whose UUID is not present in the source export is deleted.
# Preview first.
ajat import --input-dir ./jira-automations-backup --prune --dry-run
# Apply.
ajat import --input-dir ./jira-automations-backup --prune --yes--prune deletes only DISABLED target rules absent from the
source. ENABLED orphans are intentionally left in place — auto-disabling live
production rules as a side effect of an import would be a surprising blast
radius. Enabled orphans must be cleaned up explicitly.
To remove an enabled orphan, disable it first, then prune on the next run:
# 1. Find enabled orphans in the target.
ajat export --output-dir ./target-snapshot
ajat diff ./jira-automations-backup ./target-snapshot
# 2. Disable the orphan.
ajat rule disable --input-dir ./target-snapshot --uuid <UUID> --yes
# 3. Now the next prune will remove it.
ajat import --input-dir ./jira-automations-backup --prune --yes--prune issues one extra paginated summary call (filtered to DISABLED rules) to identify candidates, so the cost is bounded regardless of estate size.
Recipe: same-tenant disaster recovery
Restore the last known-good snapshot into the same tenant, keeping original UUIDs so external references stay valid, then verify with an export and a diff.
# 1. Preview the restore.
ajat import --input-dir ./last-known-good --dry-run
# 2. Apply it (default --uuid-strategy=preserve).
ajat import --input-dir ./last-known-good --yes
# 3. Verify: export the current state and confirm it matches the snapshot.
ajat export --output-dir ./post-restore
ajat diff ./last-known-good ./post-restoreA clean diff after the restore is your evidence that the tenant matches the snapshot you imported.
Recipe: sandbox-to-production promotion
Promote a validated sandbox export into production. Use --uuid-strategy=new so the server allocates fresh UUIDs, and --scope-map to rewrite each sandbox project ARI to its production equivalent.
# 1. Dry-run to confirm the plan and the scope-map entry count.
ajat import --input-dir ./sandbox-export --uuid-strategy=new \
--scope-map ari:cloud:jira:sandbox-cloud-id:project/PROJ-A=ari:cloud:jira:prod-cloud-id:project/PROJ-A \
--scope-map ari:cloud:jira:sandbox-cloud-id:project/PROJ-B=ari:cloud:jira:prod-cloud-id:project/PROJ-B \
--dry-run
# 2. Apply the promotion.
ajat import --input-dir ./sandbox-export --uuid-strategy=new \
--scope-map ari:cloud:jira:sandbox-cloud-id:project/PROJ-A=ari:cloud:jira:prod-cloud-id:project/PROJ-A \
--scope-map ari:cloud:jira:sandbox-cloud-id:project/PROJ-B=ari:cloud:jira:prod-cloud-id:project/PROJ-B \
--yes
# 3. Keep evidence: export production after the promotion.
ajat export --output-dir ./post-promotionState file
ajat import writes <input-dir>/.ajat_import_state.json (or the path given to --state-file) after every successful create, update, or recorded failure. Each entry contains:
- the local UUID parsed from the filename,
- the UUID echoed by the server — relevant when
--uuid-strategy=new, - the sha256 of the payload that was pushed,
- the status (
created,updated,failed), - the last error message, when applicable.
Delete this file to force a full re-push on the next run.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--input-dir | string | — | Directory containing exported rule JSON files (required). |
--dry-run | bool | false | Print the plan and exit without mutating the target tenant. |
--yes | bool | false | Skip the interactive confirmation prompt (required when stdin is not a TTY). |
--create | bool | true | Create rules whose UUID is missing from the target tenant. |
--update | bool | true | Update rules whose UUID is present in the target tenant. |
--uuid-strategy | string | preserve | How to handle the payload’s UUID on CREATE: preserve or new. |
--scope-map | stringArray | [] | Rewrite a scope ARI before push: --scope-map OLD=NEW (repeatable). |
--prune | bool | false | Delete DISABLED target rules absent from the source export (ENABLED orphans are not touched). |
--state-file | string | — | State-file path (defaults to <input-dir>/.ajat_import_state.json). |
--workers | int | 4 | Concurrent rule writes (1..32). |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Every planned operation succeeded or was skipped. |
| 1 | Bootstrap or configuration failure (bad credentials, unreachable tenant, missing flags). |
| 2 | At least one rule failed to import; the state file records per-rule details. |
| 130 | Interrupted by SIGINT or SIGTERM. |
Before a high-stakes import
- Run
ajat doctorto confirm credentials and Automation API access end-to-end. - Take a fresh
ajat exportof the target so you have an undo point and canajat diffbefore and after. - Always run
--dry-runand read the plan before applying.
What to read next
- Export the estate — produces the on-disk tree this command consumes.
- Compare Snapshots — verify a restore or promotion against a snapshot.
- Bulk Rule Operations — disable enabled orphans before pruning.
- Workflows & Recipes — end-to-end recovery and promotion playbooks.