elsa-workflows/elsa-core · error · DbUpdateException
Failed to insert AI proposal
Error message
Failed to insert AI proposal {proposal.Id}, and no existing record was found for retry. What it means
When the initial insert in SaveAsync fails with a concurrency/key exception, the store retries as an update (RetryAsUpdateAsync). It re-fetches the proposal record with ChangeTracker cleared; if the record no longer exists (deleted between the failed insert and the retry), it wraps the situation in a new DbUpdateException with this message, preserving the original exception as InnerException.
Solutions
- Check originalException.InnerException to find the real insert failure (unique constraint vs. connection).
- Add a retry policy around SaveAsync; if the record was concurrently deleted, decide whether to re-insert or surface a not-found error.
- Remove conflicting unique indexes that treat legitimate new proposals as duplicates.
- Log and investigate concurrent writers to the same proposal Id.
Example fix
// before
await proposalStore.SaveAsync(proposal); // DbUpdateException wrapped: "Failed to insert..."
// after
try { await proposalStore.SaveAsync(proposal); }
catch (DbUpdateException ex) when (ex.InnerException is DbUpdateConcurrencyException) { /* re-fetch and decide */ } Defensive patterns
Strategy: retry
Validate before calling
var exists = await dbContext.Proposals.AnyAsync(p => p.Id == proposal.Id); // decide: if not exists after failed insert, re-insert or surface a permanent error
Try / catch
try { await proposalStore.SaveAsync(proposal); }
catch (DbUpdateException ex) { logger.LogWarning(ex, "Insert retry failed for proposal {Id}", proposal.Id); /* check ex.InnerException; re-fetch and re-insert or fail */ } Prevention
- Configure a bounded retry policy for transient DB failures.
- Investigate InnerException to distinguish unique-constraint vs. concurrency vs. connectivity failures.
- Avoid unique indexes that collide with legitimate new proposals.
- Detect and log concurrent writers to the same proposal Id.
When it happens
Trigger: SaveAsync hits a DbUpdateException on insert, RetryAsUpdateAsync runs FindAsync on Proposals, and the record is not found — i.e., the original insert failed on a unique constraint other than the primary key, or the row was deleted concurrently.
Common situations: Concurrent deletion of the proposal between insert failure and retry; a unique-index violation on a non-PK column (e.g. conversation/sequence key) causing insert failure where no row with that Id exists; connection/transaction issues making the fetched row invisible.
Related errors
- Failed to insert AI conversation
- Cannot overwrite an AI proposal that belongs to another…
- Cannot overwrite an AI proposal that belongs to another…
- A proposal ID is required.
- A proposal conversation ID is required.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/c2e4cf4c41b80587.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs:58
Map(proposal, record);
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException e) when (isNew)
{
await RetryAsUpdateAsync(proposal, e, cancellationToken);
}
}
private async ValueTask RetryAsUpdateAsync(AIProposal proposal, DbUpdateException originalException, CancellationToken cancellationToken)
{
dbContext.ChangeTracker.Clear();
var record = await dbContext.Proposals.FindAsync([proposal.Id], cancellationToken);
if (record == null)
throw new DbUpdateException($"Failed to insert AI proposal {proposal.Id}, and no existing record was found for retry.", originalException);
if (!BelongsToTenant(record.TenantId, proposal.TenantId))
throw new InvalidOperationException("Cannot overwrite an AI proposal that belongs to another tenant.");
ValidateUserOwnership(record, proposal);
Map(proposal, record);
await dbContext.SaveChangesAsync(cancellationToken);
}
private static AIProposal Map(AIProposalRecord record) =>
new()
{
Id = record.Id,
TenantId = record.TenantId,
ConversationId = record.ConversationId,
Kind = ParseEnum(record.Kind, AIProposalKind.WorkflowCreate),
Status = ParseEnum(record.Status, AIProposalStatus.Draft),View on GitHub (pinned to fe9217bdfa)