{"record":{"id":"47e5ffd028a96455","repo":"elsa-workflows/elsa-core","slug":"invitation-verifier-names-and-allowed-actions-are-required","errorCode":null,"errorMessage":"Invitation verifier names and allowed actions are required.","messagePattern":"Invitation verifier names and allowed actions are required\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.UserTasks/Models/UserTaskModels.cs","lineNumber":217,"sourceCode":"\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; }\n    public string WorkflowInstanceId { get; set; } = \"\";\n    /// <summary>A safe, host-authored instance reference (correlation ID or instance name). Never a bookmark or token.</summary>\n    public string? WorkflowInstanceReference { get; set; }\n    public string ActivityInstanceId { get; set; } = \"\";","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs#L199-L235","documentation":"Normalize() requires every invitation in a UserTaskDefinition to have a non-empty VerifierName, at least one AllowedActions entry, and no blank entries inside AllowedActions. Violating any of these throws an ArgumentException naming Invitations. Invitations gate who can act on the task and with which actions, so empty or malformed ones are unusable.","triggerScenarios":"Calling Normalize() on a definition whose Invitations contain an entry with a null/whitespace VerifierName, an empty AllowedActions list, or an AllowedActions list containing null/empty strings — often from deserializing partial JSON or programmatically building invitations without filling all fields.","commonSituations":"Admin UIs that create an invitation row before the user fills fields; bulk imports from CSV/JSON with missing columns; copies of definitions where AllowedActions were cleared.","solutions":["Ensure each invitation has a VerifierName and at least one valid allowed action key before normalizing.","Filter out empty/blank strings from AllowedActions during construction.","Drop invitations that are only partially filled rather than shipping them in the definition."],"exampleFix":"// before\nInvitations = [ new() { VerifierName = \"\", AllowedActions = [] } ]\n// after\nInvitations = [ new() { VerifierName = \"Managers\", AllowedActions = [\"Approve\"] } ]","handlingStrategy":"validation","validationCode":"var bad = definition.Invitations.Where(i =>\n    string.IsNullOrWhiteSpace(i.VerifierName) ||\n    i.AllowedActions.Count == 0 ||\n    i.AllowedActions.Any(string.IsNullOrWhiteSpace)).ToList();\nif (bad.Count > 0) throw new InvalidOperationException(\"Invalid invitations: fill verifier name and allowed actions.\");","typeGuard":null,"tryCatchPattern":"try { definition = definition.Normalize(); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"verifier names\")) { /* prompt user to complete invitations */ }","preventionTips":["Validate invitation forms client-side before submitting","Never persist definitions with half-completed invitations","Centralize invitation construction in a factory that enforces the invariants"],"tags":["validation","user-task","invitations"],"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-16T04:17:20.429Z"}