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
- Generate a new unique job ID instead of reusing the conflicting one.
- Ensure the tenant context matches the existing record's tenant when an update is intended.
- 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
- Prefer store-generated GUIDs over client-supplied job IDs.
- Keep tenant scope consistent for the lifetime of a save operation.
- In multi-tenant tests, derive IDs per tenant rather than sharing constants.
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
- An alteration plan 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/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)