Automating Confluence Stale‑Page Audits with CQL and REST v2
Use CQL and Confluence REST v2 to find pages not updated in a year, flag them with labels, and handle rate limits safely. The post walks through query syntax, body‑format choices, a Python example, and trade‑offs.
30 Aug 2026, 08:23 UTC

Confluence spaces often become digital graveyards. Over time, project documentation, outdated specs, and abandoned meeting notes make it harder for teams to find reliable information. Manually scrolling through hundreds of pages to identify stale content is a poor use of engineering time, but automated deletion is too risky.
Use CQL to discover stale pages
The first step in any audit is discovery. Confluence uses CQL (Confluence Query Language), a SQL‑like syntax that lets you filter content by type, space, label, title, contributor, and dates. For a stale‑page audit the most critical field is lastmodified and the relative date function now(-90d) (90 days ago).
Example query that finds pages in the ENG space that haven’t been touched in a year:
type = page AND space = "ENG" AND lastmodified < now(-365d)
Run this query in the Confluence UI or through the API to validate that you’re pulling the expected pages.
REST v2 vs v1 and the body‑representation dilemma
Confluence Cloud exposes two REST families: the legacy /rest/api/content (v1) and the newer /api/v2. Atlassian recommends new integrations use v2, but v1 still offers some endpoints not yet ported. Before hard‑coding a script, check which features you need and target the appropriate version.
When writing back to a page you must choose a body representation:
| Format | Use case | Risk |
|---|---|---|
| Storage (XHTML) | Stable round‑trip for legacy content | May strip editor‑v2 macros |
| Editor v2 | Native for modern editor | Requires exact JSON shape; changes may break layout |
| Atlas Document Format (ADF) | Preferred for Cloud, used by the new editor | Complex to construct manually |
For an audit workflow that only needs to flag pages, it’s safest to avoid body writes altogether and manipulate labels instead.
Concrete example: flag stale pages with a label
The following Python snippet demonstrates the full flow: search for stale pages, add a review‑needed label, and respect rate limits. Run it from a machine that can reach your Confluence tenant and use a service account with read/write access only to the target spaces.
import requests, time
BASE_URL = "https://your-domain.atlassian.net/wiki/api/v2"
AUTH = ("[contact removed]", "YOUR_API_TOKEN")
# 1. Search for pages not modified in the last 365 days in space ENG
cql = "type = page AND space = 'ENG' AND lastmodified < now(-365d)"
search_resp = requests.get(f"{BASE_URL}/search", params={"cql": cql}, auth=AUTH)
if search_resp.status_code != 200:
raise RuntimeError(f"Search failed: {search_resp.status_code}")
pages = search_resp.json().get("results", [])
print(f"Found {len(pages)} stale pages")
# 2. Add the review‑needed label to each page
for page in pages:
page_id = page["id"]
label_url = f"{BASE_URL}/pages/{page_id}/labels"
payload = {"key": "review-needed"}
resp = requests.post(label_url, json=payload, auth=AUTH)
if resp.status_code == 201:
print(f"✔️ Page {page_id} flagged")
elif resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 30))
print(f"⚠️ Rate limited, sleeping {wait}s")
time.sleep(wait)
# retry once
resp = requests.post(label_url, json=payload, auth=AUTH)
if resp.status_code == 201:
print(f"✔️ Page {page_id} flagged after back‑off")
else:
print(f"❌ Failed to flag {page_id}: {resp.status_code}")
Trade‑offs and limitations
- Permission scope: Search and label operations return only content visible to the authenticated user. A dedicated service account with the minimal space permissions is essential.
- Rate limits: Cloud tenants enforce per‑endpoint limits. The script above backs off on 429 responses, but the exact threshold varies by tenant and can change with Atlassian releases.
- Label visibility: Adding a label does not modify the page body, so the UI still shows the page as is. If you need to archive or delete, you’ll have to add a second step that moves the page to a dedicated “Archive” space or uses the
DELETEendpoint. - Body‑format drift: If you ever need to rewrite the page body, prefer the ADF format for new pages and test round‑trips in a sandbox space.
Actionable next steps
- Validate the CQL query against your target space using the search bar.
- Run a dry‑run of the label‑update script on a single page to confirm the label appears in the UI.
- Schedule the script to run weekly or monthly, and surface flagged pages in a Confluence dashboard or Jira ticket for human review.
- Document the service account’s permissions and the rate‑limit handling strategy so new team members can maintain the workflow.
With a small, repeatable automation you can keep your Confluence spaces lean without risking accidental data loss.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.