{"record":{"id":"83023b876091a8e6","repo":"elsa-workflows/elsa-core","slug":"user-task-action-keys-must-be-unique","errorCode":null,"errorMessage":"User Task action keys must be unique.","messagePattern":"User Task action keys must be unique\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.UserTasks/Models/UserTaskModels.cs","lineNumber":213,"sourceCode":"    public IReadOnlyCollection<UserTaskAction> Actions { get; init; } = [];\n    public IReadOnlyCollection<UserTaskInvitationDefinition> Invitations { get; init; } = [];\n    public bool EnableTimeoutOutcome { get; init; }\n    public bool EnableCancellationOutcome { get; init; }\n\n    public UserTaskDefinitionSnapshot Normalize()\n    {\n        var actions = Actions.Count == 0\n            ? [new UserTaskAction(\"Complete\", \"Complete\")]\n            : Actions;\n\n        if (string.IsNullOrWhiteSpace(Title))\n            throw new ArgumentException(\"A User Task title is required.\", nameof(Title));\n        if (actions.Any(x => string.IsNullOrWhiteSpace(x.Key) || string.IsNullOrWhiteSpace(x.Label)))\n            throw new ArgumentException(\"User Task action keys and labels are required.\", nameof(Actions));\n        if (actions.Any(x => string.Equals(x.Key, \"Timeout\", StringComparison.OrdinalIgnoreCase) || string.Equals(x.Key, \"Cancelled\", StringComparison.OrdinalIgnoreCase)))\n            throw new ArgumentException(\"Timeout and Cancelled are reserved User Task action keys.\");\n        if (actions.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != actions.Count)\n            throw new ArgumentException(\"User Task action keys must be unique.\");\n        if (Priority is < 0 or > 100)\n            throw new ArgumentOutOfRangeException(nameof(Priority), \"Priority must be between 0 and 100.\");\n        if (Invitations.Any(x => string.IsNullOrWhiteSpace(x.VerifierName) || x.AllowedActions.Count == 0 || x.AllowedActions.Any(string.IsNullOrWhiteSpace)))\n            throw new ArgumentException(\"Invitation verifier names and allowed actions are required.\", nameof(Invitations));\n        if (Invitations.Any(invitation => invitation.AllowedActions.Any(allowed => !actions.Any(action => string.Equals(action.Key, allowed, StringComparison.OrdinalIgnoreCase)))))\n            throw new ArgumentException(\"Invitation actions must be configured User Task actions.\", nameof(Invitations));\n\n        return this with { Actions = actions };\n    }\n}\n\npublic sealed class UserTask\n{\n    public string Id { get; set; } = Guid.NewGuid().ToString(\"N\");\n    public string TenantId { get; set; } = \"\";\n    public string WorkflowDefinitionId { get; set; } = \"\";\n    public string? WorkflowDefinitionName { get; set; }\n    public int? WorkflowDefinitionVersion { get; set; }","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs#L195-L231","documentation":"Normalize() enforces that all User Task action keys are unique, compared case-insensitively (OrdinalIgnoreCase). Duplicate keys would make it ambiguous which action the end user selected, so the definition is rejected with an ArgumentException. Because comparison ignores case, 'Approve' and 'approve' also count as duplicates.","triggerScenarios":"Calling Normalize() on a UserTaskDefinition whose Actions contain two entries with keys differing only in casing, e.g. [{key:'Approve'},{key:'approve'}], or exact duplicates copied from a template.","commonSituations":"Merging action lists from multiple sources (defaults + user additions); case-insensitive UIs that allowed both 'Approve' and 'approve'; copy/paste of action rows.","solutions":["Deduplicate the Actions collection by key (OrdinalIgnoreCase) before normalizing.","Rename one of the colliding actions and update invitations' AllowedActions accordingly.","Add a distinct-count pre-check (as the code does) in your own validation to fail early with a clear message."],"exampleFix":"// before\nActions = [ new() { Key = \"Approve\", Label = \"Approve\" }, new() { Key = \"approve\", Label = \"OK\" } ]\n// after\nActions = [ new() { Key = \"Approve\", Label = \"Approve\" }, new() { Key = \"Ok\", Label = \"OK\" } ]","handlingStrategy":"validation","validationCode":"if (definition.Actions.Select(a => a.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != definition.Actions.Count)\n    throw new InvalidOperationException(\"Duplicate User Task action keys.\");","typeGuard":null,"tryCatchPattern":"try { definition = definition.Normalize(); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"must be unique\")) { /* show which keys collided */ }","preventionTips":["Always build actions through a keyed dictionary before converting to a list","Normalize casing of keys at creation time (e.g. lower-case them)","Write a unit test that round-trips every definition through Normalize()"],"tags":["validation","user-task","duplicate-key"],"backgroundTag":"schema-validation-failed","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}