Skip to Content
Atlassian Jiraajatv1.0.xExport Rules

Export the Estate

ajat export writes every Automation rule in your Jira Cloud tenant to a scope-aware directory tree of JSON files, plus a state file that drives incremental re-runs. The goal is not only disaster recovery: the export becomes the raw inventory for understanding how Jira Automation is distributed across global, project, multi-project, and other scopes.

This is the main operational workflow for ajat and the foundation every other command builds on. Reports, diff, search, and bulk rule operations all read the local tree this command produces.

ajat supports Atlassian Jira Cloud Automation only. It does not support Jira Server or Jira Data Center.

If you are setting ajat up for the first time, start with Installation and First-Time Setup, which walks from install to your first successful export and inventory report.

Large Jira sites often accumulate automation over years. Rules are created by different teams, copied between projects, and widened to multi-project or global scope as business processes grow. ajat export captures that full estate in one repeatable run so teams can review the automation mesh from disk instead of jumping between project administration screens.

First-time export

ajat export --output-dir ./jira-automations-backup

--output-dir is required and must be provided explicitly. The command first paginates the Jira Automation rule summary endpoint to build the full work list, then downloads each rule concurrently.

🔍 Fetching rules... 📥 Fetched 100 rule(s)... 📥 Fetched 200 rule(s)... ... 📥 Fetched 951 rule(s) ✅ Discovered 951 rule(s) Exporting rules ... 23.66% [225 in 2.1s; ~ETA: 7s]

The discovery banner, the per-rule progress bar, and the per-rule status lines all go to stderr, so piping stdout (for example into tee, jq, or a file) stays clean.

export is a read-only command. It never mutates Jira — it only reads rule definitions and, by default, resolves the account IDs those rules reference.

On-disk layout

Rules are written into a scope-aware tree so you can answer basic governance questions before opening a report: which rules are global, which projects own local rules, which automations span more than one project, and which definitions need closer inspection.

<output-dir>/ ├── .ajat_state.json # incremental state ├── .ajat_external_directory.json # account directory (omitted with --resolve-users=false) ├── global/ │ └── <uuid>__<rule-name>.json # rules with no scope ARI ├── projects/ │ └── <KEY>__<Project-Name>/ │ ├── <uuid>__<rule-name>.json # single-project rules │ └── <uuid>__<rule-name>.external-info.json # resolved identities (omitted with --resolve-users=false) ├── multi-project/ │ └── <KEY1>__<Name1>__<KEY2>__<Name2>__and-N-more/ │ └── <uuid>__<rule-name>.json # multi-project rules └── other-scope/ └── <uuid>__<rule-name>.json # rules with non-project scope ARIs

Filenames are normalized (NFKD to ASCII, special characters to -) so the tree is safe on Linux and macOS.

For multi-project rules, the project labels in the directory name are sorted alphabetically. The Atlassian API does not guarantee a stable order for ruleScopeARIs between responses, so the sort makes the path deterministic across runs — which is also what makes incremental skip work.

Incremental and sync modes

Re-running with the same --output-dir only re-downloads rules whose updated timestamp has changed in the upstream listing. Rules already on disk with the same updated are skipped. Two mutually exclusive flags control how rules that have disappeared from the upstream listing are handled:

FlagDefaultEffect on changed rulesEffect on rules removed from Jira
--incrementaltrueRe-download only changed rulesTombstone in state (status: deleted); local JSON file is kept
--syncfalseRe-download only changed rulesTombstone in state and delete the local JSON file
# Default: incremental, keep deleted rules on disk for auditing ajat export --output-dir ./jira-automations-backup # Sync mode: mirror upstream exactly, including deletions ajat export --output-dir ./jira-automations-backup --sync # Disable state entirely: re-download every rule, no orphan reconciliation ajat export --output-dir ./jira-automations-backup --sync=false --incremental=false

The default --incremental keeps tombstoned files on disk because deleted automation can be part of the audit story — a workflow changing, a project being decommissioned, a cleanup that needs review. Switch to --sync when you want the on-disk tree to mirror Jira exactly, for example when the export feeds a downstream pipeline that should never see stale rules.

This mirrors the --sync / --incremental model used by the cousin acs2md tool’s space convert: --sync deletes local files for upstream deletions, --incremental keeps them.

Choosing a mode

ScenarioRecommended mode
Compliance or audit archive that must retain removed rules--incremental
Live mirror feeding a downstream pipeline--sync
Git-backed backup that should track Jira exactly--sync
Conservative continuity copy that never loses history--incremental

The state file

The state file (.ajat_state.json) records what was downloaded, when, and with what result. It is written as a single line of JSON, which keeps it friendly to git diff and grep tooling that expects one record per line.

When --sync or --incremental is active, the final summary block prints the resolved state file path so it is easy to copy into a debugger or grep command:

Export complete discovered : 951 downloaded : 12 skipped : 939 failed : 0 deleted : 0 elapsed : 8.3s output : /Users/me/jira-automations-backup state file : /Users/me/jira-automations-backup/.ajat_state.json external : /Users/me/jira-automations-backup/.ajat_external_directory.json

The state file line is omitted when state tracking is disabled (--sync=false --incremental=false). The external line is shown only when --resolve-users was requested.

By default the state file lives at <output-dir>/.ajat_state.json. Override its location with --state-file only if you need a non-co-located state.

Resumability

.ajat_state.json is rewritten atomically (temp file, then rename(2)) after every single rule the exporter processes — successful download, failure, or scope reclassification. There is no end-of-run flush you can lose: if you press Ctrl-C, or the process is killed, or the laptop loses power, every rule that finished is durably on disk with status: "downloaded", and the next run skips it.

Rules that ended in status: "failed" are retried on the next run. The retry preserves the prior last_error field in state until the rule succeeds, so the state file is also the audit log for what is currently broken.

To force a full re-download while keeping the JSON files, delete just .ajat_state.json and re-run. To start completely fresh, delete the whole output directory. You can also ignore state for a single run with --sync=false --incremental=false.

Why is a rule re-downloading?

If you expected an incremental re-run to skip almost everything and the download counter is climbing fast, pass -v (--debug-changes). Every rule that fails the skip-check is printed with the precise reason:

↻ redownloading "Triage Bugs" — updated 1.6739e+09 → 1.6741e+09 ↻ redownloading "Onboarding" — path projects/X/... → projects/Y/... ↻ redownloading "Audit Log" — local file missing ↻ redownloading "Send Slack" — prior status: failed
ReasonWhat it means
newThe state file has no entry for this UUID (counted as a download).
prior status: <X>The previous run did not finish as downloaded (e.g. failed), so the rule is retried.
updated <a> → <b>Jira reports a fresher updated timestamp than the state remembers.
path <old> → <new>The rule’s scope-derived directory changed (project renamed, scope ARI added or removed).
local file missingState says the rule was downloaded but the JSON file is no longer on disk.

Leave -v off for routine runs — the running tally and final summary are enough — and turn it on when investigating churn.

Resolving user identities

Every exported rule is full of opaque Jira account IDs — the rule’s author, the identity it runs as (actor), its collaborators, and any account IDs buried inside component configuration. On their own these read like 712020:9c32… and tell you nothing.

ajat turns them into display identities automatically. --resolve-users is on by default, because the resolved names are what make the backup and the reports actually readable:

# Resolution runs as part of a normal export — no extra flag needed. ajat export --output-dir ./jira-automations-backup # Opt out when you only want the raw rule JSON, or the token lacks the # read:jira-user scope: ajat export --output-dir ./jira-automations-backup --resolve-users=false

When enabled, a post-download phase collects every distinct account ID referenced across the run, resolves them in one batched, concurrent, cached pass (a tenant with hundreds of rules but a handful of distinct accounts costs roughly one round-trip), and writes the resolved identities next to each rule. The exported rule JSON is never modified, so the backup stays a byte-for-byte copy of what Jira returned.

Two kinds of file are produced:

  • Per-rule sidecar <uuid>__<rule-name>.external-info.json, written beside the rule it describes. It records the rule’s author, actor, collaborators, and component references, each with the resolved display name, account type (atlassian / app / customer), active flag, and — where it appears — the JSON path inside the rule. Sidecars are pretty-printed because they are meant to be read by hand next to each rule.
  • Directory index .ajat_external_directory.json at the output root: a tenant-wide accountId → identity map plus a per-account count of how many rules reference it, so reports and tooling can answer “who owns the most automation?” without opening every sidecar. Like .ajat_state.json, it is written as a single line of JSON.

Account IDs the API cannot resolve — deactivated, deleted, or app accounts the token cannot see — are recorded as unresolved placeholders ("resolved": false) rather than failing the run. Enrichment is additive, idempotent, and re-runnable: each file carries a schemaVersion and resolvedAt timestamp, so re-running --resolve-users refreshes changed display names without re-downloading rules. Any enrichment failure is logged but never fails the backup — the rule JSON is already safely on disk by then.

ajat report inventory automatically picks up these sidecars: resolved actor and author names, a Top authors panel, and per-rule Author / Collaborators rows. Backups exported without --resolve-users render exactly as before.

Sidecars store display names and — when Jira exposes them — email addresses by design. Treat an enriched backup as containing personal data and share it accordingly. Email addresses are frequently hidden by Atlassian’s privacy settings and will simply be absent when that is the case.

Common flags

FlagShortDefaultDescription
--output-dirrequiredRoot directory for the JSON tree — must be provided explicitly
--state-file<output-dir>/.ajat_state.jsonOverride only if you need a non-co-located state
--page-size100Jira summary page size (1..100)
--workers4Concurrent per-rule downloads (1..64); see Rate limits
--redact-sensitive-fieldsfalseAsk Jira to mask secrets in the exported JSON body
--resolve-userstrueResolve referenced account IDs to display identities and write *.external-info.json sidecars plus a directory index. Pass --resolve-users=false to skip the extra read:jira-user API calls
--incrementaltrueTrack state, skip unchanged rules, keep local files for rules removed from Jira. Mutually exclusive with --sync
--syncfalseTrack state, skip unchanged rules, delete local files for rules removed from Jira. Mutually exclusive with --incremental
--debug-changes-vfalsePrint a one-line reason for every rule re-downloaded due to a state mismatch

Masking sensitive fields

--redact-sensitive-fields asks Jira to mask secrets (such as credentials embedded in webhook or connect actions) in the returned rule JSON. Enable it for exports that will be shared beyond the platform team or committed to a repository.

Progress reporting

The progress label updates in place with the running tally:

Exporting · ↓12 skip 939 fail 0 ... 99%
  • ↓N — rules fetched from the Automation API this run.
  • skip N — rules whose state showed them unchanged and whose local JSON file is still on disk.
  • fail N — rules where the API call or the local write failed.

When --resolve-users is active the run has two phases — downloading rules, then resolving the accounts they reference — and the display shows three bars together: a combined Overall aggregate, the Automations download tracker, and the External info enrichment tracker. All three are created up front so the display never collapses between phases.

Everything renders to stderr. Suppress the progress bar and the per-page discovery lines entirely with the persistent --no-progress flag — useful for CI/CD logs, scripting, and terminal recordings. Each per-rule transition is also logged as a structured event when --log-file is set, so disabling the bar never loses operator-visible information.

Rate limits and concurrency

The Atlassian Automation API throttles per tenant. ajat export is designed to back off cleanly instead of crashing the run:

  • Retry-After is honored. A 429 pauses the offending request for the advised wall-clock duration (plus jitter, capped at 120 seconds per wait), then retries, with a 5-minute cumulative retry budget per request and a 60-attempt safety belt.
  • A shared back-off gate parks every worker on the same wall. When one worker is rate-limited, parallel workers wait at the same deadline instead of hammering the API in lock-step. This is what makes high --workers values safe.
  • X-RateLimit-NearLimit: true triggers a preemptive cushion — the pool slows for roughly 750 ms to avoid tipping into a 429 wave.
  • 5xx and network errors retry independently with jittered exponential backoff, up to four attempts per request.

The default --workers 4 matches Atlassian’s recommended low-concurrency profile. Healthy tenants can usually raise it to --workers 16 without seeing 429s; very large or constrained tenants should consider --workers 2. The exporter is fully resumable — kill the run, lower the worker count, and re-launch with the same --output-dir to pick up where you left off.

Exit codes

CodeMeaning
0Every discovered rule was downloaded or skipped successfully
1Bootstrap / configuration / authentication failure
2At least one rule failed to download (see the state file for last_error)
130Interrupted (SIGINT / SIGTERM)

Scheduled exports

ajat export is safe to run from cron, GitHub Actions, or any other scheduler. Because it is incremental and resumable by default, a recurring job keeps a current inventory cheaply.

# .github/workflows/jira-automations-backup.yaml on: schedule: [{ cron: "0 4 * * *" }] # 04:00 UTC daily workflow_dispatch: jobs: export: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: | # Download URL comes from your Climakers order email; store it as a CI secret. curl -sL "${{ secrets.AJAT_DOWNLOAD_URL }}" -o ajat.zip unzip ajat.zip && chmod +x ajat && sudo mv ajat /usr/local/bin/ - run: ajat export --output-dir ./jira-automations-backup --workers 16 --redact-sensitive-fields --no-progress env: AJAT_JIRA_DOMAIN: ${{ secrets.AJAT_JIRA_DOMAIN }} AJAT_JIRA_USERNAME: ${{ secrets.AJAT_JIRA_USERNAME }} AJAT_JIRA_API_TOKEN: ${{ secrets.AJAT_JIRA_API_TOKEN }} - uses: actions/upload-artifact@v6 with: { name: jira-automations-backup, path: ./jira-automations-backup/ }

For scheduled CI, use the CI/CD Automation license edition so re-validation does not hit the licensing API on every run. See Licensing for editions.

Pair the exported artifact with a downstream job running HTML Reports or Compare Snapshots when you need a fresh view for platform reviews, change advisory meetings, or compliance evidence.

What is not exported

  • Rule execution history. Jira retains roughly 90 days of audit log; it is not part of the rule definition.
  • Workflow, project, or board configuration. ajat is scoped to Automation rules only.
  • Custom-field schemas. The full rule JSON references custom-field IDs but does not carry their schemas.

ajat v1 is export-only for backup, audit, and analysis. Restoring exported rules back into a tenant is handled by Import and Restore.

Last updated on