elsa-workflows/elsa-core · error · InvalidOperationException
A label already exists with normalized name
Error message
A label already exists with normalized name '{record.NormalizedName}' in tenant '{record.TenantId}'. What it means
InMemoryLabelStore enforces that each label has a unique NormalizedName per tenant. EnsureNormalizedNameAvailable is called by SaveAsync/SaveManyAsync and throws a DuplicateNormalizedName exception when another stored label (or another record in the same save batch) already uses the same normalized name in the same tenant. Normalization makes names case/format-insensitive, so 'Bug' and 'bug' collide.
Solutions
- Rename the new label so its normalized name is unique within the tenant
- Check existing labels (e.g. via the labels API) before creating one and handle duplicates in your code
- Ensure distinct TenantId values are set correctly for tenants, since uniqueness is per-tenant
- When using SaveManyAsync, de-duplicate the batch by NormalizedName before saving
Example fix
// before
await store.SaveAsync(new Label { Name = "Production" }, cancellationToken); // 'production' already exists
// after
var existing = (await store.ListAsync(cancellationToken)).Any(l => l.NormalizedName == "production");
if (!existing) await store.SaveAsync(new Label { Name = "Production" }, cancellationToken); Defensive patterns
Strategy: validation
Validate before calling
var existing = await store.ListAsync(cancellationToken);
bool duplicate = existing.Any(l => l.TenantId == label.TenantId && l.Id != label.Id && l.NormalizedName == label.NormalizedName);
if (duplicate) throw new InvalidOperationException($"Label '{label.NormalizedName}' already exists"); Type guard
bool IsNameAvailable(IEnumerable<Label> labels, Label candidate) => !labels.Any(l => l.TenantId == candidate.TenantId && l.Id != candidate.Id && l.NormalizedName == candidate.NormalizedName);
Try / catch
try
{
await store.SaveAsync(label, cancellationToken);
}
catch (DuplicateNormalizedNameException ex)
{
logger.LogWarning("Label name '{Name}' already in use in tenant '{Tenant}'.", ex.Record.NormalizedName, ex.Record.TenantId);
// surface a 409-style conflict to the caller
} Prevention
- Normalize and de-duplicate label names before saving
- Check for existing labels via the labels API before creating
- De-duplicate records by NormalizedName within SaveManyAsync batches
- Ensure TenantId is consistently populated in multi-tenant setups
When it happens
Trigger: Creating or updating a label whose NormalizedName equals an existing label's in the same tenant (excluding the record itself); also saving multiple labels in one SaveManyAsync call where two records normalize to the same name.
Common situations: Importing label seeds or fixtures that contain duplicates; API clients posting labels whose names differ only by case/whitespace; multi-tenant setups where tenantId is accidentally identical or empty for all records.
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
- A already exists with ' ' in tenant ' '.
- 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/3e3a294561576854.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs:178
private static void SyncNormalizedName(Label record) =>
record.NormalizedName = record.Name.ToLowerInvariant();
/// <summary>
/// Memory counterpart of the EF unique index on <c>(TenantId, NormalizedName)</c>.
/// Same-Id upserts are allowed so a row can rename itself. Incoming batch rows
/// replace same-Id store rows, so those store rows are ignored here.
/// </summary>
private void EnsureNormalizedNameAvailable(Label record, IReadOnlyCollection<Label> batch)
{
var batchIds = batch.Select(x => x.Id).ToHashSet();
var existing = _labelStore.Find(candidate =>
candidate.TenantId == record.TenantId
&& candidate.Id != record.Id
&& !batchIds.Contains(candidate.Id)
&& candidate.NormalizedName == record.NormalizedName);
if (existing is not null)
throw DuplicateNormalizedName(record);
if (batch.Any(other =>
other.Id != record.Id
&& other.TenantId == record.TenantId
&& other.NormalizedName == record.NormalizedName))
{
throw DuplicateNormalizedName(record);
}
}
private static InvalidOperationException DuplicateNormalizedName(Label record) =>
new($"A label already exists with normalized name '{record.NormalizedName}' in tenant '{record.TenantId}'.");
}
View on GitHub (pinned to fe9217bdfa)