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

  1. 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.
  2. Reuse the existing dead-letter record's Id when creating the new item so the uniqueness check passes.
  3. Delete the existing dead-letter item first if the new record should supersede it.
  4. 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

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


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)