Portal Community

Database Table: Process_ApprovalDecisions

CREATE TABLE Process_ApprovalDecisions (
    Id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    TenantId    NVARCHAR(100)    NOT NULL,
    ExecutionId NVARCHAR(100)    NOT NULL,
    NodeId      NVARCHAR(100)    NOT NULL,
    ActorId     NVARCHAR(100)    NOT NULL,
    Decision    TINYINT          NOT NULL,   -- 1=Approved, 2=Rejected
    Comment     NVARCHAR(2000)   NULL,
    DecidedAt   DATETIMEOFFSET   NOT NULL DEFAULT GETUTCDATE(),
    CONSTRAINT UQ_ApprovalDecision UNIQUE (ExecutionId, NodeId, ActorId)
);

Decision States

StateMeaning
PendingActor has been assigned a HIL task but has not yet responded (no row in Process_ApprovalDecisions yet)
ApprovedActor submitted an Approved decision — row exists with Decision=1
RejectedActor submitted a Rejected decision — row exists with Decision=2
CancelledActor's task was cancelled because consensus was already reached by others

Consensus Evaluation Flow

1

Actor submits decision

POST /api/approvals/{taskId}/decide — writes a row to Process_ApprovalDecisions.

2

ApprovalStateTracker.EvaluateAsync()

Reads all decision rows for this (ExecutionId, NodeId). Applies the strategy logic.

3a

Consensus reached → resume

IWorkflowContinuationOrchestrator.ContinueAsync() is called. Remaining HIL tasks are cancelled.

3b

Not yet → wait

No action. The execution remains suspended. Other actors' tasks stay open.

Accessing Approval State from Executors

// In a downstream executor, read approval decisions from execution memory
var approvalOutput = ctx.ExecutionMemory.GetNodeOutput<ApprovalNodeOutput>("approvalNode1");

// approvalOutput contains:
// .AggregateDecision  — Approved | Rejected
// .Decisions          — List<ActorDecision>: actorId, decision, decidedAt, comment
// .Strategy           — the strategy used
// .CompletedAt        — when consensus was reached