elsa-workflows/elsa-core · error · InvalidOperationException
Cannot overwrite an AI proposal that belongs to another…
Error message
Cannot overwrite an AI proposal that belongs to another tenant.
What it means
EFCoreAIProposalStore.SaveAsync finds or creates a record for the proposal; when a record already exists, it verifies tenant ownership before overwriting. If the existing record's TenantId does not match proposal.TenantId (after NormalizeTenantId, null/empty = default ""), it throws InvalidOperationException to block cross-tenant overwrites of AI proposals.
Solutions
- Use unique proposal IDs per tenant (Guids) so cross-tenant collisions are impossible.
- Ensure tenant ID flows consistently into SaveAsync on both create and update (check tenancy middleware).
- Inspect the conflicting record's owner tenant; delete or migrate it if created by a bug.
- Handle InvalidOperationException in your service and surface a tenant-mismatch error to the caller.
Example fix
// before
await proposalStore.SaveAsync(new AIProposal { Id = fixedId, ConversationId = cid, CreatedBy = user }); // TenantId null
// after
await proposalStore.SaveAsync(new AIProposal { Id = Guid.NewGuid().ToString(), ConversationId = cid, CreatedBy = user, TenantId = tenantContext.Id }); Defensive patterns
Strategy: try-catch
Validate before calling
var existing = await dbContext.Proposals.FindAsync([proposal.Id]);
if (existing != null && !string.Equals(existing.TenantId ?? "", proposal.TenantId ?? "", StringComparison.Ordinal))
throw new InvalidOperationException("Cross-tenant proposal write blocked."); Type guard
bool TenantMatches(string? recordTenant, string? tenantId) => string.Equals(recordTenant ?? "", tenantId ?? "", StringComparison.Ordinal);
Try / catch
try { await proposalStore.SaveAsync(proposal); }
catch (InvalidOperationException ex) when (ex.Message.Contains("belongs to another tenant")) { /* return 409/403-style conflict; do not retry blindly */ } Prevention
- Use per-tenant-unique proposal IDs.
- Ensure tenant middleware supplies TenantId on every save.
- Test tenant-scoping behavior (read and write paths) in CI.
- Audit any data-copy/import jobs for tenant ID preservation.
When it happens
Trigger: Calling SaveAsync with a proposal whose Id already exists in the Proposals table but is owned by a different tenant (record.TenantId != normalized proposal.TenantId).
Common situations: Deterministic proposal IDs (e.g. derived from conversation content) colliding across tenants; tenant context lost between creation and update so the update resolves to the default tenant; data copied between tenant databases with shared IDs.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Cannot overwrite an AI proposal that belongs to another…
- Cannot overwrite an AI conversation that belongs to another…
- Cannot overwrite an AI conversation that belongs to another…
- Failed to insert AI proposal
- A proposal ID is required.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/f44a22a879dca8c9.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs:34
x => x.Id == id && (x.TenantId ?? "") == normalizedTenantId,
cancellationToken);
return record == null ? null : Map(record);
}
public async ValueTask SaveAsync(AIProposal proposal, CancellationToken cancellationToken = default)
{
Validate(proposal);
var isNew = false;
var record = await dbContext.Proposals.FindAsync([proposal.Id], cancellationToken);
if (record == null)
{
record = new AIProposalRecord { Id = proposal.Id };
dbContext.Proposals.Add(record);
isNew = true;
}
else if (!BelongsToTenant(record.TenantId, proposal.TenantId))
{
throw new InvalidOperationException("Cannot overwrite an AI proposal that belongs to another tenant.");
}
else
{
ValidateUserOwnership(record, proposal);
}
Map(proposal, record);
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException e) when (isNew)
{
await RetryAsUpdateAsync(proposal, e, cancellationToken);
}
}
View on GitHub (pinned to fe9217bdfa)