elsa-workflows/elsa-core · error · ArgumentException

Timeout and Cancelled are reserved User Task action keys.

Error message

Timeout and Cancelled are reserved User Task action keys.

What it means

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.

Solutions

  1. Rename the action to a non-reserved key, e.g. 'Cancelled' → 'Reject' or 'Cancel-Request', and update any invitation AllowedActions referencing the old key.
  2. Handle the reserved semantics with built-in timeout/cancellation configuration instead of a custom action.
  3. Add a pre-check on action keys before constructing/normalizing the definition to surface a friendlier error to end users.

Example fix

// before
new UserTaskDefinition { Actions = [ new() { Key = "Cancelled", Label = "Cancel" } ] }
// after
new UserTaskDefinition { Actions = [ new() { Key = "Reject", Label = "Cancel" } ] }
Defensive patterns

Strategy: validation

Validate before calling

var reserved = new[]{"Timeout","Cancelled"};
if (definition.Actions.Any(a => reserved.Contains(a.Key, StringComparer.OrdinalIgnoreCase)))
    throw new InvalidOperationException("Action keys 'Timeout'/'Cancelled' are reserved.");

Try / catch

try { definition = definition.Normalize(); }
catch (ArgumentException ex) when (ex.Message.Contains("reserved")) { /* surface friendly message */ }

Prevention

When it happens

Trigger: 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.

Common situations: 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'.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/72e52a8457b2da66. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.UserTasks/Models/UserTaskModels.cs:211

    public JsonElement? TaskData { get; init; }
    public UserTaskFormReference? FormReference { get; init; }
    public IReadOnlyCollection<UserTaskAction> Actions { get; init; } = [];
    public IReadOnlyCollection<UserTaskInvitationDefinition> Invitations { get; init; } = [];
    public bool EnableTimeoutOutcome { get; init; }
    public bool EnableCancellationOutcome { get; init; }

    public UserTaskDefinitionSnapshot Normalize()
    {
        var actions = Actions.Count == 0
            ? [new UserTaskAction("Complete", "Complete")]
            : Actions;

        if (string.IsNullOrWhiteSpace(Title))
            throw new ArgumentException("A User Task title is required.", nameof(Title));
        if (actions.Any(x => string.IsNullOrWhiteSpace(x.Key) || string.IsNullOrWhiteSpace(x.Label)))
            throw new ArgumentException("User Task action keys and labels are required.", nameof(Actions));
        if (actions.Any(x => string.Equals(x.Key, "Timeout", StringComparison.OrdinalIgnoreCase) || string.Equals(x.Key, "Cancelled", StringComparison.OrdinalIgnoreCase)))
            throw new ArgumentException("Timeout and Cancelled are reserved User Task action keys.");
        if (actions.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != actions.Count)
            throw new ArgumentException("User Task action keys must be unique.");
        if (Priority is < 0 or > 100)
            throw new ArgumentOutOfRangeException(nameof(Priority), "Priority must be between 0 and 100.");
        if (Invitations.Any(x => string.IsNullOrWhiteSpace(x.VerifierName) || x.AllowedActions.Count == 0 || x.AllowedActions.Any(string.IsNullOrWhiteSpace)))
            throw new ArgumentException("Invitation verifier names and allowed actions are required.", nameof(Invitations));
        if (Invitations.Any(invitation => invitation.AllowedActions.Any(allowed => !actions.Any(action => string.Equals(action.Key, allowed, StringComparison.OrdinalIgnoreCase)))))
            throw new ArgumentException("Invitation actions must be configured User Task actions.", nameof(Invitations));

        return this with { Actions = actions };
    }
}

public sealed class UserTask
{
    public string Id { get; set; } = Guid.NewGuid().ToString("N");
    public string TenantId { get; set; } = "";
    public string WorkflowDefinitionId { get; set; } = "";

View on GitHub (pinned to fe9217bdfa)