elsa-workflows/elsa-core · error · InvalidOperationException

A stored trigger already exists for workflow

Error message

A stored trigger already exists for workflow '{record.WorkflowDefinitionId}', hash '{record.Hash}', activity '{record.ActivityId}', tenant '{record.TenantId}'.

What it means

MemoryTriggerStore ensures each stored trigger has a unique 'logical key' composed of workflow definition id, hash, activity id and tenant id. EnsureLogicalKeyAvailable throws this InvalidOperationException when another trigger with the same logical key but a different Id already exists, preventing duplicate trigger registrations.

Solutions

  1. Update the existing StoredTrigger (reuse its Id) instead of saving a new one for the same workflow/hash/activity/tenant combination.
  2. Delete stale triggers for the workflow definition (e.g. via the trigger store's delete-by-workflow-definition APIs) before re-registering after a publish.
  3. Check trigger hashing/input for the activity so distinct triggers produce distinct hashes (e.g. different cron expressions, route names).
  4. Ensure only one component registers triggers for a workflow; deduplicate in code before SaveManyAsync.

Example fix

// before
var trigger = new StoredTrigger { Id = Guid.NewGuid().ToString(), WorkflowDefinitionId = defId, Hash = hash, ActivityId = activityId };
await triggerStore.SaveAsync(trigger, ct);
// after
var existing = await triggerStore.FindAsync(new TriggerFilter { WorkflowDefinitionId = defId, Hash = hash, ActivityId = activityId }, ct);
if (existing == null) await triggerStore.SaveAsync(trigger, ct);
else { existing.Hash = hash; await triggerStore.SaveAsync(existing, ct); }
Defensive patterns

Strategy: validation

Validate before calling

var logicalKey = (record.WorkflowDefinitionId, record.Hash, record.ActivityId, record.TenantId);
bool exists = (await triggerStore.ListAsync(ct)).Any(t => (t.WorkflowDefinitionId, t.Hash, t.ActivityId, t.TenantId) == logicalKey && t.Id != record.Id);
if (exists) record.Id = /* existing trigger id */;

Try / catch

try { await triggerStore.SaveAsync(record, ct); }
catch (InvalidOperationException) { /* find and update existing trigger for the logical key */ }

Prevention

When it happens

Trigger: Calling SaveAsync or SaveManyAsync on the in-memory trigger store with a StoredTrigger whose (WorkflowDefinitionId, Hash, ActivityId, TenantId) tuple matches an existing stored trigger with a different Id.

Common situations: Re-publishing a workflow that re-registers triggers without removing/updating existing ones; multiple schedules/HTTP triggers hashing identically due to a misconfigured trigger payload; loading the same workflow definition concurrently from two hosts into one shared in-memory store; a trigger id regeneration after workflow clone/import.

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

Appendix: source

Thrown at src/modules/Elsa.Workflows.Runtime/Stores/MemoryTriggerStore.cs:145

    }

    /// <remarks>
    /// Ambient tenant is applied here rather than in <see cref="TriggerFilter.Apply"/>.
    /// EF owns that via <c>SetTenantIdFilter</c> / <c>IgnoreQueryFilters</c>; Memory must compensate.
    /// </remarks>
    private IQueryable<StoredTrigger> Filter(IQueryable<StoredTrigger> queryable, TriggerFilter filter) =>
        filter.Apply(queryable.WhereVisibleToTenant(CurrentTenantId, filter.TenantAgnostic));

    private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;

    private void EnsureLogicalKeyAvailable(StoredTrigger record)
    {
        var logicalKey = GetLogicalKey(record);
        var existing = _store.Find(x => x.Id != record.Id && GetLogicalKey(x) == logicalKey);

        if (existing is not null)
        {
            throw new InvalidOperationException(
                $"A stored trigger already exists for workflow '{record.WorkflowDefinitionId}', hash '{record.Hash}', activity '{record.ActivityId}', tenant '{record.TenantId}'.");
        }
    }

    private List<StoredTrigger> GetMissingLogicalTriggers(ICollection<StoredTrigger> triggers)
    {
        var existingKeys = GetExistingLogicalKeys(triggers);
        return triggers
            .Where(trigger => !existingKeys.Contains(GetLogicalKey(trigger)))
            .ToList();
    }

    private HashSet<TriggerLogicalKey> GetExistingLogicalKeys(ICollection<StoredTrigger> triggers)
    {
        var workflowDefinitionIds = triggers.Select(x => x.WorkflowDefinitionId).Distinct().ToHashSet();
        return _store
            .FindMany(trigger => workflowDefinitionIds.Contains(trigger.WorkflowDefinitionId))
            .Select(GetLogicalKey)

View on GitHub (pinned to fe9217bdfa)