{"record":{"id":"5a3817f5b1a393dd","repo":"elsa-workflows/elsa-core","slug":"a-stored-trigger-already-exists-for-workflow-record","errorCode":null,"errorMessage":"A stored trigger already exists for workflow '{record.WorkflowDefinitionId}', hash '{record.Hash}', activity '{record.ActivityId}', tenant '{record.TenantId}'.","messagePattern":"A stored trigger already exists for workflow '(.+?)', hash '(.+?)', activity '(.+?)', tenant '(.+?)'\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Workflows.Runtime/Stores/MemoryTriggerStore.cs","lineNumber":145,"sourceCode":"    }\n\n    /// <remarks>\n    /// Ambient tenant is applied here rather than in <see cref=\"TriggerFilter.Apply\"/>.\n    /// EF owns that via <c>SetTenantIdFilter</c> / <c>IgnoreQueryFilters</c>; Memory must compensate.\n    /// </remarks>\n    private IQueryable<StoredTrigger> Filter(IQueryable<StoredTrigger> queryable, TriggerFilter filter) =>\n        filter.Apply(queryable.WhereVisibleToTenant(CurrentTenantId, filter.TenantAgnostic));\n\n    private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;\n\n    private void EnsureLogicalKeyAvailable(StoredTrigger record)\n    {\n        var logicalKey = GetLogicalKey(record);\n        var existing = _store.Find(x => x.Id != record.Id && GetLogicalKey(x) == logicalKey);\n\n        if (existing is not null)\n        {\n            throw new InvalidOperationException(\n                $\"A stored trigger already exists for workflow '{record.WorkflowDefinitionId}', hash '{record.Hash}', activity '{record.ActivityId}', tenant '{record.TenantId}'.\");\n        }\n    }\n\n    private List<StoredTrigger> GetMissingLogicalTriggers(ICollection<StoredTrigger> triggers)\n    {\n        var existingKeys = GetExistingLogicalKeys(triggers);\n        return triggers\n            .Where(trigger => !existingKeys.Contains(GetLogicalKey(trigger)))\n            .ToList();\n    }\n\n    private HashSet<TriggerLogicalKey> GetExistingLogicalKeys(ICollection<StoredTrigger> triggers)\n    {\n        var workflowDefinitionIds = triggers.Select(x => x.WorkflowDefinitionId).Distinct().ToHashSet();\n        return _store\n            .FindMany(trigger => workflowDefinitionIds.Contains(trigger.WorkflowDefinitionId))\n            .Select(GetLogicalKey)","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Workflows.Runtime/Stores/MemoryTriggerStore.cs#L127-L163","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvar trigger = new StoredTrigger { Id = Guid.NewGuid().ToString(), WorkflowDefinitionId = defId, Hash = hash, ActivityId = activityId };\nawait triggerStore.SaveAsync(trigger, ct);\n// after\nvar existing = await triggerStore.FindAsync(new TriggerFilter { WorkflowDefinitionId = defId, Hash = hash, ActivityId = activityId }, ct);\nif (existing == null) await triggerStore.SaveAsync(trigger, ct);\nelse { existing.Hash = hash; await triggerStore.SaveAsync(existing, ct); }","handlingStrategy":"validation","validationCode":"var logicalKey = (record.WorkflowDefinitionId, record.Hash, record.ActivityId, record.TenantId);\nbool exists = (await triggerStore.ListAsync(ct)).Any(t => (t.WorkflowDefinitionId, t.Hash, t.ActivityId, t.TenantId) == logicalKey && t.Id != record.Id);\nif (exists) record.Id = /* existing trigger id */;","typeGuard":null,"tryCatchPattern":"try { await triggerStore.SaveAsync(record, ct); }\ncatch (InvalidOperationException) { /* find and update existing trigger for the logical key */ }","preventionTips":["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."],"tags":["duplicate-record","trigger","in-memory-store","uniqueness-constraint"],"backgroundTag":"file-already-exists","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}