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
- Avoid concurrent saves for the same conversation Id (serialize per conversation).
- Inspect the inner exception (originalException) for the actual DB error.
- Retry the save once more after the concurrent transaction commits.
- 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
- Serialize SaveAsync calls per conversation Id (per-conversation lock/queue).
- Avoid deleting conversations while concurrent saves may be in flight.
- Include the inner DbUpdateException when logging for diagnosis.
- Add concurrency tests that save the same conversation Id in parallel.
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
- Register with configured before calling , or call with a…
- AI conversation entity metadata was not found.
- AI conversation table metadata was not found.
- Cannot overwrite an AI conversation that belongs to another…
- Failed to insert AI proposal
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)