Workflows¶
Target Architecture — Final-State Design
This page describes the final-state governance workflows. They are realised as MassTransit-coordinated flows and stateful aggregates across the Policy and Approval contexts, grounded in the canonical Event Envelope.
The platform's signature workflow is the governance approval flow: a sensitive action is evaluated against policy; if a gate is required, a human approval is requested and the action proceeds or is blocked on the decision. Supporting flows are policy evaluation, the approval state machine, and escalation/timeout.
Governance Approval Flow¶
This is the end-to-end flow when a platform (e.g. the Control Plane or DevOps & GitOps) attempts a sensitive action.
sequenceDiagram
participant Caller as Calling Platform
participant Engine as PolicyEngineService
participant Eval as PolicyEvaluationService
participant Approval as ApprovalService
participant Human as Human Approver
participant Audit as AuditService
Caller->>Engine: POST /policies/evaluate (action)
Engine->>Eval: evaluate(subject, resource, action, risk)
Eval-->>Engine: matched rules + effect
Engine->>Audit: record PolicyDecision
alt Allow
Engine-->>Caller: decision = Allow
Caller->>Caller: proceed with action
else Deny
Engine-->>Caller: decision = Deny
Caller->>Caller: block action
else RequiresApproval (gate)
Engine->>Approval: RequestApproval(policyDecisionId)
Approval->>Audit: record ApprovalRequested
Approval-->>Engine: approvalRequestId
Engine-->>Caller: decision = RequiresApproval (pending)
Human->>Approval: approve / reject
Approval->>Audit: record ApprovalDecision
alt Approved
Approval-->>Caller: ApprovalGranted (event)
Caller->>Caller: proceed with action
else Rejected
Approval-->>Caller: ApprovalRejected (event)
Caller->>Caller: block action
end
end
The calling platform never proceeds with a gated action until it receives an ApprovalGranted event (or a synchronous Allow). Every step writes an AuditEntry, so the full decision chain is provable and replayable via traceId.
Policy Evaluation Flow¶
How PolicyEngineService produces a decision, composing rule evaluation with ABAC suppliers and risk.
flowchart TB
Start["Evaluate request"] --> Load["Load applicable PolicyDefinitions (by domain + scope)"]
Load --> Attrs["Gather ABAC attributes"]
Attrs --> Iso["TenantIsolationPolicyService: isolation attrs"]
Attrs --> Class["DataClassificationService: classification labels"]
Attrs --> Risk["RiskScoringService: risk score"]
Iso --> Match["Match rules by priority"]
Class --> Match
Risk --> Match
Match --> Effect{"Resolved effect?"}
Effect -->|Allow| RecordA["Record PolicyDecision = Allow"]
Effect -->|Deny| RecordD["Record PolicyDecision = Deny"]
Effect -->|Gate| RecordG["Record PolicyDecision = RequiresApproval"]
RecordG --> OpenApproval["Open ApprovalRequest"]
RecordA --> Emit["Emit PolicyDecisionRecorded + AuditEntry"]
RecordD --> Emit
OpenApproval --> Emit
Evaluation is deterministic: rules are matched in priority order, the highest-priority matching rule's effect wins, and a Deny always overrides an Allow at equal priority. A RequiresApproval gate is selected when a matched rule's gate is HumanApproval or when the supplied RiskScore crosses the policy's risk threshold.
ApprovalRequest State Machine¶
The lifecycle of an ApprovalRequest aggregate.
stateDiagram-v2
[*] --> Pending: ApprovalRequested
Pending --> Approved: approver grants
Pending --> Rejected: approver rejects
Pending --> Escalated: timeout (escalatable)
Pending --> Expired: timeout (final)
Escalated --> Approved: escalated approver grants
Escalated --> Rejected: escalated approver rejects
Escalated --> Expired: max escalation reached
Approved --> [*]
Rejected --> [*]
Expired --> [*]
| State | Meaning | Exit |
|---|---|---|
Pending |
Open, awaiting an authorized approver. | Approve / Reject / Escalate / Expire. |
Escalated |
Timed out at the current level; routed to the next approver tier. | Approve / Reject / Expire. |
Approved |
Granted by an authorized approver — terminal. | — |
Rejected |
Rejected by an approver or auto-rejected — terminal. | — |
Expired |
Timed out with no escalation path — terminal (treated as a block). | — |
Escalation & Timeout¶
The ApprovalTimeoutWorker ticks every minute and reconciles open requests against their expiresAt.
flowchart TB
Tick["ApprovalTimeoutWorker (1 min)"] --> Find["Find Pending/Escalated requests past expiresAt"]
Find --> Decide{"Escalation level < max?"}
Decide -->|Yes| Escalate["Escalate to next approver tier"]
Escalate --> Reset["Reset timer, increment escalationLevel"]
Reset --> Notify["Re-emit ApprovalRequested (escalation)"]
Decide -->|No| AutoReject["Auto-reject (Expired)"]
AutoReject --> EmitRej["Emit ApprovalRejected + AuditEntry"]
- Escalation policy is part of the matched policy: each tier has a timeout and a target approver set (e.g.
ReleaseManager→SecurityLead→ComplianceOfficer). - Default-deny on expiry — if no escalation path remains, the request expires and the action is blocked (fail-safe, never fail-open).
- Segregation of duties — escalation never routes a request to its raising subject; every grant/reject records the
approverId, comment, and justification for audit.
How the Workflows Tie Back¶
- Traceability — every decision, approval, and audit entry shares the
traceIdfrom the originating action, so the whole chain is one queryable thread. - Autonomy —
Allow/Denyresolve synchronously with no human; only gated actions pause for approval. - Governance — nothing irreversible proceeds without a recorded
PolicyDecisionand (when gated) anApprovalDecision. - Observability — flow transitions emit canonical events consumed by Observability & Feedback.
Related¶
- Events · Workers · APIs · Aggregate Roots · Security
- Control Plane · DevOps & GitOps
- Reference: Event Envelope