{"record":{"id":"72e52a8457b2da66","repo":"elsa-workflows/elsa-core","slug":"timeout-and-cancelled-are-reserved-user-task-action-keys","errorCode":null,"errorMessage":"Timeout and Cancelled are reserved User Task action keys.","messagePattern":"Timeout and Cancelled are reserved User Task action keys\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.UserTasks/Models/UserTaskModels.cs","lineNumber":211,"sourceCode":"    public JsonElement? TaskData { get; init; }\n    public UserTaskFormReference? FormReference { get; init; }\n    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; } = \"\";","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs#L193-L229","documentation":"Normalize() validates a UserTaskDefinition before materialization and rejects any action whose key is the reserved string 'Timeout' or 'Cancelled' (case-insensitive). These keys are reserved because the runtime generates synthetic Timeout and Cancelled actions for every User Task, and a user-defined action with the same key would collide with them. The exception carries no parameter name, indicating a definition-level rule violation.","triggerScenarios":"Calling Normalize() (directly or via DefaultUserTaskManager.ProjectAsync) on a UserTaskDefinition whose Actions collection contains an entry with Key equal to 'timeout' or 'cancelled' in any casing — e.g. authoring a definition with actions [{key:'Cancel', label:'Cancel'}] is fine, but {key:'Cancelled'} is not.","commonSituations":"Workflow designers modeling a custom cancel button or timeout action on a User Task; migrations from older task models where Cancelled was a plain action key; case-insensitive duplicates like 'TIMEOUT'.","solutions":["Rename the action to a non-reserved key, e.g. 'Cancelled' → 'Reject' or 'Cancel-Request', and update any invitation AllowedActions referencing the old key.","Handle the reserved semantics with built-in timeout/cancellation configuration instead of a custom action.","Add a pre-check on action keys before constructing/normalizing the definition to surface a friendlier error to end users."],"exampleFix":"// before\nnew UserTaskDefinition { Actions = [ new() { Key = \"Cancelled\", Label = \"Cancel\" } ] }\n// after\nnew UserTaskDefinition { Actions = [ new() { Key = \"Reject\", Label = \"Cancel\" } ] }","handlingStrategy":"validation","validationCode":"var reserved = new[]{\"Timeout\",\"Cancelled\"};\nif (definition.Actions.Any(a => reserved.Contains(a.Key, StringComparer.OrdinalIgnoreCase)))\n    throw new InvalidOperationException(\"Action keys 'Timeout'/'Cancelled' are reserved.\");","typeGuard":null,"tryCatchPattern":"try { definition = definition.Normalize(); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"reserved\")) { /* surface friendly message */ }","preventionTips":["Keep a shared constant list of reserved action keys and check it when building task definitions","Validate definitions in your UI/form layer before they reach the runtime","Never map UI button labels 1:1 onto action keys without a key-naming convention"],"tags":["validation","user-task","reserved-key"],"backgroundTag":"invalid-argument-value","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}