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
- Update the existing StoredTrigger (reuse its Id) instead of saving a new one for the same workflow/hash/activity/tenant combination.
- 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.
- Check trigger hashing/input for the activity so distinct triggers produce distinct hashes (e.g. different cron expressions, route names).
- 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
- Delete stale triggers for a workflow definition before re-publishing.
- Deduplicate triggers by (workflow, hash, activity, tenant) before SaveManyAsync.
- Ensure trigger inputs are configured so distinct triggers hash distinctly.
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
- A bookmark queue dead-letter item for original queue item
- A conversation ID is required. (Parameter 'conversation')
- A conversation user ID is required. (Parameter…
- A already exists with ' ' in tenant ' '.
- A label already exists with normalized name
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)