Policies & Workflows
A one-off question produces an AnswerRun. But most CFO work is standing: “is our D&O coverage still sized correctly?”, “does NAV reconcile this period?”. OpsDNA models these as DomainPolicies — materialized standing queries — executed by @opsdna/workflow-policy and traced as PolicyRun records, because a standing obligation re-asked ad hoc in chat would drift, go stale, or silently stop being asked. Side-effectful work (actually doing something after approval) runs through workflow-core/workflow-runtime against decision packets.
DomainPolicy (packages/workflow-policy)
src/schema.ts defines DomainPolicySchema: a policy has at least one activation (DomainPolicyActivationSchema — what triggers evaluation), a typed query plan, and a source with provenance. A load-bearing refinement: model-proposed policy sources must include a proposalRef — an LLM can propose a standing policy, but the proposal itself becomes auditable state before the policy can run. hashDomainPolicy gives every policy a canonical content hash.
The package also ships two concrete policy domains as first-class implementations: D&O coverage sizing (insurance.ts, do-coverage-projections.ts) and NAV reconciliation (nav-persona-projections.ts), plus audience-projection registries that decide what different personas may see of a policy result, and approval-proof.ts for approval evidence.
PolicyRun invariants
PolicyRunSchema (src/schema.ts) is deliberately strict. Its refinement (policyRunIssue) enforces, among others:
// packages/workflow-policy/src/schema.ts (refinement highlights)
if (run.tenantId !== run.typedQueryPlan.tenantId)
return "PolicyRun tenantId must match typedQueryPlan.tenantId."
if (run.queryPlanHash !== hashTypedQueryPlan(run.typedQueryPlan))
return "PolicyRun queryPlanHash must match the typed query plan."
// every permission decision must be bound to this plan hash...
if (decision.planHash !== run.queryPlanHash)
return "PolicyRun permission decisions must be bound to the PolicyRun query plan hash."
// ...to resolved runtime resources, not plan templates...
if (containsUnresolvedPlanRef(decision.scope) || containsUnresolvedPlanRef(decision.resourceRef))
return "PolicyRun permission decisions must be bound to resolved runtime resources and scopes, not plan-template refs."
// ...and every step must have a decision for every required gate:
if (!permissionDecisionKeys.has(`${step.id}:${gate}`))
return `PolicyRun is missing a ${gate} permission decision for step ${step.id}.`Linked AnswerRun ids must be unique within a run, and every audit verdict must actually target the PolicyRun’s graph (filterAuditVerdictsByRole). derivePolicyRunAggregateTrustState folds audit verdicts into a single trust state, mirroring AnswerRun. In short: a PolicyRun that decodes is a PolicyRun whose plan, permissions, and audits are mutually consistent — you cannot persist a half-gated run.
Findings
packages/findings turns policy results into managed state: registry.ts (finding types), lifecycle.ts (open → resolved transitions), policy-adapter.ts (bridging PolicyRuns to findings), and contradiction-flow.ts (what happens when new evidence contradicts an existing finding).
Workflow execution (workflow-core / workflow-runtime)
When a policy or user action needs to do something, packages/workflow-runtime/src/runtime.ts executes tool steps against a WorkflowToolRegistry, and commits or fails a DecisionPacket (@opsdna/decision-packets) — the approval artifact that authorized the run. Traces are hash-versioned (WORKFLOW_RUN_TRACE_HASH_VERSION = "workflow-run-trace:v1"). Two authority modes exist:
executeApprovedWorkflow— real execution undergeneric_approvalauthorityexecuteReviewedWorkflowDemo— fixture tools for review demos, explicitly “without granting production authority”
packages/workflow-core holds the shared plan/action schemas and planner primitives that both the policy layer and the runtime consume.
Don’t confuse the three “workflow” packages. workflow-policy = standing queries and PolicyRuns (read/evaluate). workflow-runtime = side-effectful execution gated by decision packets (write/act). workflow-core = shared schemas. And as everywhere in the runtime: the legacy document-to-workflow experiments in packages/query-execution are not the place for new policy contracts.
Policy evals live in the eval harness (bun run evals:workflow-policy) — see AI Layer & Evals. Trace persistence is covered by packages/persistence; permission gating by Permissions & Evidence.