
Move, copy, label and archive up to 500 selected Confluence pages in one job, after a dry run shows you what will happen to each page.
Bulk Page Operations is a Forge app for Confluence Cloud. It adds a page to each space's settings. You pick pages with CQL or by choosing a parent page, pick what to do with them, check the dry run, and start the job. The job runs from the open page, a few pages at a time and as you, while you watch its progress. You can cancel it, and you can undo label changes later.
| Operation | What happens in Confluence |
|---|---|
| Move | Each top-level selected page moves under the target page, children included |
| Copy | Each page is copied under the target page, either on its own or with all its children |
| Add labels | Up to 10 labels are added to every selected page |
| Remove labels | Up to 10 labels are removed from every selected page |
| Archive | Each selected page is archived with Confluence's archive API |
You can select pages in two ways.
label = "draft" AND lastmodified < now("-180d"). The app always adds space = "<this space>" AND type = page around your query, so a job can't reach pages in other spaces. An ORDER BY is dropped, and unbalanced quotes or parentheses are refused before anything runs.Nothing changes until you've seen a preview. It lists every selected page with one of three results.
The preview also reads your permissions in the space (or in the target page's space for move and copy) and warns you if Confluence says you can't edit, add or archive pages there. It's a warning, not a block. Confluence still decides page by page when the job runs, because page restrictions can differ.
When you start the job, the server runs the same search and plan again. If any selected page or its result differs from the preview, the job doesn't start and you're asked to preview again. That holds even when the count is the same, for example when one page drops out of a CQL date range and another comes in. Children that aren't selected aren't part of this check, so a copy with children copies the children a page has when the job runs.
Jobs run from the settings page of the person who started them. The page calls the app's runJobBatch resolver again and again. Each call handles up to 5 pages (it starts no new page after about 12 seconds) and saves progress after every page. These are normal user-invoked resolver calls, so every Confluence request runs as you (asUser()) with your permissions. Keep the page open while the job runs. If you close it, the job pauses where it was. Open the page again and choose "Resume job", or cancel it. A paused job stops blocking new jobs in the space after 15 minutes, and you can still resume it later if no other job has started there. Only the person who started a job can run or resume it.
The app used to run jobs in the background with Forge async events. Forge doesn't give asUser() a usable user there. On a live site every page failed with PROXY_ERR. So there's no background step any more.
The page shows a progress bar, the counts and the first 100 failures with Confluence's own error message for each. Details in the job history lists every page of a job with its result (only for the person who started it).
PROXY_ERR), the page isn't counted as failed. The job retries a label or move request up to 3 times, 30 seconds apart. Then, or at once for copy and archive, it stops with a message that says so.You can always see and cancel the jobs you started. Other people's jobs are listed only for space admins, and only a space admin can cancel someone else's job. The server checks this with Confluence as you each time (your space operations must include administer on the space). If Confluence doesn't confirm it, you see only your own jobs. Admins see the operation, status and counts of other people's jobs. Page titles, the target page's title and failure details are shown only to the person who started the job.
Label jobs keep an undo log for 30 days. It records only what the job changed. A label that was already on a page before an add job isn't removed by undo, and a page that never had the label isn't touched.
The job notes each label change in the log before it sends it. If Confluence's answer gets lost (a server error after the change was made, or a batch cut off mid-request), the next attempt reads the page's labels again and logs the change if it happened. If the page fails for good, the job reads the labels once more and keeps only the changes it finds. Undo also checks each page before it changes anything, so it only reverses a label that's still in the state the job left it in. Only the person who started the job can undo it. Undo runs as a new job you can watch like any other, and if it fails or is cancelled you can run it again.
Move, copy and archive jobs can't be undone by the app. The confirmation dialog says so before you start.
Every call to Confluence runs as the person using the app (asUser()), from resolver calls made by the open settings page. That includes the job batches. There are no background steps and no asApp() calls. The app never calls Confluence as itself. So a job can only do what that person could do by hand, and the server runs a job's batches only for the person who started it.
| Scope | Why |
|---|---|
search:confluence | CQL page search |
read:confluence-content.summary | Read search results and Confluence task status |
write:confluence-content | Move, copy, archive, add and remove labels (v1 label POST and DELETE; v2 has no label write) |
read:page:confluence | Check the target page and the parent page, read page labels (v2) |
read:space:confluence | Find the space and read your permissions in it |
storage:app | Job records, progress and undo logs (30 days) |
The app has no external permissions, no remote back end and no egress. All code runs on Atlassian's Forge platform, so it's built to qualify for Runs on Atlassian. Atlassian decides eligibility, and forge eligibility checks it after deploy.
Once a week a scheduled trigger reports the stored account IDs to Atlassian with the Forge personal data reporting API (privacy.reportPersonalData). Job records of accounts that Atlassian reports as closed lose their account ID.
PRIVACY.md lists what's stored and for how long.
| What | Limit | What you see |
|---|---|---|
| Pages per job | 500 selected pages (copy with children also copies every child of each page) | More than 500 matches blocks the job with "narrow the selection" |
| CQL length | 2,000 characters | Input error |
| Labels per job | 10, each up to 100 characters (letters with accents, digits, _, -, +) | Input error |
| Copy title prefix | 50 characters | Input error |
| Jobs per space | 1 running at a time | "Another job is still running in this space" |
| Preview time | about 20 seconds of searching | "Confluence was too slow…", the job can't start |
| Failures listed | first 100 per job | The count still includes every failure |
| Pages per batch | 5 per resolver call, no new page after about 12 seconds | The page calls again at once |
| Job history | 20 most recent jobs per space, kept 30 days |
type = page).GET /wiki/api/v2/spaces/{id}/operations. Its live answer hasn't been checked yet. If it doesn't list administer for space admins, admins see only their own jobs and can't cancel other people's jobs. A stuck job from someone else then stops blocking new jobs after 15 minutes.forge lint to see if it needs a scope, and check the trigger's log line after the first week.Zovo builds and maintains this app. If it saves you time, you can support the work on GitHub Sponsors. Sponsorship is optional, and the tool works the same without it.
Zovo is made by theluckystrike on GitHub.
MIT, see LICENSE.
If this app saves you time, you can support the work on GitHub Sponsors. Monthly tiers start at $5, and one-time tiers start at $15. Sponsorship is optional, and the tool works the same without it.