elsa-workflows/elsa-core · error · InvalidOperationException
An alteration plan with ID
Error message
An alteration plan with ID '{plan.Id}' already exists and is not visible to the current tenant. What it means
MemoryAlterationPlanStore.EnsureIdAvailable throws when saving an alteration plan whose ID already exists in the in-memory store but is not replaceable by the current writer's tenant context. Tenant-agnostic entries ('*') are visible to all tenants but can only be replaced by an agnostic writer, so any cross-tenant or tenant-vs-agnostic collision raises this InvalidOperationException from SaveAsync.
Solutions
- Use a fresh unique plan ID for the new plan.
- Align the current tenant context with the existing plan's tenant to perform a legitimate update.
- Write with a tenant-agnostic context if intentionally replacing an agnostic ('*') plan.
Example fix
// before
var plan = new AlterationPlan { Id = existingId, TenantId = currentTenant };
await planStore.SaveAsync(plan, ct);
// after
var plan = new AlterationPlan { Id = Guid.NewGuid().ToString(), TenantId = currentTenant };
await planStore.SaveAsync(plan, ct); Defensive patterns
Strategy: try-catch
Validate before calling
var existing = await planStore.FindAsync(new AlterationPlanFilter { Id = plan.Id }, ct);
if (existing is not null && existing.TenantId != plan.TenantId)
throw new InvalidOperationException($"Plan ID '{plan.Id}' already exists in another tenant scope; use a new ID."); Try / catch
try
{
await planStore.SaveAsync(plan, ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists"))
{
plan.Id = Guid.NewGuid().ToString();
await planStore.SaveAsync(plan, ct);
} Prevention
- Use auto-generated plan IDs unless importing with a deliberate, conflict-checked ID.
- Verify the tenant context before saving plans in multi-tenant deployments.
- For imports, check for existing IDs first and choose update-vs-create explicitly.
When it happens
Trigger: Calling SaveAsync with an AlterationPlan whose Id matches an existing plan where CanReplace(existing) is false (plan owned by a different tenant, or an agnostic plan being written by a tenant-scoped writer).
Common situations: Importing plans with hardcoded or client-provided IDs into a multi-tenant environment; retrying a save under a different tenant scope; test fixtures reusing the same plan ID across tenants.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- An alteration job with ID
- Cannot overwrite an AI conversation that belongs to another…
- Cannot overwrite an AI conversation that belongs to another…
- A conversation ID is required. (Parameter 'conversation')
- A conversation user ID is required. (Parameter…
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/f38c0d1affc9ca82.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationPlanStore.cs:76
/// <remarks>
/// Ambient tenant is applied here rather than in <see cref="AlterationPlanFilter.Apply"/>.
/// EF owns that via <c>SetTenantIdFilter</c>; Memory must compensate.
/// </remarks>
private IQueryable<AlterationPlan> Filter(IQueryable<AlterationPlan> query, AlterationPlanFilter filter) =>
filter.Apply(query.WhereVisibleToTenant(CurrentTenantId));
private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;
private bool IsVisible(Entity entity) => TenantVisibility.IsVisible(entity.TenantId, CurrentTenantId);
private void EnsureIdAvailable(AlterationPlan plan)
{
var existing = _store.Find(x => x.Id == plan.Id);
if (existing is not null && !CanReplace(existing))
{
throw new InvalidOperationException(
$"An alteration plan with ID '{plan.Id}' already exists and is not visible to the current tenant.");
}
}
/// <summary>
/// <c>*</c> is visible to every tenant, but only an agnostic writer may replace it.
/// Named tenants may upsert their own visible rows.
/// </summary>
private bool CanReplace(Entity existing) =>
existing.TenantId == Tenant.AgnosticTenantId
? CurrentTenantId == Tenant.AgnosticTenantId
: IsVisible(existing);
private void ApplyCurrentTenant(Entity entity)
{
if (entity.TenantId == Tenant.AgnosticTenantId || _tenantAccessor is null)
return;
View on GitHub (pinned to fe9217bdfa)