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 ARIsFilenames 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:
| Flag | Default | Effect on changed rules | Effect on rules removed from Jira |
|---|---|---|---|
--incremental | true | Re-download only changed rules | Tombstone in state (status: deleted); local JSON file is kept |
--sync | false | Re-download only changed rules | Tombstone 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=falseThe 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
| Scenario | Recommended 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.jsonThe 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| Reason | What it means |
|---|---|
new | The 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 missing | State 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=falseWhen 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.jsonat the output root: a tenant-wideaccountId → identitymap 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
| Flag | Short | Default | Description |
|---|---|---|---|
--output-dir | required | Root directory for the JSON tree — must be provided explicitly | |
--state-file | <output-dir>/.ajat_state.json | Override only if you need a non-co-located state | |
--page-size | 100 | Jira summary page size (1..100) | |
--workers | 4 | Concurrent per-rule downloads (1..64); see Rate limits | |
--redact-sensitive-fields | false | Ask Jira to mask secrets in the exported JSON body | |
--resolve-users | true | Resolve 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 | |
--incremental | true | Track state, skip unchanged rules, keep local files for rules removed from Jira. Mutually exclusive with --sync | |
--sync | false | Track state, skip unchanged rules, delete local files for rules removed from Jira. Mutually exclusive with --incremental | |
--debug-changes | -v | false | Print 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-Afteris 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
--workersvalues safe. X-RateLimit-NearLimit: truetriggers 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
| Code | Meaning |
|---|---|
| 0 | Every discovered rule was downloaded or skipped successfully |
| 1 | Bootstrap / configuration / authentication failure |
| 2 | At least one rule failed to download (see the state file for last_error) |
| 130 | Interrupted (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.
Related
- Installation and First-Time Setup — from install to your first export.
- HTML Reports — turn the export tree into inventory, collision, consistency, risk, and workflow reports.
- Compare Snapshots — diff two export directories for drift review and CI gates.
- Bulk Rule Operations — run label, name, scope, and state selectors against an export directory.
- Import and Restore — push an export tree back into Jira, safely and plan-first.
- Configuration — precedence, environment variables, and proxy settings.