atlassian-cli is an independent, community open-source project, not affiliated with, endorsed by, or sponsored by Atlassian, and is not Atlassian's official CLI (acli). Product names are used only to identify compatibility.

How to sync Jira to Confluence in three steps

To sync Jira to Confluence, you pull the issues you care about from Jira as structured data, render them into a Confluence table, and write that table into a page. With atlassian-cli the whole loop is three commands: jira issue search to fetch, a bit of jq to render, and confluence page update to publish. Drop those steps into a shell script, schedule it with cron, and you get a release-notes or status page that refreshes itself instead of going stale the moment someone stops updating it by hand.

This is a common request on developer forums: teams want a single Confluence page that always shows the current state of a release, but nobody wants to babysit a table, copying issue keys and statuses out of Jira every morning. A copied table is out of date within hours. A generated one is only ever as old as its last scheduled run.

How this differs from Atlassian's own CLI: atlassian-cli is an independent, community open-source project. It is not Atlassian's official CLI (acli) and is not affiliated with, endorsed by, or sponsored by Atlassian. Use the official acli if you want first-party vendor support; use atlassian-cli if you want a single free Rust binary that spans Jira, Confluence, Bitbucket, and Jira Service Management with the JSON output and scripting flags this workflow relies on.

The rest of this post builds the pattern up one command at a time, then wires the pieces into a script you can run on a schedule. Every command is real and copy-pasteable; check the flags against the command reference if you want the full option list.

Step 1: Pull the data from Jira

Start by deciding which issues belong on the page. A release page usually maps to one fix version or one JQL filter. Fetch those issues as JSON so a script can read the fields cleanly:

# Pull every issue in the 4.2 release as JSON
atlassian-cli jira issue search \
  --profile prod \
  --jql "project = REL AND fixVersion = 4.2 ORDER BY status" \
  --format json > issues.json

The --format json flag returns the raw issue array, which is what you want for scripting. Each element carries the fields you will surface in the table: .key, .fields.summary, .fields.status.name, and .fields.assignee.displayName. If a spreadsheet is your target instead of a page, --format csv --output issues.csv writes a flat file directly.

Because the query is just JQL, you control exactly what lands on the page. Swap fixVersion = 4.2 for sprint in openSprints() for a live sprint board, or labels = incident AND status != Done for an active-incident table. The sync logic never changes; only the query does.

Keep the result set intentional. A release page listing a few dozen issues renders cleanly, while dumping several hundred rows into a single table makes for a page nobody scrolls. Use the --limit flag to cap how many issues the search returns, and lean on ORDER BY in the JQL so the most relevant rows land at the top of the table. If you need more than one view, run the pull-render step twice with different queries and concatenate the two tables into the same file before pushing.

Step 2: Render a storage-format table

Confluence stores page bodies in storage format, which is XHTML. That is good news: a table is just <table>, <tr>, <th>, and <td> tags, and any script that can print HTML can build one. Use jq to turn the Jira JSON into table rows:

# Render issues.json into a Confluence storage-format table
{
  echo '<h2>Release 4.2 status</h2>'
  echo "<p>Last synced: $(date -u '+%Y-%m-%d %H:%M UTC')</p>"
  echo '<table><tbody>'
  echo '<tr><th>Key</th><th>Summary</th><th>Status</th><th>Assignee</th></tr>'
  jq -r '.[] | "<tr><td>\(.key)</td><td>\(.fields.summary)</td><td>\(.fields.status.name)</td><td>\(.fields.assignee.displayName // "Unassigned")</td></tr>"' issues.json
  echo '</tbody></table>'
} > release-table.html

The // "Unassigned" fallback in the jq filter keeps the table clean when an issue has no assignee. The date -u line stamps a synced-at timestamp so readers know how fresh the data is. You now have a self-contained release-table.html file holding valid Confluence storage format.

Escaping tip: issue summaries can contain &, <, or >, which are special in XHTML. For production tables, pipe summary text through a small escape step (for example gsub in jq) so a stray angle bracket does not break the page render.

Step 3: Push it into the page

Now write the rendered table into a real page. The --body flag on confluence page update takes a path to a file containing storage format, not an inline string, which is exactly why the render step wrote to release-table.html:

# Replace the page body with the freshly rendered table
atlassian-cli confluence page update 1310724 \
  --profile prod \
  --body release-table.html

Updating an existing page rather than creating a new one is the key to this pattern. The page keeps one stable ID and one URL that everyone bookmarks; each run overwrites the body in place and atlassian-cli bumps the version number for you, so Confluence's page history still records every sync. You never end up with twelve near-duplicate "Release 4.2 status (final) (v2)" pages.

If you do not have a page yet, create one once and note the ID it returns:

# One-time: create the page that the sync will keep updated
atlassian-cli confluence page create \
  --profile prod \
  --space 65601 \
  --title "Release 4.2 status" \
  --body release-table.html

Two flags make this safe to run against a live page. Use --status draft on confluence page update when you want to stage a change without publishing it to readers, and use confluence page get 1310724 --body-only to dump the current body if you ever want to diff what is live against what your script is about to push. Both are handy while you are still tuning the table layout.

Make it self-updating with cron

The payoff is scheduling. Put the three steps in one script that authenticates with a non-interactive profile, and the Confluence page maintains itself:

#!/usr/bin/env bash
# sync-release-page.sh -- keep a Confluence page in sync with Jira
set -euo pipefail

PROFILE="prod"
PAGE_ID="1310724"
JQL="project = REL AND fixVersion = 4.2 ORDER BY status"

# 1. Pull the issues from Jira
atlassian-cli jira issue search \
  --profile "$PROFILE" \
  --jql "$JQL" \
  --format json > issues.json

# 2. Render the storage-format table
{
  echo '<h2>Release 4.2 status</h2>'
  echo "<p>Last synced: $(date -u '+%Y-%m-%d %H:%M UTC')</p>"
  echo '<table><tbody><tr><th>Key</th><th>Summary</th><th>Status</th><th>Assignee</th></tr>'
  jq -r '.[] | "<tr><td>\(.key)</td><td>\(.fields.summary)</td><td>\(.fields.status.name)</td><td>\(.fields.assignee.displayName // "Unassigned")</td></tr>"' issues.json
  echo '</tbody></table>'
} > release-table.html

# 3. Push it into the page
atlassian-cli confluence page update "$PAGE_ID" \
  --profile "$PROFILE" \
  --body release-table.html

echo "Synced $(jq 'length' issues.json) issues to page $PAGE_ID"

Then add one cron entry. This refreshes the page every weekday at 08:00 and logs each run so you can confirm it worked and catch auth or API errors:

# crontab -e
0 8 * * 1-5  /home/ci/scripts/sync-release-page.sh >> /var/log/release-sync.log 2>&1

The set -euo pipefail line at the top of the script is doing quiet but important work here. If the Jira fetch fails, for example because of an expired token or a rate-limit response, the script aborts before the render step, so it never pushes a half-built or empty table over good content that is already on the page. That failure surfaces in the log file, and the last successful sync stays live until the next run repairs it. This is the difference between an automation you can trust to run unattended and one you have to check every morning anyway.

Store the credentials for the prod profile once with atlassian-cli auth login, and the scheduled job runs unattended. The same script slots neatly into a CI pipeline stage if you would rather trigger the sync on every deployment than on a clock. For a fuller markdown-driven variant of this pipeline, see the markdown sync runbook.

Approaches compared

There is more than one way to keep a Confluence page aligned with Jira. Here is how the CLI sync compares to the common alternatives on the factors that actually matter for a self-updating page:

Approach Stays current on its own Setup Notes
Manual copy-paste No None Accurate only at the moment someone updates it; drifts immediately after.
Jira issue macro Live In-editor Renders live from JQL, but layout and columns are fixed by the macro and it needs a viewer with Jira access.
CLI sync + cron On a schedule One script Full control of table markup and query; renders to plain page content that any reader can see.

The macro is the right tool when a live, interactive issue list is what you want inside the editor. The CLI sync wins when you want a fixed, readable snapshot with custom columns, computed fields, or extra context around the table, refreshed on a cadence you choose and visible to readers who do not have Jira seats.

Try atlassian-cli

One free, MIT-licensed Rust binary for Jira, Confluence, Bitbucket, and Jira Service Management. Script the whole sync in a few lines.

Install atlassian-cli

FAQ

How do I sync Jira data to a Confluence page automatically?

Pull the issues you care about with atlassian-cli jira issue search --jql "..." --format json, render that JSON into a Confluence storage-format HTML table with a small jq snippet, and push the result into an existing page with atlassian-cli confluence page update <PAGE_ID> --body table.html. Put those three steps in a shell script and run it from cron, and the page refreshes itself with no manual copy-paste.

Can atlassian-cli update an existing Confluence page instead of creating a new one?

Yes. confluence page update <PAGE_ID> --body file.html replaces the body of an existing page and bumps its version automatically. You keep one stable page ID and URL that everyone bookmarks, and each sync overwrites the content in place rather than creating a new page every run.

What format does the --body flag expect?

The --body flag on confluence page create and confluence page update takes a path to a file containing Confluence storage format, which is XHTML. Standard tags like <h2>, <p>, <table>, <tr>, <th>, and <td> work directly, so you can generate the file with any script that emits HTML.

How do I schedule the Jira to Confluence sync?

Wrap the pull-render-push steps in one shell script that uses a non-interactive profile, then add a cron entry such as 0 8 * * 1-5 /path/to/sync-release-page.sh to run it every weekday morning. Redirect output to a log file so you can confirm each run succeeded and spot API or auth failures.

Related Resources