#8191·elsa-core

WorkflowInstancesDeleting does not purge bookmark queue / dead-letter items

Author: sfmskywalkerCreated Sep 20, 2026Updated Sep 20, 2026
Labelsbugcorepersistenceprio lowelsa 3triaged

Problem

WorkflowInstancesDeleting already cascades bookmarks + activity/workflow execution logs, but bookmark queue and bookmark queue dead-letter rows keyed by WorkflowInstanceId are left behind.

Both stores already expose filter-scoped DeleteAsync (and filters already support WorkflowInstanceId), but nothing wires them to the instance-delete notification — same shape as Labels version-delete orphans (#8190).

Evidence (main / elsa-core)

What does cascade today

src/modules/Elsa.Workflows.Runtime/Handlers/

  • DeleteBookmarksINotificationHandler<WorkflowInstancesDeleting>
  • DeleteActivityExecutionLogRecords → same
  • DeleteWorkflowExecutionLogRecords → same

Published from WorkflowInstanceManager before store.DeleteAsync.

What does not

No handler under Handlers/ (or elsewhere) deletes:

  • BookmarkQueueItem via IBookmarkQueueStore.DeleteAsync(BookmarkQueueFilter)
  • BookmarkQueueDeadLetterItem via IBookmarkQueueDeadLetterStore.DeleteAsync(BookmarkQueueDeadLetterFilter)

on WorkflowInstancesDeleting.

Store delete APIs exist (used elsewhere, not on instance delete)

  • IBookmarkQueueStore.DeleteAsync — called from BookmarkQueueProcessor (per-item after success/dead-letter) and DefaultBookmarkQueuePurger (age / CreatedAtLessThan)
  • IBookmarkQueueDeadLetterStore.DeleteAsync — API delete endpoint + age purger

Filters already key by instance

  • BookmarkQueueFilter.WorkflowInstanceId (…/Filters/BookmarkQueueFilter.cs)
  • BookmarkQueueDeadLetterFilter.WorkflowInstanceId (…/Filters/BookmarkQueueDeadLetterFilter.cs)

Entities carry the FK: BookmarkQueueItem.WorkflowInstanceId, BookmarkQueueDeadLetterItem.WorkflowInstanceId.

Runtime symptom (not just a leak)

BookmarkQueueProcessor.ProcessItemAsync: when resume returns zero matching bookmarks (instance already gone + bookmarks already deleted by DeleteBookmarks), the item is retained (return true). Orphans sit until age purge / max delivery attempts → dead-letter noise, wasted worker cycles, and operator dead-letter lists that reference deleted instances.

Why it matters

  • Persistence hygiene: instance delete is incomplete relative to sibling runtime stores.
  • Correctness: queue worker keeps probing deleted instances; dead-letter accumulates unreplayable junk.
  • Subtractive: wire existing DeleteAsync + filters — no new store surface required for the common path.

Proposed subtractive fix

  1. Add a DeleteBookmarkQueueItems (name flexible) handler on WorkflowInstancesDeleting that, for each id in notification.Ids, calls IBookmarkQueueStore.DeleteAsync(new BookmarkQueueFilter { WorkflowInstanceId = id }) and the same for dead-letter via BookmarkQueueDeadLetterFilter.
  2. Optional small contract polish: add WorkflowInstanceIds to both filters (mirror BookmarkFilter.WorkflowInstanceIds / ActivityExecutionRecordFilter.WorkflowInstanceIds) so bulk instance delete is one round-trip — not required for correctness.
  3. Do not rely on age purger as the cascade; purger is TTL hygiene, not delete semantics.

Related / not duplicate

  • #8190 — Labels version-delete orphans (definition/version cascade; same pattern, different notification)
  • Age-based DefaultBookmarkQueuePurger / dead-letter APIs — complementary, not a substitute
  • Avoid: UserTasks / Alterations instance projections (tracked elsewhere)

Light milestone note for Crew Lead: Soft Backlog / correctness; small handler wiring; off 3.9 unless queue noise is user-blocking. Do not auto-assign.

Source: elsa-workflows/elsa-core