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

673 lines
27 KiB
C#
Raw Normal View History

2026-08-24 22:09:06 +00:00
using System.Security.Claims;
using System.Text.Json;
using Elsa.Mediator.Contracts;
namespace Elsa.UserTasks.Models;
public enum UserTaskParticipantType
{
User,
Group
}
public enum UserTaskMembershipResolutionMode
{
Live,
Snapshot
}
public enum UserTaskStatus
{
Unassigned,
Available,
Assigned,
Completing,
TimingOut,
Cancelling,
Completed,
TimedOut,
Cancelled
}
public enum UserTaskHealthSeverity
{
Advisory,
Blocking
}
public enum UserTaskAccessOperation
{
ReadSummary,
ReadProtected,
Claim,
Release,
Assign,
UpdateScheduling,
Complete,
Cancel,
Manage,
IssueInvitation,
RetryResolution
}
public enum UserTaskOperationKind
{
Claim,
Release,
Assign,
ScheduleUpdate,
Complete,
Timeout,
Cancel,
RetryResolution,
InvitationVerification
}
public enum UserTaskOperationStatus
{
Accepted,
Completed,
Failed
}
public enum UserTaskInvitationStatus
{
Pending,
Dispatched,
Verified,
Consumed,
Revoked,
Expired
}
public sealed record ParticipantReference(
string TenantId,
string Provider,
UserTaskParticipantType Type,
string Id,
string? DisplayName = null)
{
public bool Matches(ParticipantReference? other) => other != null
&& string.Equals(TenantId, other.TenantId, StringComparison.Ordinal)
&& string.Equals(Provider, other.Provider, StringComparison.Ordinal)
&& Type == other.Type
&& string.Equals(Id, other.Id, StringComparison.Ordinal);
}
public sealed record UserTaskActor(
ParticipantReference Subject,
IReadOnlyCollection<ParticipantReference> Groups,
string? DisplayName = null)
{
/// <summary>Indicates that the host has granted tenant-scoped manager access.</summary>
public bool IsManager { get; init; }
public IReadOnlySet<string> Permissions { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Set when the caller authenticated with a guest invitation session. A guest is scoped to exactly
/// one task; every policy decision for any other task must deny.
/// </summary>
public string? GuestTaskId { get; init; }
/// <summary>The completion action keys a guest session was issued for. Empty for ordinary actors.</summary>
public IReadOnlySet<string> GuestAllowedActions { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public bool IsGuest => GuestTaskId != null;
public bool HasPermission(string permission) => Permissions.Contains(permission) || Permissions.Contains("*");
}
public sealed record UserTaskAction(
string Key,
string Label,
IReadOnlyDictionary<string, object?>? Metadata = null);
/// <summary>
/// Describes a guest invitation that is materialized with a task. The secret itself is created only
/// by Core at runtime and is never part of this definition.
/// </summary>
public sealed record UserTaskInvitationDefinition(
string VerifierName,
IReadOnlyCollection<string> AllowedActions,
TimeSpan? Lifetime = null,
bool BearerOnly = false,
string? Recipient = null,
IReadOnlyDictionary<string, object?>? Configuration = null);
public sealed record UserTaskFormReference(
string ProviderName,
string Key,
string? Binding = null,
string? Version = null);
public sealed record ResolvedUserTaskForm(
UserTaskFormReference Requested,
string PinnedVersion,
IReadOnlyDictionary<string, object?> Metadata)
{
/// <summary>
/// Provider-neutral field descriptors used to render the response surface. Descriptors carry shape and
/// disclosure flags only; values are read from the task payload under the protected-access decision.
/// </summary>
public IReadOnlyCollection<UserTaskFormFieldDescriptor> Fields { get; init; } = [];
}
public sealed record UserTaskFormFieldDescriptor(
string Key,
string Label,
string Type = "text",
bool Required = false,
bool Masked = false,
bool CanReveal = false);
public sealed record UserTaskDefinitionSnapshot
{
public string Title { get; init; } = "";
public string? Summary { get; init; }
public string? Reference { get; init; }
public IReadOnlyCollection<string> Tags { get; init; } = [];
public string? TaskType { get; init; }
public ParticipantReference? Requester { get; init; }
public ParticipantReference? Assignee { get; init; }
public IReadOnlyCollection<ParticipantReference> CandidateUsers { get; init; } = [];
public IReadOnlyCollection<ParticipantReference> CandidateGroups { get; init; } = [];
public IReadOnlyCollection<ParticipantReference> ExcludedUsers { get; init; } = [];
public UserTaskMembershipResolutionMode MembershipResolutionMode { get; init; } = UserTaskMembershipResolutionMode.Live;
public bool AllowManagerExclusionOverride { get; init; }
public int Priority { get; init; } = 50;
public DateTimeOffset? DueAt { get; init; }
public string? Instructions { get; init; }
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; } = "";
public string? WorkflowDefinitionName { get; set; }
public int? WorkflowDefinitionVersion { get; set; }
public string WorkflowInstanceId { get; set; } = "";
/// <summary>A safe, host-authored instance reference (correlation ID or instance name). Never a bookmark or token.</summary>
public string? WorkflowInstanceReference { get; set; }
public string ActivityInstanceId { get; set; } = "";
public string BookmarkId { get; set; } = "";
public string MaterializationKey { get; set; } = "";
public string Title { get; set; } = "User task";
public string? Summary { get; set; }
public string? Reference { get; set; }
public HashSet<string> Tags { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public string? TaskType { get; set; }
public ParticipantReference? Requester { get; set; }
public ParticipantReference? Assignee { get; set; }
public List<ParticipantReference> CandidateUsers { get; set; } = [];
public List<ParticipantReference> CandidateGroups { get; set; } = [];
public List<ParticipantReference> SnapshotMembers { get; set; } = [];
public List<ParticipantReference> SnapshotGroups { get; set; } = [];
public List<ParticipantReference> ExcludedUsers { get; set; } = [];
public UserTaskMembershipResolutionMode MembershipResolutionMode { get; set; } = UserTaskMembershipResolutionMode.Live;
public bool AllowManagerExclusionOverride { get; set; }
public int Priority { get; set; } = 50;
public DateTimeOffset? DueAt { get; set; }
public bool IsOverdue { get; set; }
public string? Instructions { get; set; }
public JsonElement? TaskData { get; set; }
public UserTaskFormReference? RequestedForm { get; set; }
public ResolvedUserTaskForm? PinnedForm { get; set; }
public List<UserTaskAction> Actions { get; set; } = [new("Complete", "Complete")];
public List<UserTaskInvitationDefinition> InvitationDefinitions { get; set; } = [];
public bool EnableTimeoutOutcome { get; set; }
public bool EnableCancellationOutcome { get; set; }
public UserTaskStatus Status { get; set; } = UserTaskStatus.Available;
public UserTaskHealthSeverity? HealthSeverity { get; set; }
public string? HealthCode { get; set; }
public string? HealthMessage { get; set; }
public string? CompletionActionKey { get; set; }
public JsonElement? CompletionData { get; set; }
public ParticipantReference? CompletedBy { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? AssignedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
public int Revision { get; set; } = 1;
public List<UserTaskEvent> Events { get; set; } = [];
public List<UserTaskOperation> Operations { get; set; } = [];
public List<UserTaskInvitation> Invitations { get; set; } = [];
public bool IsTerminal => Status is UserTaskStatus.Completed or UserTaskStatus.TimedOut or UserTaskStatus.Cancelled;
public bool IsOpen => !IsTerminal;
}
public sealed record UserTaskEvent(
string Id,
string TenantId,
string TaskId,
int Revision,
string EventType,
DateTimeOffset OccurredAt,
ParticipantReference? Actor = null,
string? OperationId = null,
string? Reason = null,
IReadOnlyDictionary<string, object?>? Metadata = null);
public sealed record UserTaskOperation(
string Id,
string TenantId,
string TaskId,
string OperationId,
UserTaskOperationKind Kind,
int ExpectedRevision,
string RequestHash,
UserTaskOperationStatus Status,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? ActionKey = null,
JsonElement? Data = null,
string? ErrorCode = null);
public sealed record UserTaskInvitation(
string Id,
string TenantId,
string TaskId,
string? Recipient,
string TokenHash,
UserTaskInvitationStatus Status,
DateTimeOffset IssuedAt,
DateTimeOffset ExpiresAt,
string? VerifierName = null,
DateTimeOffset? VerifiedAt = null,
DateTimeOffset? ConsumedAt = null,
DateTimeOffset? RevokedAt = null,
string? SiblingGroupId = null)
{
/// <summary>
/// The completion actions this invitation was issued for. It is copied from the activity's invitation
/// definition at issuance so a later definition change cannot widen an outstanding guest link.
/// </summary>
public IReadOnlyCollection<string> AllowedActions { get; init; } = [];
}
public sealed record UserTaskInvitationDelivery(
string Id,
string TenantId,
string TaskId,
string InvitationId,
string DispatcherName,
string Token,
DateTimeOffset ExpiresAt)
{
public string? Recipient { get; init; }
public int Attempt { get; init; }
public DateTimeOffset? NotBefore { get; init; }
}
public sealed record UserTaskInvitationChallenge(string Token, string? Code = null, string? State = null);
/// <summary>
/// A deliberately uniform challenge descriptor. The same shape is returned for valid and invalid tokens so
/// the anonymous surface never becomes an invitation-existence oracle.
/// </summary>
public sealed record UserTaskInvitationChallengeDescriptor(string ChallengeType, string Prompt, bool RequiresCode);
public sealed record UserTaskInvitationVerificationResult(bool Succeeded, string? FailureCode = null, string? Subject = null);
public sealed record GuestSessionResult(bool Succeeded, string? Token = null, DateTimeOffset? ExpiresAt = null, string? FailureCode = null, string? TaskId = null);
/// <summary>A resolved guest session. It authorizes exactly one task and one set of completion actions.</summary>
public sealed record UserTaskGuestSession(
string TenantId,
string TaskId,
ParticipantReference Subject,
IReadOnlyCollection<string> AllowedActions,
DateTimeOffset ExpiresAt);
public sealed record UserTaskResult(
string ActionKey,
JsonElement? Data,
ParticipantReference? CompletedBy,
DateTimeOffset CompletedAt);
public sealed record UserTaskInvitationSummary(
string Id,
string TaskId,
string? Recipient,
UserTaskInvitationStatus Status,
DateTimeOffset IssuedAt,
DateTimeOffset ExpiresAt,
string? VerifierName);
public sealed record UserTaskInvitationIssueRequest(
int ExpectedRevision,
string VerifierName,
IReadOnlyCollection<string> AllowedActions,
string? Recipient = null,
TimeSpan? Lifetime = null,
string? OperationId = null);
public sealed record UserTaskInvitationIssueResult(
UserTaskInvitationSummary Invitation,
string? OperationId = null);
public sealed record UserTaskInvitationVerificationResultWithSession(
bool Succeeded,
string? TaskId = null,
string? SessionToken = null,
DateTimeOffset? ExpiresAt = null,
string? FailureCode = null);
public sealed record UserTaskMaterialization(
string TenantId,
string WorkflowDefinitionId,
string WorkflowInstanceId,
string ActivityInstanceId,
string BookmarkId,
UserTaskDefinitionSnapshot Definition,
IReadOnlyCollection<ParticipantReference> SnapshotMembers,
IReadOnlyCollection<ParticipantReference> SnapshotGroups,
DateTimeOffset CreatedAt,
string? TaskId = null,
string? WorkflowDefinitionName = null,
int? WorkflowDefinitionVersion = null,
string? WorkflowInstanceReference = null);
public sealed record UserTaskStimulus(
string TenantId,
string TaskId,
string OperationId,
string ActionKey,
JsonElement? CompletionData,
ParticipantReference? CompletedBy = null,
DateTimeOffset? CompletedAt = null,
string? BookmarkId = null);
public sealed record UserTaskBookmarkRemoval(
string TenantId,
string TaskId,
string BookmarkId,
DateTimeOffset RemovedAt);
public sealed record UserTaskMutationRequest(int ExpectedRevision, string? OperationId = null);
public sealed record UserTaskAssignRequest(
int ExpectedRevision,
ParticipantReference Assignee,
string? Reason = null,
string? OperationId = null);
public sealed record UserTaskSchedulingUpdate(
int ExpectedRevision,
int? Priority = null,
DateTimeOffset? DueAt = null,
string? OperationId = null);
public sealed record UserTaskCompletionRequest(
int ExpectedRevision,
string OperationId,
string ActionKey,
JsonElement? Data = null);
public sealed record UserTaskCancelRequest(int ExpectedRevision, string OperationId, string Reason);
/// <summary>
/// The caller-selected list scope. It is part of the authorization predicate, not a display filter:
/// <see cref="All"/> and <see cref="NeedsAttention"/> require tenant-scoped manager access.
/// </summary>
public enum UserTaskQueryScopeKind
{
Assigned,
Available,
History,
All,
NeedsAttention
}
public sealed record UserTaskQueryScope(
string TenantId,
ParticipantReference Subject,
IReadOnlyCollection<ParticipantReference> Groups,
bool IsManager = false,
UserTaskQueryScopeKind Kind = UserTaskQueryScopeKind.Assigned,
bool ExcludeBlocking = true)
{
public bool IncludeAssigned => Kind is UserTaskQueryScopeKind.Assigned;
public bool IncludeCandidates => Kind is UserTaskQueryScopeKind.Available;
public bool IncludeHistory => Kind is UserTaskQueryScopeKind.History;
public bool RequiresManager => Kind is UserTaskQueryScopeKind.All or UserTaskQueryScopeKind.NeedsAttention;
}
public sealed record UserTaskQuery
{
public string TenantId { get; init; } = "";
public UserTaskQueryScope? Scope { get; init; }
public string? Cursor { get; init; }
public int Limit { get; init; } = 50;
public string? Search { get; init; }
public IReadOnlyCollection<UserTaskStatus> Statuses { get; init; } = [];
/// <summary>Restricts the page to tasks whose due date has already elapsed.</summary>
public bool OnlyOverdue { get; init; }
/// <summary>Restricts the page to tasks that carry no due date at all.</summary>
public bool OnlyWithoutDueDate { get; init; }
public string? TaskType { get; init; }
public int? PriorityFrom { get; init; }
public int? PriorityTo { get; init; }
public DateTimeOffset? DueFrom { get; init; }
public DateTimeOffset? DueTo { get; init; }
public string? WorkflowDefinitionId { get; init; }
public string? WorkflowInstanceId { get; init; }
public string? Reference { get; init; }
public string Sort { get; init; } = "created";
public bool Descending { get; init; }
public bool IncludeTotalCount { get; init; }
}
public sealed record UserTaskQueryResult(
IReadOnlyCollection<UserTask> Items,
string? NextCursor,
int? TotalCount);
public sealed record UserTaskProjectionResult(UserTask Task, bool Created);
public sealed record UserTaskReconciliationRequest(int PageSize = 100, DateTimeOffset? OlderThan = null)
{
/// <summary>Runs a tenant-scoped pass. Hosts with a tenant catalog can invoke one pass per tenant.</summary>
public string TenantId { get; init; } = "";
}
public sealed record UserTaskReconciliationResult(int Recreated, int Requeued, int Finalized, int Ambiguous);
public sealed record UserTaskOperationResult(
UserTask Task,
UserTaskOperation Operation,
bool Accepted,
string? ConflictCode = null);
public sealed record UserTaskQueryResultDto(
IReadOnlyCollection<UserTaskSummary> Items,
string? NextCursor,
int? TotalCount);
/// <summary>
/// Wire projection of a participant. The tenant is implicit in the caller's own scope and is deliberately
/// never emitted, so a response can never be used to enumerate foreign tenants.
/// </summary>
public sealed record UserTaskParticipantSummary(string Kind, string? Provider, string Id, string? DisplayName)
{
public const string UserKind = "user";
public const string GroupKind = "group";
public static UserTaskParticipantSummary? From(ParticipantReference? reference) => reference == null
? null
: new(reference.Type == UserTaskParticipantType.Group ? GroupKind : UserKind, reference.Provider, reference.Id, reference.DisplayName);
}
/// <summary>The safe summary every authorized caller receives. Protected fields live on <see cref="UserTaskDetail"/>.</summary>
public record UserTaskSummary
{
public string Id { get; init; } = "";
public string Title { get; init; } = "";
public string? Summary { get; init; }
public string? Reference { get; init; }
public IReadOnlyCollection<string> Tags { get; init; } = [];
public string? TaskType { get; init; }
public string Status { get; init; } = nameof(UserTaskStatus.Available);
public int Priority { get; init; } = 50;
public UserTaskParticipantSummary? Assignee { get; init; }
/// <summary>A count-only candidate description. Candidate identities are never disclosed to peers.</summary>
public string? CandidateSummary { get; init; }
public DateTimeOffset? DueAt { get; init; }
public bool IsOverdue { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
public DateTimeOffset? AssignedAt { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
public string? WorkflowDefinitionId { get; init; }
public string? WorkflowDefinitionName { get; init; }
public int? WorkflowDefinitionVersion { get; init; }
public string? WorkflowInstanceId { get; init; }
public string? WorkflowInstanceReference { get; init; }
public string? HealthSeverity { get; init; }
public string? HealthCode { get; init; }
public IReadOnlyCollection<string> AllowedActions { get; init; } = [];
public int Revision { get; init; }
}
public sealed record UserTaskDetail : UserTaskSummary
{
public string? Instructions { get; init; }
/// <summary>The protected task payload. Present only when <see cref="UserTaskDisclosure.CanViewProtected"/> is true.</summary>
public JsonElement? Data { get; init; }
public UserTaskDisclosure Disclosure { get; init; } = new();
public UserTaskWorkflowContext? Workflow { get; init; }
public UserTaskFormProjection? Form { get; init; }
public IReadOnlyCollection<UserTaskFormAction> Actions { get; init; } = [];
/// <summary>The recorded completion action key, when the task has reached a terminal or transitional outcome.</summary>
public string? Outcome { get; init; }
public JsonElement? Response { get; init; }
public UserTaskParticipantSummary? CompletedBy { get; init; }
}
public sealed record UserTaskDisclosure
{
public bool CanViewProtected { get; init; }
public bool CanViewWorkflow { get; init; }
public bool CanViewHistory { get; init; }
/// <summary>True when the projection was produced for a guest session and is therefore already narrowed.</summary>
public bool GuestVisible { get; init; }
}
public sealed record UserTaskWorkflowContext
{
public string? DefinitionId { get; init; }
public string? DefinitionName { get; init; }
public int? DefinitionVersion { get; init; }
public string? InstanceId { get; init; }
public string? InstanceReference { get; init; }
}
public sealed record UserTaskFormProjection
{
public string Provider { get; init; } = "";
public string Key { get; init; } = "";
public string? Version { get; init; }
public IReadOnlyCollection<UserTaskFormField> Fields { get; init; } = [];
public IReadOnlyCollection<UserTaskFormAction> Actions { get; init; } = [];
}
public sealed record UserTaskFormField
{
public string Key { get; init; } = "";
public string Label { get; init; } = "";
public string Type { get; init; } = "text";
public bool Required { get; init; }
public bool Masked { get; init; }
public bool CanReveal { get; init; }
/// <summary>Always null for masked fields; those are read through the explicit reveal command.</summary>
public JsonElement? Value { get; init; }
}
public sealed record UserTaskFormAction(string Key, string Label);
/// <summary>Safe audit projection. Actor identifiers, reasons containing input, and metadata are not disclosed.</summary>
public sealed record UserTaskEventSummary(string Id, string Kind, string? Summary, DateTimeOffset OccurredAt, string? ActorDisplayName);
public sealed record UserTaskEventsResult(IReadOnlyCollection<UserTaskEventSummary> Items, string? NextCursor);
/// <summary>Per-task capability and concurrency projection.</summary>
public sealed record UserTaskCapabilities(
string TaskId,
int Revision,
IReadOnlyCollection<string> AllowedActions,
bool CanReadProtected,
bool CanManage);
/// <summary>
/// Tenant- and actor-scoped feature descriptor. Studio reads this before rendering navigation; it is
/// advisory only and never a substitute for per-request authorization.
/// </summary>
public sealed record UserTaskFeatureCapabilities
{
public bool Enabled { get; init; }
public bool CanList { get; init; }
public bool CanRead { get; init; }
public bool CanReadAll { get; init; }
public bool CanClaim { get; init; }
public bool CanComplete { get; init; }
public bool CanRelease { get; init; }
public bool CanAssign { get; init; }
public bool CanUpdate { get; init; }
public bool CanCancel { get; init; }
public bool CanCreateGuestLinks { get; init; }
public bool CanViewProtected { get; init; }
public bool ParticipantPicker { get; init; }
public bool Realtime { get; init; }
public int PollingIntervalSeconds { get; init; } = 30;
}
public sealed record UserTaskParticipantQuery(string TenantId, string? Search = null, UserTaskParticipantType? Type = null, string? Cursor = null, int Limit = 50);
public sealed record ParticipantSearchResult(IReadOnlyCollection<ParticipantReference> Items, string? NextCursor, int? TotalCount);
public sealed record UserTaskFormValidationResult(bool Succeeded, JsonElement? NormalizedData = null, IReadOnlyCollection<string>? Errors = null);
public abstract record UserTaskLifecycleNotification(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : INotification;
public sealed record UserTaskCreated(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision, IReadOnlyCollection<string> ChangedFields) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskCompletionAccepted(string TenantId, string TaskId, UserTaskStatus Status, int Revision, string OperationId) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskCompleted(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskTimedOut(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskCancelled(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskOverdue(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskInvitationChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);
public sealed record UserTaskHealthChanged(string TenantId, string TaskId, UserTaskStatus Status, int Revision) : UserTaskLifecycleNotification(TenantId, TaskId, Status, Revision);