Notes from the work

One email when a tutorial or migration write-up goes live. Nothing else, and an unsubscribe link in every email.

LeanZero

Two people in Romania doing Atlassian migrations, Forge apps and practical AI work for teams that would rather talk to the person doing the job. Most of what we learn ends up on this site.

Services

  • Atlassian Migrations
  • Atlassian FastShift
  • Atlassian Maintenance
  • Forge App Development
  • AI Development Consultation

Topics

  • Jira
  • Jira Service Management
  • Confluence
  • Bitbucket
  • Atlassian Forge
  • Cloud Migration
  • Local AI
  • AI Coding
  • Certifications
  • Atlassian Team
  • All topics

Company

  • Blog
  • Tutorials
  • Contact

Community

  • Join Discord
  • Support this site

© 2026 LeanZero. All rights reserved.

Privacy PolicyTerms of Service24/7 SupportTrust CenterSecurityDPALegal notice
  1. Home
  2. Tutorials
  3. Confluence Forge Delete Page Event
Confluence Forge Delete Page Event: Why Space and Parent-Page Deletes Leak Orphaned Data
Atlassian Forge

Confluence Forge Delete Page Event: Why Space and Parent-Page Deletes Leak Orphaned Data

Gabriela Perdum
Gabriela Perdum
Author
11 min readSeptember 28, 2026

Key takeaways

  • END STATE: a Forge app that correctly detects every page, blog post, comment, attachment and custom-content deletion in Confluence, including the ones caused by a space or parent-page delete — not just the ones that fire their own event.
  • Confluence does not fire a page delete event for a Forge app when a space or a parent page is deleted. Atlassian's own documentation is explicit: 'We only emit a delete event for top-level entities.' Deleting a space fires no per-page, per-blog-post, per-comment or per-attachment delete events; deleting a parent page fires none for its descendants.
  • The one exception: trashing a single content-tree entity on its own, not as part of a larger deletion, detaches it from the hierarchy first — so that one entity does get its own delete event.
  • Atlassian's prescribed fix is a cache-then-recurse pattern: listen only at the space/page/blog-post/custom-content level, cache the entity structure yourself via REST before anything is deleted, then recurse over your own cached hierarchy to find everything else that needs cleaning up when a higher-level delete event fires.
  • A separate, apparently open bug is easy to confuse with this by-design behavior: a real developer report from June 2026 describes trashed:page and deleted:page events not firing AT ALL, with correct scopes and a fresh redeploy, tied to two still-open Atlassian tickets. That is not the same problem as this tutorial solves, and this tutorial does not claim it is fixed.

A Forge app that listens for avi:confluence:deleted:page to keep its own storage in sync looks correct in testing. Trash a single page on its own, the event fires, your cleanup handler runs, everything's fine. Then a customer deletes a space with four hundred pages in it, and your app never hears about any of them. The storage sits there, orphaned, forever — not because your trigger is broken, but because Confluence was never going to tell you.

Atlassian's own documentation says this plainly, in a section most people never read until they've already shipped the bug: "We only emit a delete event for top-level entities." This tutorial is that rule in full, the one exception to it, the workaround Atlassian itself prescribes, and — because it shares a search cluster with a real, separate problem — a clear line between "the event doesn't fire for a descendant, by design" and "the event doesn't fire at all, which might be a bug." It's the same shape of trap as a Forge bridge call that resolves cleanly while silently doing nothing — the platform not telling you something happened is a recurring category of Forge bug, not a one-off.

Note

Prerequisites

  • A Forge app with a Confluence delete-event trigger already registered (avi:confluence:deleted:page, avi:confluence:trashed:page, or the blog-post/space equivalents).
  • write:confluence-content and read:confluence-content.summary scopes granted, per the current reference docs.
  • Basic familiarity with the Forge manifest and Confluence's REST API v2.
  • A Confluence Cloud site (sandbox is fine) where you can create a small page hierarchy and delete it to observe the behavior yourself.
  1. 1

    Know the rule before you build around it

    cascading deletes emit exactly one event, not one per descendant.

  2. 2

    Understand the one exception

    an individually-trashed entity still gets its own event.

  3. 3

    Cache the hierarchy before anything is deleted, using the REST descendants endpoint.

  4. 4

    Listen at the right level, and recurse over your own cache when a higher-level delete fires.

  5. 5

    Don't confuse this with a different, apparently open bug

    events that don't fire at all.

Why Confluence page delete events never fire in your Forge trigger

Atlassian's Forge events documentation has a section called "Handling cascading events in Confluence," and it states the scope directly: "When a space is deleted in Confluence, all its descendants are also removed. This includes entities from a content tree (pages, whiteboards, databases, smart links, folders) as well as blog posts, and child entities: comments, attachments, and custom content. However, it's important to note: We only emit a delete event for top-level entities. When a space is deleted, we won't emit separate delete events for its children, which include pages, blog posts, whiteboards, databases, smart links, folders, custom content, comments, attachments, or child custom content." The same applies one level down: deleting a page or a blog post fires no delete events for its own comments, attachments, or custom content.

That's the whole rule, and it's worth restating precisely because it's easy to round it off into something looser. This isn't "delete events are unreliable" or "delete events sometimes don't fire" — it's a specific, documented, by-design scope limit: the event fires once, for the entity actually named in the delete action, and never for anything underneath it.

The reference table for these events lists what actually exists to listen for: avi:confluence:trashed:page, avi:confluence:restored:page, and avi:confluence:deleted:page for pages; avi:confluence:trashed:blogpost, avi:confluence:restored:blogpost, and avi:confluence:deleted:blogpost for blog posts; avi:confluence:trashed:custom_content, avi:confluence:restored:custom_content, and avi:confluence:deleted:custom_content for custom content; avi:confluence:trashed:attachment, avi:confluence:restored:attachment, and avi:confluence:deleted:attachment for attachments; avi:confluence:deleted:comment for comments (comments have no trashed state — they go straight to deleted); and avi:confluence:deleted:space:V2 for spaces. Page and blog-post events need the read:confluence-content.summary scope; the trashed variants additionally need write:confluence-content — a requirement the docs themselves flag as one that "may be removed in the future," so don't build a permanent assumption around it either way. One more gap worth knowing before you rely on the exception in the next section: whiteboards, databases, folders, and smart links currently have no trashed/deleted/restored trigger at all — only created/updated/moved/copied/permissions_updated — so the individually-trashed exception is only actually actionable today for pages, blog posts, attachments, comments, and custom content.

Understand the one exception

There's exactly one case where an individual entity gets its own delete event even though it's part of a hierarchy: trashing it on its own. The documentation states it exactly: "When an individual content tree entity is trashed, it gets detached from a content tree hierarchy, so all individually deleted entities will receive delete events." Trash one page by itself, in isolation, and that page's own delete event fires normally. Trash the space it lives in, and that same page's delete event never fires — because this time it wasn't deleted on its own, it was deleted as part of the space.

This is the distinction that makes the rule easy to miss in testing. If your test plan is "trash a page, confirm the event fires, ship it," you will never see the gap — you're always testing the one case that works. It's the same testing blind spot behind Confluence's nested-macro migration trap: the simple case you naturally test first is exactly the one Atlassian's own documentation already covers well, and the gap only shows up once real content gets more structured than your test fixture.

Cache the content tree with the descendants REST API before you delete

Atlassian's own prescribed fix isn't a different event to listen for — there isn't one — it's caching the structure yourself ahead of time. The documentation frames it in three steps: listen for delete events only at the higher levels (space, page, blog post, custom content) as your trigger that a cascading delete may have happened; fetch and save the entity structure before anything is deleted, using Confluence's REST API to record all the content tree entities, blog posts, comments, attachments, and custom content your app cares about; then, when a higher-level delete event fires, recurse over your own cached hierarchy to find everything else that needs handling, rather than waiting for events that will never come.

The REST endpoint that does the fetching is GET /wiki/api/v2/pages/{id}/descendants, which returns descendants in the content tree for a given page, in top-to-bottom order, going as deep as you ask:

bash
1curl -s -H "Authorization: Basic $AUTH" \
2  "https://your-site.atlassian.net/wiki/api/v2/pages/${PAGE_ID}/descendants?depth=5&limit=250"
text
1{
2  "results": [
3    {
4      "id": "98765",
5      "status": "current",
6      "title": "Runbook: escalation paths",
7      "type": "page",
8      "parentId": "12345",
9      "depth": 1,
10      "childPosition": 0
11    }
12  ],
13  "_links": { "next": "..." }
14}

depth defaults to 2 and goes up to 10; limit defaults to 25 and goes up to 250 per page of results. It requires the read:hierarchical-content:confluence scope. Call it on a schedule, or on every page-create/update event you already receive, and keep your own record current — the whole point is that by the time a space or parent page is deleted, you already know what was underneath it, because Confluence isn't going to tell you afterward.

How you know it worked: run the curl call above against a page you know has children, and confirm the response's results array actually lists them — check depth and parentId on each entry match what you expect, and that raising depth picks up grandchildren too. If results comes back empty for a page you know has children, check the read:hierarchical-content:confluence scope is granted before assuming the endpoint itself is wrong.

Detecting page deletes in Confluence Cloud (the workaround)

Putting the pieces together, your app's delete-handling logic ends up with two distinct paths. The low-level path handles the case the exception covers: an individually trashed page, blog post, comment, or attachment fires its own event, and you handle it directly, the way you'd expect. The high-level path handles everything else: a space-deleted or page-deleted event fires once, and your handler doesn't ask "what was deleted" — it asks "what did I have cached under this entity," walks that cached list, and cleans up every one of those records itself, because no further events are coming for any of them.

javascript
1export async function onPageDeleted(event, context) {
2  const cachedDescendants = await getCachedDescendants(event.page.id);
3  for (const entity of cachedDescendants) {
4    await cleanUpStoredData(entity.id, entity.type);
5  }
6  await clearCachedHierarchy(event.page.id);
7}

How you know it worked: create a small hierarchy in a test space — a parent page with two child pages, each with a comment and an attachment — run your caching step, then delete the parent page. If your app's storage still holds references to the child pages, comments, or attachments after the parent's single delete event has fired and your handler has run, the recursion step isn't reaching them; check that your cached structure actually includes descendants at the depth you deleted, not just direct children.

Don't confuse this with a different, apparently open bug

There's a second, unrelated problem living in the same search results, and it's worth separating clearly so you don't waste time debugging the wrong thing. A developer reported in June 2026 that avi:confluence:trashed:page and avi:confluence:deleted:page weren't firing at all — not "only for the top-level entity," but zero invocations, zero logs, for any delete of any kind, despite correct event names in the manifest, the right scopes granted, and a full uninstall-and-clean-redeploy to rule out a stale install. The same developer cross-posted the same report to Atlassian's community forum — not a second, independent confirmation, the same person following up in a second venue — where it's tied to two open tickets on Atlassian's public tracker asking for avi:confluence:deleted:page and avi:confluence:deleted:attachment support and for the write:confluence-content scope requirement to be relaxed. As of this writing, that thread ends without a staff reply confirming or denying it as a known issue — the last response just points the poster at Developer Support. There's a real tension worth naming rather than glossing over: one of the two open tickets, CONFCLOUD-82453, asks Atlassian to "add support for avi:confluence:deleted:page" — but the current reference docs already list that exact trigger as existing. Nothing found resolves whether the ticket is stale, scoped more narrowly than its title suggests, or points at a genuine reliability gap between "documented as shipped" and "actually fires." This tutorial doesn't resolve it either — it's recorded here as open, not explained away.

If you've registered the right events, granted the right scopes, and redeployed cleanly, and you're still seeing nothing fire for any delete — not just the descendants this tutorial explains — that's the other problem, not this one. This tutorial doesn't attempt to fix it, and you shouldn't assume the cache-and-recurse pattern above will help if events aren't reaching your app in the first place.

What this tutorial doesn't cover

This tutorial documents Confluence's cascading-delete event scope exactly as Atlassian's current documentation states it, the REST-based workaround Atlassian itself prescribes, and the boundary between that documented behavior and a separate, apparently unresolved delivery bug. It does not reproduce the cascading-delete behavior against a live Forge app deployment — the rule and the workaround are sourced entirely to Atlassian's own current documentation and REST API reference, not to a fresh test run for this piece. It does not resolve whether the two open tickets referenced above indicate a genuine reliability gap or something narrower — that's recorded as open, not settled. And it does not track a 2023 community answer that names a avi:confluence:archived:page event, which doesn't appear anywhere in the current trigger reference — if you find that name in older material, treat it as unverified against today's documented events, not as something to build against.

Key takeaways

  • You now have the exact scope of Confluence's cascading-delete event limitation, quoted from Atlassian's own current documentation: one delete event per top-level entity deleted, none for its descendants, with the single exception of an entity trashed entirely on its own.
  • You now have the REST-based workaround Atlassian itself prescribes — cache the hierarchy via GET /wiki/api/v2/pages/{id}/descendants before deletion, listen only at the higher levels, and recurse over your own cached structure when a higher-level delete fires.
  • This does not cover a separate, apparently unresolved bug where delete events don't fire at all, for any entity — reported by a real developer with correct configuration, tied to two open Atlassian tickets, and unresolved in the thread as fetched.
  • This does not cover whether the two open Atlassian tickets referenced here indicate a broader reliability problem — that's recorded as an open question, not something this tutorial claims to answer.
ShareXLinkedIn
PreviousWe couldn't export Issue Security Level Permission: fixing the Jira JCMA error step by stepNextForge app access rule: what your app sees when a data security policy blocks it

Tags

forgeconfluenceeventstriggersrest-apicustom-ui

Comments

0

Add a comment

Confirm your address using the email we send. You can unsubscribe at any time. Your email stays private and this does not sign you up for the newsletter.

Have Questions?

Join our Discord community to ask questions and get help with implementing this tutorial.

Join Discord

On this page

  • Why Confluence page delete events never fire in your Forge trigger
  • Understand the one exception
  • Cache the content tree with the descendants REST API before you delete
  • Detecting page deletes in Confluence Cloud (the workaround)
  • Don't confuse this with a different, apparently open bug
  • What this tutorial doesn't cover

Join the Community

Join the community to ask questions, share your work, and connect with other professionals.

Join Discord

Found My Advice Helpful?

If I helped you solve something for free, coffee money is always appreciated (but never expected).

Buy Me a Coffee

About the Author

Gabriela Perdum

Gabriela Perdum

Atlassian & AI Specialist

Related reading

DeepSeek V4.1 Flash coding benchmark: 0.33 on a Forge app upgrade, Opus 5.5 got 0.67

DeepSeek V4.1 Flash coding benchmark: 0.33 on a Forge app upgrade, Opus 5.5 got 0.67

Jira Gantt Chart Critical Path: Show the Work That Sets the Finish of a 1,992-Row Plan

Jira Gantt Chart Critical Path: Show the Work That Sets the Finish of a 1,992-Row Plan

Claude Haiku 5.5 vs GPT-6 Luna on two coding benchmarks: same price, 3.6x the agent bill

Claude Haiku 5.5 vs GPT-6 Luna on two coding benchmarks: same price, 3.6x the agent bill