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

  1. Rename the new label so its normalized name is unique within the tenant
  2. Check existing labels (e.g. via the labels API) before creating one and handle duplicates in your code
  3. Ensure distinct TenantId values are set correctly for tenants, since uniqueness is per-tenant
  4. 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

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


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)