elsa-workflows/elsa-core · error · InvalidOperationException
A bookmark queue dead-letter item for original queue item
Error message
A bookmark queue dead-letter item for original queue item '{record.OriginalQueueItemId}' already exists. What it means
MemoryBookmarkQueueDeadLetterStore.SaveAsync enforces that at most one dead-letter item exists per OriginalQueueItemId. If a record with the same OriginalQueueItemId (and a different Id) already exists, it throws this InvalidOperationException instead of creating a duplicate.
Solutions
- Before saving, query the store for an existing item with the same OriginalQueueItemId and update/replace it (keeping its Id) instead of inserting a new record.
- Reuse the existing dead-letter record's Id when creating the new item so the uniqueness check passes.
- Delete the existing dead-letter item first if the new record should supersede it.
- This duplicate check is specific to the in-memory store; if you see it, a production database provider may behave differently — verify which store implementation is registered.
Example fix
// before
var item = new BookmarkQueueDeadLetterItem { OriginalQueueItemId = queueItemId, /* ... */ };
await deadLetterStore.SaveAsync(item, ct);
// after
var existing = await deadLetterStore.FindByOriginalQueueItemIdAsync(queueItemId, ct);
if (existing != null) { existing.Timestamp = DateTimeOffset.UtcNow; await deadLetterStore.SaveAsync(existing, ct); }
else await deadLetterStore.SaveAsync(item, ct); Defensive patterns
Strategy: validation
Validate before calling
var existing = await deadLetterStore.ListAsync(ct);
bool dup = existing.Any(x => x.OriginalQueueItemId == item.OriginalQueueItemId && x.Id != item.Id);
if (dup) throw new InvalidOperationException("Duplicate dead-letter item for " + item.OriginalQueueItemId); Try / catch
try { await store.SaveAsync(record, ct); }
catch (InvalidOperationException) { /* fetch existing by OriginalQueueItemId and update it instead */ } Prevention
- Always look up by OriginalQueueItemId before inserting a dead-letter item.
- Reuse the existing record's Id when re-dead-lettering the same queue item.
- Treat dead-letter insertion as upsert semantics in your service layer.
When it happens
Trigger: Calling SaveAsync on the in-memory dead-letter store (directly or via the dead-letter service) with a BookmarkQueueDeadLetterItem whose OriginalQueueItemId matches an existing dead-letter record but whose own Id differs.
Common situations: Re-enqueueing then re-dead-lettering the same queue item without reusing/replacing the existing dead-letter record; seeding test data that duplicates an original queue item id; a retry flow that creates a new item object instead of updating the existing one.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- A stored trigger already exists for workflow
- A conversation ID is required. (Parameter 'conversation')
- A conversation user ID is required. (Parameter…
- A already exists with ' ' in tenant ' '.
- A label already exists with normalized name
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/cbcb55e9c404352c.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Workflows.Runtime/Stores/MemoryBookmarkQueueDeadLetterStore.cs:25
using Elsa.Workflows.Runtime.Options;
using JetBrains.Annotations;
namespace Elsa.Workflows.Runtime.Stores;
/// <inheritdoc />
[UsedImplicitly]
public class MemoryBookmarkQueueDeadLetterStore(MemoryStore<BookmarkQueueDeadLetterItem> store) : IBookmarkQueueDeadLetterStore
{
private readonly object _lock = new();
/// <inheritdoc />
public Task SaveAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default)
{
lock (_lock)
{
var existing = store.Find(x => x.OriginalQueueItemId == record.OriginalQueueItemId && x.Id != record.Id);
if (existing != null)
throw new InvalidOperationException($"A bookmark queue dead-letter item for original queue item '{record.OriginalQueueItemId}' already exists.");
store.Save(Clone(record), x => x.Id);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task AddAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default)
{
await AddOrGetExistingAsync(record, cancellationToken);
}
/// <inheritdoc />
public Task<BookmarkQueueDeadLetterItem> AddOrGetExistingAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default)
{
lock (_lock)
{View on GitHub (pinned to fe9217bdfa)