elsa-workflows/elsa-core · error · InvalidOperationException

An alteration job with ID

Error message

An alteration job with ID '{job.Id}' already exists and is not visible to the current tenant.

What it means

MemoryAlterationJobStore.EnsureIdAvailable throws when saving an alteration job whose ID already exists in the in-memory store but cannot be replaced because the existing entry is not visible to the current tenant. Tenant-agnostic entries ('*') are visible to everyone but only an agnostic writer may replace them, so a tenant-scoped writer colliding with such an entry (or another tenant's entry) triggers this InvalidOperationException.

Solutions

  1. Generate a new unique job ID instead of reusing the conflicting one.
  2. Ensure the tenant context matches the existing record's tenant when an update is intended.
  3. If replacing an agnostic ('*') record is intended, perform the write with a tenant-agnostic writer context.

Example fix

// before
var job = new AlterationJob { Id = fixedId, TenantId = currentTenant };
await jobStore.SaveAsync(job, ct);

// after
var job = new AlterationJob { Id = Guid.NewGuid().ToString(), TenantId = currentTenant };
await jobStore.SaveAsync(job, ct);
Defensive patterns

Strategy: try-catch

Validate before calling

var existing = await jobStore.FindAsync(new AlterationJobFilter { Id = job.Id }, ct);
if (existing is not null && existing.TenantId != job.TenantId)
    throw new InvalidOperationException($"Job ID '{job.Id}' already exists in another tenant scope; use a new ID.");

Try / catch

try
{
    await jobStore.SaveAsync(job, ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists"))
{
    job.Id = Guid.NewGuid().ToString();
    await jobStore.SaveAsync(job, ct);
}

Prevention

When it happens

Trigger: Calling SaveAsync or SaveManyAsync with an AlterationJob whose Id matches an existing job that CanReplace(existing) evaluates false for (existing belongs to a different tenant, or existing is tenant-agnostic and the writer is tenant-scoped).

Common situations: Re-running job creation with a client-supplied or persisted deterministic ID after a tenant context change; seeding multi-tenant data where one tenant's ID collides with an agnostic record; tests reusing fixed job IDs across tenant scopes.

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/b9d56cc35c096746. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationJobStore.cs:109

    /// <remarks>
    /// Ambient tenant is applied here rather than in <see cref="AlterationJobFilter.Apply"/>.
    /// EF owns that via <c>SetTenantIdFilter</c>; Memory must compensate.
    /// </remarks>
    private IQueryable<AlterationJob> Filter(IQueryable<AlterationJob> query, AlterationJobFilter 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(AlterationJob job)
    {
        var existing = _store.Find(x => x.Id == job.Id);

        if (existing is not null && !CanReplace(existing))
        {
            throw new InvalidOperationException(
                $"An alteration job with ID '{job.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)