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
| Field | Type | Required | Description |
|---|---|---|---|
_id / id | string | yes | MongoDB ObjectId (also exposed as id virtual) |
name | string | yes | Operator-readable name; max 100 characters |
description | string | no | Free-form context; max 500 characters |
enabled | boolean | no | Default true; disabled policies are not evaluated |
priority | number | no | Default 1; lower numbers evaluate first |
conditions | Condition[] | yes | All conditions must match (with AND/OR composition) |
actions | Action[] | yes | One or more actions to apply when conditions match |
organizationId | string | yes | Owning org |
workspaceId | string | yes | Workspace scope |
matchCount | number | no | Read-only; incremented on each match |
lastMatched | string (ISO 8601) | no | Read-only; last match timestamp |
createdAt / updatedAt | string (ISO 8601) | no | Standard timestamps |
Condition
| Field | Type | Required | Description |
|---|---|---|---|
field | string | yes | Trace field path (e.g., confidenceScore, outputDecision.action, tags) |
operator | enum | yes | One of equals, contains, greater_than, less_than, regex |
value | any | yes | Comparison value; type matches the field |
logicalOperator | enum | no | AND or OR — how this condition combines with the next |
Action
| Field | Type | Required | Description |
|---|---|---|---|
type | enum | yes | One of block, flag_for_review, notify, approve |
config | object | no | Type-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
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
List policies
GET /api/v1/policies
Returns every non-soft-deleted policy in the caller's organisation.
Workspace-scoped via x-workspace-id.
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 field | Required | Description |
|---|---|---|
name | yes | Non-empty string |
conditions | yes | Array of Condition objects |
actions | yes | Array of Action objects |
description | no | Free-form context |
priority | no | Defaults to 1 |
enabled | no | Defaults to true |
workspaceId | no | Defaults to caller's active workspace |
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" }
]
}'
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 -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 /policiesandPOST /from-templateare mutating endpoints but the Idempotency-Key middleware is currently wired only onPOST /traces. A retried policy create produces a duplicate. Send a uniquenameper attempt and check the audit log if you suspect a duplicate. workspaceIdis 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: 1evaluates beforepriority: 100. The verdict order (`block > flag > notifyapprove
) is independent ofpriority` — priority only controls evaluation order, not which action wins. - Soft-delete. A deleted policy is invisible to
GETbut still in the database. There is no restore endpoint today; recreate.
See also
- Policies & Human Review — the concept page covering the engine semantics in narrative form
- Traces API — the endpoint where
policy verdicts are returned (
201/202/403) - Reviews API — what
flag_for_reviewlands on - Error Codes — the failure
envelope including
ADJ_BLOCKED_BY_POLICY