elsa-workflows/elsa-core · error · DbUpdateException

Failed to insert AI conversation

Error message

Failed to insert AI conversation {conversation.Id}, and no existing record was found for retry.

What it means

When an insert fails with DbUpdateException (typically a concurrent insert of the same conversation Id), SaveAsync retries as an update by re-fetching the record. If the record is still not found, RetryAsUpdateAsync throws a DbUpdateException wrapping the original exception, signaling that neither insert nor an updateable record succeeded.

Solutions

  1. Avoid concurrent saves for the same conversation Id (serialize per conversation).
  2. Inspect the inner exception (originalException) for the actual DB error.
  3. Retry the save once more after the concurrent transaction commits.
  4. Delete-and-recreate the conversation if the record was intentionally removed.

Example fix

// before
// concurrent tasks saving the same conversation id
await Task.WhenAll(task1, task2); // one fails insert, retry finds nothing
// after
// serialize saves per conversation
await conversationLocks.Get(id).WaitAsync();
try { await store.SaveAsync(conversation); } finally { conversationLocks.Release(id); }
Defensive patterns

Strategy: retry

Validate before calling

var exists = await dbContext.Conversations.AnyAsync(c => c.Id == conversation.Id);
// serialize saves for the same conversation id before calling SaveAsync

Try / catch

try { await store.SaveAsync(conversation); }
catch (DbUpdateException ex) when (ex.InnerException is not null && ex.Message.Contains("no existing record was found"))
{ logger.LogWarning(ex, "Lost race saving conversation {Id}; retrying", conversation.Id); await Task.Delay(50); await store.SaveAsync(conversation); }

Prevention

When it happens

Trigger: Concurrent SaveAsync calls racing on the same conversation Id: the first insert fails on a unique-constraint, but the retry's FindAsync returns null (row deleted or transaction not yet committed).

Common situations: Duplicate message handling / concurrent AI turns saving the same conversation; retry storms after a transient DB failure; the conversation being deleted between the failed insert and the retry.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/0d83c1e3934dcc0b. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs:66

        Map(conversation, record);

        try
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateException e) when (isNew)
        {
            await RetryAsUpdateAsync(conversation, e, cancellationToken);
        }
    }

    private async ValueTask RetryAsUpdateAsync(AIConversation conversation, DbUpdateException originalException, CancellationToken cancellationToken)
    {
        dbContext.ChangeTracker.Clear();
        var record = await dbContext.Conversations.FindAsync([conversation.Id], cancellationToken);
        if (record == null)
            throw new DbUpdateException($"Failed to insert AI conversation {conversation.Id}, and no existing record was found for retry.", originalException);

        if (!BelongsToTenant(record.TenantId, conversation.TenantId))
            throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another tenant.");

        ValidateUserOwnership(record, conversation);

        Map(conversation, record);
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private static AIConversation Map(AIConversationRecord record) =>
        new()
        {
            Id = record.Id,
            TenantId = record.TenantId,
            UserId = record.UserId,
            Title = record.Title,
            Status = ParseEnum(record.Status, AIConversationStatus.Active),

View on GitHub (pinned to fe9217bdfa)