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

  1. Use a fresh unique plan ID for the new plan.
  2. Align the current tenant context with the existing plan's tenant to perform a legitimate update.
  3. 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

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


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)