{"record":{"id":"679f73b55e98d36f","repo":"elsa-workflows/elsa-core","slug":"user-task-action-keys-and-labels-are-required","errorCode":null,"errorMessage":"User Task action keys and labels are required.","messagePattern":"User Task action keys and labels are required\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.UserTasks/Models/UserTaskModels.cs","lineNumber":209,"sourceCode":"    public DateTimeOffset? DueAt { get; init; }\n    public string? Instructions { get; init; }\n    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\");","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs#L191-L227","documentation":"UserTask.Normalize requires every action in the Actions collection to have both a non-whitespace Key and a non-whitespace Label. If any action fails this check, Normalize throws ArgumentException pointing at the Actions parameter. This guarantees persisted tasks always render usable action buttons.","triggerScenarios":"Calling Normalize (directly or via create/save) with an Actions list containing a UserTaskAction whose Key or Label is null, empty, or whitespace.","commonSituations":"Dynamically generated action lists where some entries lack labels; API payloads with incomplete action objects; bulk-imported task definitions with placeholder actions.","solutions":["Ensure every UserTaskAction has both Key and Label set before calling Normalize.","Filter out incomplete actions instead of passing them through.","Validate the incoming DTO action list at the API boundary."],"exampleFix":"// before\ntask.Actions.Add(new UserTaskAction(\"Approve\", null)); // label missing\ntask.Normalize(); // throws\n// after\ntask.Actions.Add(new UserTaskAction(\"Approve\", \"Approve\"));\ntask.Normalize();","handlingStrategy":"validation","validationCode":"var invalid = task.Actions.Where(a => string.IsNullOrWhiteSpace(a.Key) || string.IsNullOrWhiteSpace(a.Label)).ToList();\nif (invalid.Count > 0)\n    return Results.ValidationProblem(new { actions = new[] { \"All action keys and labels are required.\" } });","typeGuard":"bool HasValidActions(UserTask t) =>\n    t.Actions.All(a => !string.IsNullOrWhiteSpace(a.Key) && !string.IsNullOrWhiteSpace(a.Label));","tryCatchPattern":"try\n{\n    task.Normalize();\n}\ncatch (ArgumentException ex) when (ex.ParamName == nameof(UserTask.Actions))\n{\n    return Results.ValidationProblem(new { actions = new[] { ex.Message } });\n}","preventionTips":["Enforce Key+Label in the UI when users add custom actions.","Filter out incomplete actions before persisting.","Reject action payloads at the DTO mapping stage rather than relying on Normalize."],"tags":["usertasks","validation","model"],"backgroundTag":"empty-required-field","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"}