Skip to Content
Atlassian Jiraajatv1.0.xImport & Promote

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:

  1. 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.
  2. Default: interactive confirmation. When stdin is a TTY, import waits for y / yes before applying the plan.
  3. --dry-run prints the plan and exits without prompting — safe to wire into scheduled jobs that only need to detect drift.
  4. --yes skips the prompt for CI use, and is required when stdin is not a TTY.
  5. State-based skip. A sibling .ajat_import_state.json records 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:

OperationRequestWhen
CREATEPOST /rest/v1/ruleThe rule UUID is not present in the target tenant.
UPDATEPUT /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 --yes

The 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
CountMeaning
createRules whose UUID is missing from the target — they will be POSTed.
updateRules whose UUID already exists — they will be PUT (overwritten).
skipRules unchanged since the last successful push and still present in the target.
pruneDISABLED target rules absent from the source that --prune will delete.
errorsFiles 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>.json

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

StrategyBehaviour on CREATEUse 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.
newThe 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:

FieldWhy it needs rewritingFlag
Rule UUIDCross-tenant UUID collisions are not desired.--uuid-strategy=new
ruleScopeARIsEach 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 entries

Notes:

  • --scope-map applies 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 ruleScopeARIs is a silent no-op.
  • --scope-map is independent of --uuid-strategy. You can use it with preserve when 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-restore

A 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-promotion

State 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

FlagTypeDefaultDescription
--input-dirstringDirectory containing exported rule JSON files (required).
--dry-runboolfalsePrint the plan and exit without mutating the target tenant.
--yesboolfalseSkip the interactive confirmation prompt (required when stdin is not a TTY).
--createbooltrueCreate rules whose UUID is missing from the target tenant.
--updatebooltrueUpdate rules whose UUID is present in the target tenant.
--uuid-strategystringpreserveHow to handle the payload’s UUID on CREATE: preserve or new.
--scope-mapstringArray[]Rewrite a scope ARI before push: --scope-map OLD=NEW (repeatable).
--pruneboolfalseDelete DISABLED target rules absent from the source export (ENABLED orphans are not touched).
--state-filestringState-file path (defaults to <input-dir>/.ajat_import_state.json).
--workersint4Concurrent rule writes (1..32).

Exit codes

CodeMeaning
0Every planned operation succeeded or was skipped.
1Bootstrap or configuration failure (bad credentials, unreachable tenant, missing flags).
2At least one rule failed to import; the state file records per-rule details.
130Interrupted by SIGINT or SIGTERM.

Before a high-stakes import

  • Run ajat doctor to confirm credentials and Automation API access end-to-end.
  • Take a fresh ajat export of the target so you have an undo point and can ajat diff before and after.
  • Always run --dry-run and read the plan before applying.
Last updated on