elsa-workflows/elsa-core · error · InvalidOperationException
Cannot overwrite an AI conversation that belongs to another…
Error message
Cannot overwrite an AI conversation that belongs to another tenant.
What it means
EFCoreAIConversationStore.SaveAsync refuses to update an existing AIConversationRecord whose TenantId differs from the incoming conversation (using BelongsToTenant with null-to-default normalization). This blocks cross-tenant overwrites at the persistence layer. Thrown as InvalidOperationException from the public SaveAsync method.
Solutions
- Match the stored TenantId when saving an existing conversation Id.
- Generate a new conversation Id when the tenant changes.
- Delete the old record first if a tenant change is genuinely intended.
- Confirm tenant resolution in the app supplies the same TenantId used at creation.
Example fix
// before
await store.SaveAsync(new AIConversation { Id = id, TenantId = "tenant-b" });
// after
await store.SaveAsync(new AIConversation { Id = id, TenantId = "tenant-a" }); Defensive patterns
Strategy: validation
Validate before calling
var existing = await dbContext.Conversations.AsNoTracking().FirstOrDefaultAsync(c => c.Id == conversation.Id);
if (existing is not null && !Equals(existing.TenantId ?? "", conversation.TenantId ?? ""))
throw new InvalidOperationException("Tenant mismatch for " + conversation.Id); Type guard
bool BelongsToTargetTenant(AIConversationRecord? existing, AIConversation c) => existing is null || Equals(existing.TenantId ?? "", c.TenantId ?? "");
Try / catch
try { await store.SaveAsync(conversation); } catch (InvalidOperationException ex) when (ex.Message.Contains("another tenant")) { logger.LogWarning(ex, "Cross-tenant EF save blocked for {Id}", conversation.Id); } Prevention
- Persist and reapply TenantId from ambient context on every save.
- Generate fresh Ids when data moves between tenants.
- Cover cross-tenant save scenarios in integration tests.
When it happens
Trigger: Saving a conversation whose Id exists in the database but whose TenantId differs from the stored record's TenantId (including null vs set values).
Common situations: Lost or default tenant context in background jobs; reusing conversation IDs across tenants in seeds/tests; tenant ID casing or normalization differences upstream.
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 conversation that belongs to another…
- Cannot overwrite an AI conversation that belongs to another…
- ' ' is not a well-formed permission. Expected ' : '.
- Register with configured before calling , or call with a…
- AI conversation entity metadata was not found.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/f3a3551aa98094ab.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs:42
return conversation;
return null;
}
public async ValueTask SaveAsync(AIConversation conversation, CancellationToken cancellationToken = default)
{
Validate(conversation);
var isNew = false;
var record = await dbContext.Conversations.FindAsync([conversation.Id], cancellationToken);
if (record == null)
{
record = new AIConversationRecord { Id = conversation.Id };
dbContext.Conversations.Add(record);
isNew = true;
}
else if (!BelongsToTenant(record.TenantId, conversation.TenantId))
{
throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another tenant.");
}
else
{
ValidateUserOwnership(record, conversation);
}
Map(conversation, record);
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException e) when (isNew)
{
await RetryAsUpdateAsync(conversation, e, cancellationToken);
}
}
View on GitHub (pinned to fe9217bdfa)