Skip to main content

Policies

A Policy is a deterministic rule the Policy Engine evaluates on every trace ingestion. The engine returns the highest-priority action across all matching policies and translates it into an HTTP status: 403 for block, 202 for flag-for-review, 201 for approved. Policies are workspace-scoped, soft-deletable, and auditable through the Operations Audit Log. Tested against API version v1. JWT auth on every endpoint; mutations require the admin or owner role.

The Policy object

FieldTypeRequiredDescription
_id / idstringyesMongoDB ObjectId (also exposed as id virtual)
namestringyesOperator-readable name; max 100 characters
descriptionstringnoFree-form context; max 500 characters
enabledbooleannoDefault true; disabled policies are not evaluated
prioritynumbernoDefault 1; lower numbers evaluate first
conditionsCondition[]yesAll conditions must match (with AND/OR composition)
actionsAction[]yesOne or more actions to apply when conditions match
organizationIdstringyesOwning org
workspaceIdstringyesWorkspace scope
matchCountnumbernoRead-only; incremented on each match
lastMatchedstring (ISO 8601)noRead-only; last match timestamp
createdAt / updatedAtstring (ISO 8601)noStandard timestamps

Condition

FieldTypeRequiredDescription
fieldstringyesTrace field path (e.g., confidenceScore, outputDecision.action, tags)
operatorenumyesOne of equals, contains, greater_than, less_than, regex
valueanyyesComparison value; type matches the field
logicalOperatorenumnoAND or OR — how this condition combines with the next

Action

FieldTypeRequiredDescription
typeenumyesOne of block, flag_for_review, notify, approve
configobjectnoType-specific configuration (e.g., notify channel)

Engine semantics

Every trace evaluates against every enabled policy in the workspace in priority order. Conditions on a policy combine via the per-condition logicalOperator (default AND). On match, the Policy Engine collects every matched action across every matched policy and resolves the winning verdict:

priority: block > flag_for_review > notify > approve

A trace with one matched block and three matched flag_for_review returns block. A trace with no matches returns approve (HTTP 201). The matched-policy name and reason ride along on the response so the agent can log the gate.

Endpoints

# Core CRUD + templates
GET /api/v1/policies/templates
POST /api/v1/policies/from-template
GET /api/v1/policies
POST /api/v1/policies
PATCH /api/v1/policies/:id
DELETE /api/v1/policies/:id

# ADL editor (Phase 2 A.1-A.3)
POST /api/v1/policies/from-adl # create v1 policy from ADL source (admin/owner)
POST /api/v1/policies/_new/validate-adl # validate ADL — no policy id (new-policy flow)
POST /api/v1/policies/_new/dry-run # dry-run ADL against fixture (new-policy flow)
POST /api/v1/policies/:id/validate-adl # parse + type-check ADL source
POST /api/v1/policies/:id/dry-run # evaluate candidate against fixture
GET /api/v1/policies/:id/tests # list regression tests
POST /api/v1/policies/:id/tests # create regression test
DELETE /api/v1/policies/:policyId/tests/:testId
POST /api/v1/policies/:id/run-tests # execute every attached test

# Read-only views (Phase 1/2)
GET /api/v1/policies/:id/adl # lifted ADL representation
GET /api/v1/policies/:id/canary-status # SPRT decision + sample sizes

# Multi-stakeholder workflow (Phase 2 B)
GET /api/v1/policies/approvals # filter by ?state=
POST /api/v1/policies/approvals/:id/:action # reviewer-approve|reviewer-reject|
# enforcer-promote|enforcer-reject|
# author-withdraw|break-glass

# Effectiveness loop (Phase 2 D)
GET /api/v1/policies/effectiveness # per-policy precision/recall/F1/FPR

# Portable bundle export (Phase 3 A)
POST /api/v1/policies/export-bundle # preview manifest
GET /api/v1/policies/export-bundle?download=1 # stream tar.gz

# Auditor view (Phase 3 B.2 — role 'auditor', 'admin', 'owner')
GET /api/v1/policies/audit/auditor-view

# Insurance attestation feed (Phase 3 C — opt-in)
GET /api/v1/policies/insurance-feed # regenerate current-month feed (no audit-log)
POST /api/v1/policies/insurance-feed # regenerate + audit-log entry

All endpoints are org-scoped (Cardinal Rule #1). Approval-queue actions require either the policy's lifecycle-state precondition (e.g. pending_reviewreviewer-approve|reviewer-reject only) or owner/admin role for break-glass. The auditor role is read-only and limited to /audit/auditor-view + /:id/adl + /effectiveness.

List policies

GET /api/v1/policies

Returns every non-soft-deleted policy in the caller's organisation. Workspace-scoped via x-workspace-id.

curl
curl https://api.adjudon.com/api/v1/policies \
-H "Authorization: Bearer $ADJUDON_API_KEY"

Errors: 401, 500 INTERNAL_ERROR.

Create a policy

POST /api/v1/policies
Body fieldRequiredDescription
nameyesNon-empty string
conditionsyesArray of Condition objects
actionsyesArray of Action objects
descriptionnoFree-form context
prioritynoDefaults to 1
enablednoDefaults to true
workspaceIdnoDefaults to caller's active workspace
curl — block low-confidence loan denials
curl -X POST https://api.adjudon.com/api/v1/policies \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Block low-confidence loan denials",
"priority": 10,
"conditions": [
{ "field": "confidenceScore", "operator": "less_than", "value": 0.7, "logicalOperator": "AND" },
{ "field": "outputDecision.action", "operator": "equals", "value": "deny" }
],
"actions": [
{ "type": "block" }
]
}'
Python
import os, requests
r = requests.post(
"https://api.adjudon.com/api/v1/policies",
headers={"Authorization": f"Bearer {os.environ['ADJUDON_API_KEY']}"},
json={
"name": "Block low-confidence loan denials",
"priority": 10,
"conditions": [
{"field": "confidenceScore", "operator": "less_than", "value": 0.7, "logicalOperator": "AND"},
{"field": "outputDecision.action", "operator": "equals", "value": "deny"},
],
"actions": [{"type": "block"}],
},
)

Errors: 400 VALIDATION_ERROR (missing name, non-array conditions/actions), 401, 403 (role gate), 500.

Update a policy

PATCH /api/v1/policies/:id

Partial update; any subset of name, description, enabled, priority, conditions, actions may be supplied.

curl — disable a policy
curl -X PATCH https://api.adjudon.com/api/v1/policies/65b1f2c4 \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'

Errors: 401, 404 NOT_FOUND, 500.

Delete a policy

DELETE /api/v1/policies/:id

Soft-delete via the softDelete plugin: the row stays in the database, marked deleted; subsequent GET responses exclude it; the operations audit-log entry on policy.delete remains by Cardinal Rule 5.

Errors: 401, 404 NOT_FOUND, 500.

Templates

GET   /api/v1/policies/templates
POST /api/v1/policies/from-template

GET /templates returns the library of pre-built policy shapes a new organisation can import without writing conditions by hand. POST /from-template clones the template into a real Policy attached to the caller's workspace, optionally overriding the name. Errors: 400 VALIDATION_ERROR (missing templateId), 404 NOT_FOUND (template id not in library), 500.

Common gotchas

  • Idempotency. POST /policies and POST /from-template are mutating endpoints but the Idempotency-Key middleware is currently wired only on POST /traces. A retried policy create produces a duplicate. Send a unique name per attempt and check the audit log if you suspect a duplicate.
  • workspaceId is required on the schema. Omit it on the body and the server falls back to the caller's active workspace; pass it explicitly when scripting cross-workspace creates.
  • Priority is ASCending. A policy with priority: 1 evaluates before priority: 100. The verdict order (`block > flag > notify

    approve) is independent of priority` — priority only controls evaluation order, not which action wins.

  • Soft-delete. A deleted policy is invisible to GET but still in the database. There is no restore endpoint today; recreate.

See also