Policy model
The PolicyEvaluator contract, three policy kinds, and dispatch evidence.
A policy in Fabric is a predicate that returns pass, warn, or block for a given action invocation. Source: packages/platform/policies/index.ts.
The evaluator contract
interface PolicyEvaluator<TDb = unknown> {
policyId: PolicyId; // "orders.payment_consent.v1"
version: number;
kind?: "code";
previewSafe?: boolean;
evaluate: (ctx: PolicyContext<TDb>) => Promise<PolicyOutcome>;
}public interface IPolicyEvaluator
{
PolicyId PolicyId { get; } // "orders.payment_consent.v1"
int Version { get; }
PolicyKind Kind { get; }
bool PreviewSafe { get; }
Task<PolicyEvaluationResult> EvaluateAsync(IPolicyContext ctx, CancellationToken ct);
}interface PolicyContext<TDb> {
tenantId: string;
spaceId: string;
actionInvocationId: string;
actionId: string;
parameters: unknown;
db: TDb;
now?: Date;
mode?: "preview" | "execute";
services?: FabricRuntimeServices; // host-injected runtime deps
}public interface IPolicyContext
{
string TenantId { get; }
string SpaceId { get; }
string ActionInvocationId { get; }
string ActionId { get; }
object Parameters { get; }
object Db { get; }
DateTimeOffset? Now { get; }
string? Mode { get; } // "preview" | "execute"
IFabricRuntimeServices Services { get; }
}The evaluator gets read-only access to db (for cross-entity checks like "has consent been recorded?") and the action's full parameters. services mirrors ActionContext.services and lets the host inject HTTP clients, secret managers, or external policy engines without module-level globals. It returns:
interface PolicyOutcome {
policyId: PolicyId;
policyVersion: number;
policyKind?: PolicyKind;
result: "pass" | "warn" | "block";
reason?: string;
guidance?: PolicyGuidance;
metadata?: Record<string, unknown>;
dispatchEvidence?: PolicyDispatchEvidence;
}public sealed class PolicyEvaluationResult
{
public PolicyId PolicyId { get; }
public int PolicyVersion { get; }
public PolicyKind? PolicyKind { get; }
public PolicyOutcome Result { get; } // Pass | Warn | Block
public string? Reason { get; }
public PolicyGuidance? Guidance { get; }
public Dictionary<string, object?>? Metadata { get; }
public PolicyDispatchEvidence? DispatchEvidence { get; }
}reason is the short, human-readable explanation. guidance is the structured operator/audit contract for any outcome that needs user action, especially block:
interface PolicyGuidance {
summary?: string;
factsUsed: Array<{
name: string;
value: unknown;
label?: string;
}>;
correctiveActions: Array<{
kind: "invoke_action" | "navigate" | "manual";
label: string;
description?: string;
actionId?: string; // required when kind === "invoke_action"
parameters?: Record<string, unknown>;
route?: string;
target?: {
surface?: string;
tab?: string;
field?: string;
};
requiresRole?: string[];
requiresConfirmation?: boolean;
}>;
}public sealed class PolicyGuidance
{
public string? Summary { get; }
public IReadOnlyList<PolicyFact> FactsUsed { get; }
public IReadOnlyList<PolicyCorrectiveAction> CorrectiveActions { get; }
}
public sealed class PolicyFact
{
public string Name { get; }
public object Value { get; }
public string? Label { get; }
}
public sealed class PolicyCorrectiveAction
{
public string Kind { get; } // "invoke_action" | "navigate" | "manual"
public string Label { get; }
public string? Description { get; }
public string? ActionId { get; }
public Dictionary<string, object?>? Parameters { get; }
public string? Route { get; }
public PolicyTarget? Target { get; }
public string[]? RequiresRole { get; }
public bool RequiresConfirmation { get; }
}The platform owns and validates the shape. Verticals own the meaning. A lending policy might point to lending.record_consent; an insurance policy might point to a coverage-evidence screen. Platform UIs can render both without knowing those domains.
Corrective actions are explicit:
invoke_actionmeans the host may run a Fabric action with the suppliedactionIdandparameters.navigatemeans the host should open a route, tab, field, or work queue so an operator can fix data.manualmeans the next step requires judgment, external work, or a state that cannot be fixed by automation.
Three kinds
| Kind | Where the logic lives | When to use |
|---|---|---|
code | A registered TypeScript PolicyEvaluator / C# IPolicyEvaluator. | Anything requiring database reads or non-trivial logic. |
data | A declarative DataPolicyDefinition (condition tree). | Simple parameter/context comparisons that compliance teams want to inspect/edit. |
hybrid | Data first; on a configured trigger, fall back to code. | Default-deny rules where the data check covers 90 % of cases and code handles edges. |
Data policies
interface DataPolicyDefinition {
conditions: DataPolicyCondition[];
defaultResult?: PolicyEvaluationResult;
reason?: string;
}public sealed class DataPolicyDefinition
{
public IReadOnlyList<DataPolicyCondition> Conditions { get; }
public PolicyEvaluationResult? DefaultResult { get; }
public string? Reason { get; }
}Conditions are one of: always, parameter, comparison, all, any, not. Comparisons are limited to exists, equals, notEquals, gt, gte, lt, lte. Path access is restricted to parameters.* and a small allowlisted set of context paths (tenantId, spaceId, actionInvocationId, actionId, mode).
The bounded shape (max depth 5, max 100 conditions, max 12 path segments — see invariants) is intentional. A data policy that needs more is a code policy.
Hybrid fallback
interface PolicyFallbackDefinition {
codeEvaluatorPolicyId: PolicyId;
onResults?: PolicyEvaluationResult[]; // default: ["warn", "block"]
triggers?: PolicyFallbackTrigger[]; // default: all three triggers
reason?: string;
}public sealed class PolicyFallbackDefinition
{
public PolicyId CodeEvaluatorPolicyId { get; }
public IReadOnlyList<PolicyOutcome> OnResults { get; }
public IReadOnlyList<PolicyFallbackTrigger> Triggers { get; }
public string? Reason { get; }
}Triggers:
data_result— the data outcome was inonResults.missing_data_definition— the policy is hybrid but has nodataDefinition.invalid_data_definition— the data definition failed validation.
Hybrid lets compliance own the common path declaratively while engineering owns the edge.
Dispatch evidence
Every outcome carries a dispatchEvidence blob recording:
interface PolicyDispatchEvidence {
policyKind: PolicyKind;
policyId?: PolicyId;
policyVersion?: number;
dispatchPath: Array<"data" | "code" | "fallback">;
data?: { definitionVersion, definitionStatus, conditionResults, validationErrors };
code?: { requestedPolicyId, policyId, version, registered, skipped?, skipReason? };
fallback?: { used, trigger, reason, fromResult, definitionVersion, codeEvaluatorPolicyId };
engine?: { name, version?, metadata? }; // external-engine provenance (see BYO engine)
}public sealed class PolicyDispatchEvidence
{
public PolicyKind PolicyKind { get; }
public PolicyId? PolicyId { get; }
public int? PolicyVersion { get; }
public IReadOnlyList<string> DispatchPath { get; }
public DataDispatchEvidence? Data { get; }
public CodeDispatchEvidence? Code { get; }
public FallbackDispatchEvidence? Fallback { get; }
public EngineDispatchEvidence? Engine { get; }
}This is what makes the policy decision inspectable. A regulator does not have to trust the result; they can read why it was reached. The engine block carries deployment provenance when a code evaluator delegates to an external engine such as OPA — see bring your own policy engine. The code.skipped / code.skipReason fields are populated when a non-preview-safe evaluator is skipped in preview mode; the platform yields result: "warn" in that case so the preview UI can render would be evaluated at execute time.
dispatchEvidence explains how the decision was dispatched. guidance explains what the user can do next. Keep those separate: evidence is for audit/provenance; guidance is for operator remediation.
Aggregation
Multiple policies may attach to one action. The runtime evaluates each, then aggregates:
function aggregatePolicyOutcomes(outcomes: PolicyOutcome[]): PolicyOutcome | undefined {
return outcomes.find((o) => o.result === "block")
?? outcomes.find((o) => o.result === "warn");
}public static PolicyEvaluationResult? AggregatePolicyOutcomes(
IEnumerable<PolicyEvaluationResult> outcomes)
{
return outcomes.FirstOrDefault(o => o.Result == PolicyOutcome.Block)
?? outcomes.FirstOrDefault(o => o.Result == PolicyOutcome.Warn);
}Any block halts. Otherwise the first warn is surfaced. Otherwise pass.
See also
- Enforcement modes — practitioner's guide: when to reach for code, data, hybrid, or OPA.
- Enforcement — when and how dispatch happens.
- Policy checkpoints — pipeline placement.
- Audit trail — how dispatch evidence becomes compliance evidence.