< Summary

Information
Class: Elsa.UserTasks.Models.UserTaskEvent
Assembly: Elsa.UserTasks
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs
Line coverage
100%
Covered lines: 11
Uncovered lines: 0
Coverable lines: 11
Total lines: 686
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Id()100%11100%
get_TenantId()100%11100%
get_TaskId()100%11100%
get_Revision()100%11100%
get_EventType()100%11100%
get_OccurredAt()100%11100%
get_Actor()100%11100%
get_OperationId()100%11100%
get_Reason()100%11100%
get_Metadata()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/Models/UserTaskModels.cs

#LineLine coverage
 1using System.Security.Claims;
 2using System.Text.Json;
 3using Elsa.Authorization;
 4using Elsa.Mediator.Contracts;
 5
 6namespace Elsa.UserTasks.Models;
 7
 8public enum UserTaskParticipantType
 9{
 10    User,
 11    Group
 12}
 13
 14public enum UserTaskMembershipResolutionMode
 15{
 16    Live,
 17    Snapshot
 18}
 19
 20public enum UserTaskStatus
 21{
 22    Unassigned,
 23    Available,
 24    Assigned,
 25    Completing,
 26    TimingOut,
 27    Cancelling,
 28    Completed,
 29    TimedOut,
 30    Cancelled
 31}
 32
 33public enum UserTaskHealthSeverity
 34{
 35    Advisory,
 36    Blocking
 37}
 38
 39public enum UserTaskAccessOperation
 40{
 41    ReadSummary,
 42    ReadProtected,
 43    Claim,
 44    Release,
 45    Assign,
 46    UpdateScheduling,
 47    Complete,
 48    Cancel,
 49    Manage,
 50    IssueInvitation,
 51    RetryResolution
 52}
 53
 54public enum UserTaskOperationKind
 55{
 56    Claim,
 57    Release,
 58    Assign,
 59    ScheduleUpdate,
 60    Complete,
 61    Timeout,
 62    Cancel,
 63    RetryResolution,
 64    InvitationVerification
 65}
 66
 67public enum UserTaskOperationStatus
 68{
 69    Accepted,
 70    Completed,
 71    Failed
 72}
 73
 74public enum UserTaskInvitationStatus
 75{
 76    Pending,
 77    Dispatched,
 78    Verified,
 79    Consumed,
 80    Revoked,
 81    Expired
 82}
 83
 84public sealed record ParticipantReference(
 85    string TenantId,
 86    string Provider,
 87    UserTaskParticipantType Type,
 88    string Id,
 89    string? DisplayName = null)
 90{
 91    public bool Matches(ParticipantReference? other) => other != null
 92        && string.Equals(TenantId, other.TenantId, StringComparison.Ordinal)
 93        && string.Equals(Provider, other.Provider, StringComparison.Ordinal)
 94        && Type == other.Type
 95        && string.Equals(Id, other.Id, StringComparison.Ordinal);
 96}
 97
 98public sealed record UserTaskActor(
 99    ParticipantReference Subject,
 100    IReadOnlyCollection<ParticipantReference> Groups,
 101    string? DisplayName = null)
 102{
 103    /// <summary>Indicates that the host has granted tenant-scoped manager access.</summary>
 104    public bool IsManager { get; init; }
 105    public IReadOnlySet<string> Permissions { get; init; } = new HashSet<string>(StringComparer.Ordinal);
 106
 107    /// <summary>
 108    /// Set when the caller authenticated with a guest invitation session. A guest is scoped to exactly
 109    /// one task; every policy decision for any other task must deny.
 110    /// </summary>
 111    public string? GuestTaskId { get; init; }
 112
 113    /// <summary>The completion action keys a guest session was issued for. Empty for ordinary actors.</summary>
 114    public IReadOnlySet<string> GuestAllowedActions { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCas
 115
 116    public bool IsGuest => GuestTaskId != null;
 117
 118    /// <summary>Whether a grant this actor holds satisfies <paramref name="required"/>.</summary>
 119    /// <remarks>
 120    /// Matched through <see cref="PermissionMatcher"/> rather than by string equality, so a subtree or verb
 121    /// wildcard reaches this check exactly as it reaches an endpoint's own gate. Comparing strings would
 122    /// leave the two disagreeing: the endpoint would admit a caller holding <c>user-tasks:*</c> and the
 123    /// policy would then deny them, which reads as a broken task rather than as a missing grant. A bare
 124    /// <c>*</c> keeps working because it parses as <c>*:*</c>, not because it is special-cased here.
 125    /// </remarks>
 126    public bool HasPermission(Permission required) =>
 127        Permissions.Any(x => Permission.TryParse(x, out var granted) && PermissionMatcher.Satisfies(granted, required));
 128
 129    /// <inheritdoc cref="HasPermission(Permission)" />
 130    public bool HasPermission(string resource, string verb) => HasPermission(new Permission(resource, verb));
 131}
 132
 133public sealed record UserTaskAction(
 134    string Key,
 135    string Label,
 136    IReadOnlyDictionary<string, object?>? Metadata = null);
 137
 138/// <summary>
 139/// Describes a guest invitation that is materialized with a task. The secret itself is created only
 140/// by Core at runtime and is never part of this definition.
 141/// </summary>
 142public sealed record UserTaskInvitationDefinition(
 143    string VerifierName,
 144    IReadOnlyCollection<string> AllowedActions,
 145    TimeSpan? Lifetime = null,
 146    bool BearerOnly = false,
 147    string? Recipient = null,
 148    IReadOnlyDictionary<string, object?>? Configuration = null);
 149
 150public sealed record UserTaskFormReference(
 151    string ProviderName,
 152    string Key,
 153    string? Binding = null,
 154    string? Version = null);
 155
 156public sealed record ResolvedUserTaskForm(
 157    UserTaskFormReference Requested,
 158    string PinnedVersion,
 159    IReadOnlyDictionary<string, object?> Metadata)
 160{
 161    /// <summary>
 162    /// Provider-neutral field descriptors used to render the response surface. Descriptors carry shape and
 163    /// disclosure flags only; values are read from the task payload under the protected-access decision.
 164    /// </summary>
 165    public IReadOnlyCollection<UserTaskFormFieldDescriptor> Fields { get; init; } = [];
 166}
 167
 168public sealed record UserTaskFormFieldDescriptor(
 169    string Key,
 170    string Label,
 171    string Type = "text",
 172    bool Required = false,
 173    bool Masked = false,
 174    bool CanReveal = false);
 175
 176public sealed record UserTaskDefinitionSnapshot
 177{
 178    public string Title { get; init; } = "";
 179    public string? Summary { get; init; }
 180    public string? Reference { get; init; }
 181    public IReadOnlyCollection<string> Tags { get; init; } = [];
 182    public string? TaskType { get; init; }
 183    public ParticipantReference? Requester { get; init; }
 184    public ParticipantReference? Assignee { get; init; }
 185    public IReadOnlyCollection<ParticipantReference> CandidateUsers { get; init; } = [];
 186    public IReadOnlyCollection<ParticipantReference> CandidateGroups { get; init; } = [];
 187    public IReadOnlyCollection<ParticipantReference> ExcludedUsers { get; init; } = [];
 188    public UserTaskMembershipResolutionMode MembershipResolutionMode { get; init; } = UserTaskMembershipResolutionMode.L
 189    public bool AllowManagerExclusionOverride { get; init; }
 190    public int Priority { get; init; } = 50;
 191    public DateTimeOffset? DueAt { get; init; }
 192    public string? Instructions { get; init; }
 193    public JsonElement? TaskData { get; init; }
 194    public UserTaskFormReference? FormReference { get; init; }
 195    public IReadOnlyCollection<UserTaskAction> Actions { get; init; } = [];
 196    public IReadOnlyCollection<UserTaskInvitationDefinition> Invitations { get; init; } = [];
 197    public bool EnableTimeoutOutcome { get; init; }
 198    public bool EnableCancellationOutcome { get; init; }
 199
 200    public UserTaskDefinitionSnapshot Normalize()
 201    {
 202        var actions = Actions.Count == 0
 203            ? [new UserTaskAction("Complete", "Complete")]
 204            : Actions;
 205
 206        if (string.IsNullOrWhiteSpace(Title))
 207            throw new ArgumentException("A User Task title is required.", nameof(Title));
 208        if (actions.Any(x => string.IsNullOrWhiteSpace(x.Key) || string.IsNullOrWhiteSpace(x.Label)))
 209            throw new ArgumentException("User Task action keys and labels are required.", nameof(Actions));
 210        if (actions.Any(x => string.Equals(x.Key, "Timeout", StringComparison.OrdinalIgnoreCase) || string.Equals(x.Key,
 211            throw new ArgumentException("Timeout and Cancelled are reserved User Task action keys.");
 212        if (actions.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != actions.Count)
 213            throw new ArgumentException("User Task action keys must be unique.");
 214        if (Priority is < 0 or > 100)
 215            throw new ArgumentOutOfRangeException(nameof(Priority), "Priority must be between 0 and 100.");
 216        if (Invitations.Any(x => string.IsNullOrWhiteSpace(x.VerifierName) || x.AllowedActions.Count == 0 || x.AllowedAc
 217            throw new ArgumentException("Invitation verifier names and allowed actions are required.", nameof(Invitation
 218        if (Invitations.Any(invitation => invitation.AllowedActions.Any(allowed => !actions.Any(action => string.Equals(
 219            throw new ArgumentException("Invitation actions must be configured User Task actions.", nameof(Invitations))
 220
 221        return this with { Actions = actions };
 222    }
 223}
 224
 225public sealed class UserTask
 226{
 227    public string Id { get; set; } = Guid.NewGuid().ToString("N");
 228    public string TenantId { get; set; } = "";
 229    public string WorkflowDefinitionId { get; set; } = "";
 230    public string? WorkflowDefinitionName { get; set; }
 231    public int? WorkflowDefinitionVersion { get; set; }
 232    public string WorkflowInstanceId { get; set; } = "";
 233    /// <summary>A safe, host-authored instance reference (correlation ID or instance name). Never a bookmark or token.<
 234    public string? WorkflowInstanceReference { get; set; }
 235    public string ActivityInstanceId { get; set; } = "";
 236    public string BookmarkId { get; set; } = "";
 237    public string MaterializationKey { get; set; } = "";
 238    public string Title { get; set; } = "User task";
 239    public string? Summary { get; set; }
 240    public string? Reference { get; set; }
 241    public HashSet<string> Tags { get; set; } = new(StringComparer.OrdinalIgnoreCase);
 242    public string? TaskType { get; set; }
 243    public ParticipantReference? Requester { get; set; }
 244    public ParticipantReference? Assignee { get; set; }
 245    public List<ParticipantReference> CandidateUsers { get; set; } = [];
 246    public List<ParticipantReference> CandidateGroups { get; set; } = [];
 247    public List<ParticipantReference> SnapshotMembers { get; set; } = [];
 248    public List<ParticipantReference> SnapshotGroups { get; set; } = [];
 249    public List<ParticipantReference> ExcludedUsers { get; set; } = [];
 250    public UserTaskMembershipResolutionMode MembershipResolutionMode { get; set; } = UserTaskMembershipResolutionMode.Li
 251    public bool AllowManagerExclusionOverride { get; set; }
 252    public int Priority { get; set; } = 50;
 253    public DateTimeOffset? DueAt { get; set; }
 254    public bool IsOverdue { get; set; }
 255    public string? Instructions { get; set; }
 256    public JsonElement? TaskData { get; set; }
 257    public UserTaskFormReference? RequestedForm { get; set; }
 258    public ResolvedUserTaskForm? PinnedForm { get; set; }
 259    public List<UserTaskAction> Actions { get; set; } = [new("Complete", "Complete")];
 260    public List<UserTaskInvitationDefinition> InvitationDefinitions { get; set; } = [];
 261    public bool EnableTimeoutOutcome { get; set; }
 262    public bool EnableCancellationOutcome { get; set; }
 263    public UserTaskStatus Status { get; set; } = UserTaskStatus.Available;
 264    public UserTaskHealthSeverity? HealthSeverity { get; set; }
 265    public string? HealthCode { get; set; }
 266    public string? HealthMessage { get; set; }
 267    public string? CompletionActionKey { get; set; }
 268    public JsonElement? CompletionData { get; set; }
 269    public ParticipantReference? CompletedBy { get; set; }
 270    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
 271    public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
 272    public DateTimeOffset? AssignedAt { get; set; }
 273    public DateTimeOffset? CompletedAt { get; set; }
 274    public int Revision { get; set; } = 1;
 275    public List<UserTaskEvent> Events { get; set; } = [];
 276    public List<UserTaskOperation> Operations { get; set; } = [];
 277    public List<UserTaskInvitation> Invitations { get; set; } = [];
 278
 279    public bool IsTerminal => Status is UserTaskStatus.Completed or UserTaskStatus.TimedOut or UserTaskStatus.Cancelled;
 280
 281    public bool IsOpen => !IsTerminal;
 282}
 283
 247284public sealed record UserTaskEvent(
 59285    string Id,
 26286    string TenantId,
 26287    string TaskId,
 46288    int Revision,
 31289    string EventType,
 30290    DateTimeOffset OccurredAt,
 216291    ParticipantReference? Actor = null,
 26292    string? OperationId = null,
 28293    string? Reason = null,
 273294    IReadOnlyDictionary<string, object?>? Metadata = null);
 295
 296public sealed record UserTaskOperation(
 297    string Id,
 298    string TenantId,
 299    string TaskId,
 300    string OperationId,
 301    UserTaskOperationKind Kind,
 302    int ExpectedRevision,
 303    string RequestHash,
 304    UserTaskOperationStatus Status,
 305    DateTimeOffset CreatedAt,
 306    DateTimeOffset UpdatedAt,
 307    string? ActionKey = null,
 308    JsonElement? Data = null,
 309    string? ErrorCode = null);
 310
 311public sealed record UserTaskInvitation(
 312    string Id,
 313    string TenantId,
 314    string TaskId,
 315    string? Recipient,
 316    string TokenHash,
 317    UserTaskInvitationStatus Status,
 318    DateTimeOffset IssuedAt,
 319    DateTimeOffset ExpiresAt,
 320    string? VerifierName = null,
 321    DateTimeOffset? VerifiedAt = null,
 322    DateTimeOffset? ConsumedAt = null,
 323    DateTimeOffset? RevokedAt = null,
 324    string? SiblingGroupId = null)
 325{
 326    /// <summary>
 327    /// The completion actions this invitation was issued for. It is copied from the activity's invitation
 328    /// definition at issuance so a later definition change cannot widen an outstanding guest link.
 329    /// </summary>
 330    public IReadOnlyCollection<string> AllowedActions { get; init; } = [];
 331}
 332
 333public sealed record UserTaskInvitationDelivery(
 334    string Id,
 335    string TenantId,
 336    string TaskId,
 337    string InvitationId,
 338    string DispatcherName,
 339    string Token,
 340    DateTimeOffset ExpiresAt)
 341{
 342    public string? Recipient { get; init; }
 343    public int Attempt { get; init; }
 344    public DateTimeOffset? NotBefore { get; init; }
 345}
 346
 347public sealed record UserTaskInvitationChallenge(string Token, string? Code = null, string? State = null);
 348
 349/// <summary>
 350/// A deliberately uniform challenge descriptor. The same shape is returned for valid and invalid tokens so
 351/// the anonymous surface never becomes an invitation-existence oracle.
 352/// </summary>
 353public sealed record UserTaskInvitationChallengeDescriptor(string ChallengeType, string Prompt, bool RequiresCode);
 354
 355public sealed record UserTaskInvitationVerificationResult(bool Succeeded, string? FailureCode = null, string? Subject = 
 356
 357public sealed record GuestSessionResult(bool Succeeded, string? Token = null, DateTimeOffset? ExpiresAt = null, string? 
 358
 359/// <summary>A resolved guest session. It authorizes exactly one task and one set of completion actions.</summary>
 360public sealed record UserTaskGuestSession(
 361    string TenantId,
 362    string TaskId,
 363    string InvitationId,
 364    ParticipantReference Subject,
 365    IReadOnlyCollection<string> AllowedActions,
 366    DateTimeOffset ExpiresAt);
 367
 368public sealed record UserTaskResult(
 369    string ActionKey,
 370    JsonElement? Data,
 371    ParticipantReference? CompletedBy,
 372    DateTimeOffset CompletedAt);
 373
 374public sealed record UserTaskInvitationSummary(
 375    string Id,
 376    string TaskId,
 377    string? Recipient,
 378    UserTaskInvitationStatus Status,
 379    DateTimeOffset IssuedAt,
 380    DateTimeOffset ExpiresAt,
 381    string? VerifierName);
 382
 383public sealed record UserTaskInvitationIssueRequest(
 384    int ExpectedRevision,
 385    string VerifierName,
 386    IReadOnlyCollection<string> AllowedActions,
 387    string? Recipient = null,
 388    TimeSpan? Lifetime = null,
 389    string? OperationId = null);
 390
 391public sealed record UserTaskInvitationIssueResult(
 392    UserTaskInvitationSummary Invitation,
 393    string? OperationId = null);
 394
 395public sealed record UserTaskInvitationVerificationResultWithSession(
 396    bool Succeeded,
 397    string? TaskId = null,
 398    string? SessionToken = null,
 399    DateTimeOffset? ExpiresAt = null,
 400    string? FailureCode = null);
 401
 402public sealed record UserTaskMaterialization(
 403    string TenantId,
 404    string WorkflowDefinitionId,
 405    string WorkflowInstanceId,
 406    string ActivityInstanceId,
 407    string BookmarkId,
 408    UserTaskDefinitionSnapshot Definition,
 409    IReadOnlyCollection<ParticipantReference> SnapshotMembers,
 410    IReadOnlyCollection<ParticipantReference> SnapshotGroups,
 411    DateTimeOffset CreatedAt,
 412    string? TaskId = null,
 413    string? WorkflowDefinitionName = null,
 414    int? WorkflowDefinitionVersion = null,
 415    string? WorkflowInstanceReference = null);
 416
 417public sealed record UserTaskStimulus(
 418    string TenantId,
 419    string TaskId,
 420    string OperationId,
 421    string ActionKey,
 422    JsonElement? CompletionData,
 423    ParticipantReference? CompletedBy = null,
 424    DateTimeOffset? CompletedAt = null,
 425    string? BookmarkId = null);
 426
 427public sealed record UserTaskBookmarkRemoval(
 428    string TenantId,
 429    string TaskId,
 430    string BookmarkId,
 431    DateTimeOffset RemovedAt);
 432
 433public sealed record UserTaskMutationRequest(int ExpectedRevision, string? OperationId = null);
 434
 435public sealed record UserTaskAssignRequest(
 436    int ExpectedRevision,
 437    ParticipantReference Assignee,
 438    string? Reason = null,
 439    string? OperationId = null);
 440
 441public sealed record UserTaskSchedulingUpdate(
 442    int ExpectedRevision,
 443    int? Priority = null,
 444    DateTimeOffset? DueAt = null,
 445    string? OperationId = null);
 446
 447public sealed record UserTaskCompletionRequest(
 448    int ExpectedRevision,
 449    string OperationId,
 450    string ActionKey,
 451    JsonElement? Data = null);
 452
 453public sealed record UserTaskCancelRequest(int ExpectedRevision, string OperationId, string Reason);
 454
 455/// <summary>
 456/// The caller-selected list scope. It is part of the authorization predicate, not a display filter:
 457/// <see cref="All"/> and <see cref="NeedsAttention"/> require tenant-scoped manager access.
 458/// </summary>
 459public enum UserTaskQueryScopeKind
 460{
 461    Assigned,
 462    Available,
 463    History,
 464    All,
 465    NeedsAttention
 466}
 467
 468public sealed record UserTaskQueryScope(
 469    string TenantId,
 470    ParticipantReference Subject,
 471    IReadOnlyCollection<ParticipantReference> Groups,
 472    bool IsManager = false,
 473    UserTaskQueryScopeKind Kind = UserTaskQueryScopeKind.Assigned,
 474    bool ExcludeBlocking = true)
 475{
 476    public bool IncludeAssigned => Kind is UserTaskQueryScopeKind.Assigned;
 477    public bool IncludeCandidates => Kind is UserTaskQueryScopeKind.Available;
 478    public bool IncludeHistory => Kind is UserTaskQueryScopeKind.History;
 479    public bool RequiresManager => Kind is UserTaskQueryScopeKind.All or UserTaskQueryScopeKind.NeedsAttention;
 480}
 481
 482public sealed record UserTaskQuery
 483{
 484    public string TenantId { get; init; } = "";
 485    public UserTaskQueryScope? Scope { get; init; }
 486    public string? Cursor { get; init; }
 487    public int Limit { get; init; } = 50;
 488    public string? Search { get; init; }
 489    public IReadOnlyCollection<UserTaskStatus> Statuses { get; init; } = [];
 490    /// <summary>Restricts the page to tasks whose due date has already elapsed.</summary>
 491    public bool OnlyOverdue { get; init; }
 492    /// <summary>Restricts the page to tasks that carry no due date at all.</summary>
 493    public bool OnlyWithoutDueDate { get; init; }
 494    public string? TaskType { get; init; }
 495    public int? PriorityFrom { get; init; }
 496    public int? PriorityTo { get; init; }
 497    public DateTimeOffset? DueFrom { get; init; }
 498    public DateTimeOffset? DueTo { get; init; }
 499    public string? WorkflowDefinitionId { get; init; }
 500    public string? WorkflowInstanceId { get; init; }
 501    public string? Reference { get; init; }
 502    public string Sort { get; init; } = "created";
 503    public bool Descending { get; init; }
 504    public bool IncludeTotalCount { get; init; }
 505}
 506
 507public sealed record UserTaskQueryResult(
 508    IReadOnlyCollection<UserTask> Items,
 509    string? NextCursor,
 510    int? TotalCount);
 511
 512public sealed record UserTaskProjectionResult(UserTask Task, bool Created);
 513
 514public sealed record UserTaskReconciliationRequest(int PageSize = 100, DateTimeOffset? OlderThan = null)
 515{
 516    /// <summary>Runs a tenant-scoped pass. Hosts with a tenant catalog can invoke one pass per tenant.</summary>
 517    public string TenantId { get; init; } = "";
 518}
 519
 520public sealed record UserTaskReconciliationResult(int Recreated, int Requeued, int Finalized, int Ambiguous);
 521
 522public sealed record UserTaskOperationResult(
 523    UserTask Task,
 524    UserTaskOperation Operation,
 525    bool Accepted,
 526    string? ConflictCode = null);
 527
 528public sealed record UserTaskQueryResultDto(
 529    IReadOnlyCollection<UserTaskSummary> Items,
 530    string? NextCursor,
 531    int? TotalCount);
 532
 533/// <summary>
 534/// Wire projection of a participant. The tenant is implicit in the caller's own scope and is deliberately
 535/// never emitted, so a response can never be used to enumerate foreign tenants.
 536/// </summary>
 537public sealed record UserTaskParticipantSummary(string Kind, string? Provider, string Id, string? DisplayName)
 538{
 539    public const string UserKind = "user";
 540    public const string GroupKind = "group";
 541
 542    public static UserTaskParticipantSummary? From(ParticipantReference? reference) => reference == null
 543        ? null
 544        : new(reference.Type == UserTaskParticipantType.Group ? GroupKind : UserKind, reference.Provider, reference.Id, 
 545}
 546
 547/// <summary>The safe summary every authorized caller receives. Protected fields live on <see cref="UserTaskDetail"/>.</
 548public record UserTaskSummary
 549{
 550    public string Id { get; init; } = "";
 551    public string Title { get; init; } = "";
 552    public string? Summary { get; init; }
 553    public string? Reference { get; init; }
 554    public IReadOnlyCollection<string> Tags { get; init; } = [];
 555    public string? TaskType { get; init; }
 556    public string Status { get; init; } = nameof(UserTaskStatus.Available);
 557    public int Priority { get; init; } = 50;
 558    public UserTaskParticipantSummary? Assignee { get; init; }
 559    /// <summary>A count-only candidate description. Candidate identities are never disclosed to peers.</summary>
 560    public string? CandidateSummary { get; init; }
 561    public DateTimeOffset? DueAt { get; init; }
 562    public bool IsOverdue { get; init; }
 563    public DateTimeOffset CreatedAt { get; init; }
 564    public DateTimeOffset UpdatedAt { get; init; }
 565    public DateTimeOffset? AssignedAt { get; init; }
 566    public DateTimeOffset? CompletedAt { get; init; }
 567    public string? WorkflowDefinitionId { get; init; }
 568    public string? WorkflowDefinitionName { get; init; }
 569    public int? WorkflowDefinitionVersion { get; init; }
 570    public string? WorkflowInstanceId { get; init; }
 571    public string? WorkflowInstanceReference { get; init; }
 572    public string? HealthSeverity { get; init; }
 573    public string? HealthCode { get; init; }
 574    public IReadOnlyCollection<string> AllowedActions { get; init; } = [];
 575    public int Revision { get; init; }
 576}
 577
 578public sealed record UserTaskDetail : UserTaskSummary
 579{
 580    public string? Instructions { get; init; }
 581    /// <summary>The protected task payload. Present only when <see cref="UserTaskDisclosure.CanViewProtected"/> is true
 582    public JsonElement? Data { get; init; }
 583    public UserTaskDisclosure Disclosure { get; init; } = new();
 584    public UserTaskWorkflowContext? Workflow { get; init; }
 585    public UserTaskFormProjection? Form { get; init; }
 586    public IReadOnlyCollection<UserTaskFormAction> Actions { get; init; } = [];
 587    /// <summary>The recorded completion action key, when the task has reached a terminal or transitional outcome.</summ
 588    public string? Outcome { get; init; }
 589    public JsonElement? Response { get; init; }
 590    public UserTaskParticipantSummary? CompletedBy { get; init; }
 591}
 592
 593public sealed record UserTaskDisclosure
 594{
 595    public bool CanViewProtected { get; init; }
 596    public bool CanViewWorkflow { get; init; }
 597    public bool CanViewHistory { get; init; }
 598    /// <summary>True when the projection was produced for a guest session and is therefore already narrowed.</summary>
 599    public bool GuestVisible { get; init; }
 600}
 601
 602public sealed record UserTaskWorkflowContext
 603{
 604    public string? DefinitionId { get; init; }
 605    public string? DefinitionName { get; init; }
 606    public int? DefinitionVersion { get; init; }
 607    public string? InstanceId { get; init; }
 608    public string? InstanceReference { get; init; }
 609}
 610
 611public sealed record UserTaskFormProjection
 612{
 613    public string Provider { get; init; } = "";
 614    public string Key { get; init; } = "";
 615    public string? Version { get; init; }
 616    public IReadOnlyCollection<UserTaskFormField> Fields { get; init; } = [];
 617    public IReadOnlyCollection<UserTaskFormAction> Actions { get; init; } = [];
 618}
 619
 620public sealed record UserTaskFormField
 621{
 622    public string Key { get; init; } = "";
 623    public string Label { get; init; } = "";
 624    public string Type { get; init; } = "text";
 625    public bool Required { get; init; }
 626    public bool Masked { get; init; }
 627    public bool CanReveal { get; init; }
 628    /// <summary>Always null for masked fields; those are read through the explicit reveal command.</summary>
 629    public JsonElement? Value { get; init; }
 630}
 631
 632public sealed record UserTaskFormAction(string Key, string Label);
 633
 634/// <summary>Safe audit projection. Actor identifiers, reasons containing input, and metadata are not disclosed.</summar
 635public sealed record UserTaskEventSummary(string Id, string Kind, string? Summary, DateTimeOffset OccurredAt, string? Ac
 636
 637public sealed record UserTaskEventsResult(IReadOnlyCollection<UserTaskEventSummary> Items, string? NextCursor);
 638
 639/// <summary>Per-task capability and concurrency projection.</summary>
 640public sealed record UserTaskCapabilities(
 641    string TaskId,
 642    int Revision,
 643    IReadOnlyCollection<string> AllowedActions,
 644    bool CanReadProtected,
 645    bool CanManage);
 646
 647/// <summary>
 648/// Tenant- and actor-scoped feature descriptor. Studio reads this before rendering navigation; it is
 649/// advisory only and never a substitute for per-request authorization.
 650/// </summary>
 651public sealed record UserTaskFeatureCapabilities
 652{
 653    public bool Enabled { get; init; }
 654    public bool CanList { get; init; }
 655    public bool CanRead { get; init; }
 656    public bool CanReadAll { get; init; }
 657    public bool CanClaim { get; init; }
 658    public bool CanComplete { get; init; }
 659    public bool CanRelease { get; init; }
 660    public bool CanAssign { get; init; }
 661    public bool CanUpdate { get; init; }
 662    public bool CanCancel { get; init; }
 663    public bool CanCreateGuestLinks { get; init; }
 664    public bool CanViewProtected { get; init; }
 665    public bool ParticipantPicker { get; init; }
 666    public bool Realtime { get; init; }
 667    public int PollingIntervalSeconds { get; init; } = 30;
 668}
 669
 670public sealed record UserTaskParticipantQuery(string TenantId, string? Search = null, UserTaskParticipantType? Type = nu
 671
 672public sealed record ParticipantSearchResult(IReadOnlyCollection<ParticipantReference> Items, string? NextCursor, int? T
 673
 674public sealed record UserTaskFormValidationResult(bool Succeeded, JsonElement? NormalizedData = null, IReadOnlyCollectio
 675
 676public abstract record UserTaskLifecycleNotification(string TenantId, string TaskId, UserTaskStatus Status, int Revision
 677
 678public sealed record UserTaskCreated(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLife
 679public sealed record UserTaskChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision, IReadOnlyColle
 680public sealed record UserTaskCompletionAccepted(string TenantId, string TaskId, UserTaskStatus Status, int Revision, str
 681public sealed record UserTaskCompleted(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLi
 682public sealed record UserTaskTimedOut(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLif
 683public sealed record UserTaskCancelled(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLi
 684public sealed record UserTaskOverdue(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLife
 685public sealed record UserTaskInvitationChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : Us
 686public sealed record UserTaskHealthChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTa