Logic Controller
Call a deployed decision policy from your own systems: the decision for any record, the rules that held, and the evidence behind each one.
When to use this API
Use the Logic Controller when your own systems need the decision your business policy makes on a record: which rules hold for a customer, an asset, an order or a case, and why.
You build the policy in the Logic Controller on top of a context graph from the Context Compiler: your SOP is read into business rules you confirm, you test them on the graph's records, and you deploy a version. Every deployed version gets its own API. A call evaluates the deployed rules on the record you send, reading what you leave out from the graph and walking the record's relationships where a rule needs them. The answer names the version that made it, the rules that held, the rules another rule took precedence over, and the evidence for every clause.
The same policy is also served as an MCP server for AI agents, and it can push webhooks to your systems when a record's decision changes. To act on decisions (an agent, an assembly or a digital worker with human oversight), build on the policy in the Action Builder.
version, so you can always tell which logic made a decision.
A call always reads the version currently deployed.
Authentication
Every request carries the policy's API key. Create one in the console: open the decision policy in the
Logic Controller and choose API & integration. A key opens this one policy only, is
shown once when you create it (only a hash of it is kept), and can be set to expire after 30, 90 or 365 days.
Keys start with ok_live_.
x-ok-api-key: ok_live_xxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Bearer ok_live_xxxxxxxxxxxxxxxxxxxxxxxx
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
| policyId | string | required | The decision policy's id, shown under API & integration (for example uc-3f9a1c). |
Body of POST /decide
| Name | Type | Required | Description |
|---|---|---|---|
| entity | string | required | The entity the record is, by its id or its label (for example Customer). GET / lists the entities the policy decides on. |
| record | object | required | The record's fields, its primary key included. When the key matches a record in the graph, any field you leave out is read from the graph, and relationship rules (for example "an open order over 100") are evaluated through the graph. |
Request
curl -X POST https://okp.openknowra.ai/api/okp/d/$POLICY_ID/decide \
-H "x-ok-api-key: $OPENKNOWRA_POLICY_KEY" \
-H "Content-Type: application/json" \
-d '{"entity":"Customer","record":{"customerId":"C-1042","tenureMonths":4,"openComplaints":2}}'import os, json, urllib.request
base = "https://okp.openknowra.ai/api/okp/d/" + os.environ["POLICY_ID"]
req = urllib.request.Request(
base + "/decide",
data=json.dumps({"entity": "Customer",
"record": {"customerId": "C-1042", "tenureMonths": 4, "openComplaints": 2}}).encode(),
headers={"x-ok-api-key": os.environ["OPENKNOWRA_POLICY_KEY"], "Content-Type": "application/json"},
method="POST")
print(json.load(urllib.request.urlopen(req, timeout=30)))const base = `https://okp.openknowra.ai/api/okp/d/${process.env.POLICY_ID}`;
const res = await fetch(`${base}/decide`, {
method: "POST",
headers: { "x-ok-api-key": process.env.OPENKNOWRA_POLICY_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({ entity: "Customer", record: { customerId: "C-1042", tenureMonths: 4, openComplaints: 2 } }),
});
console.log(await res.json());Response
200 OK - the decision for the record under the deployed version.
{
"ok": true,
"version": 3,
"entity": "Customer",
"key": "C-1042",
"known": true,
"decision": [
{ "rule": "Early-tenure complaints", "ruleId": "r-7c21", "outcome": "Retention call", "kind": "rule" }
],
"overridden": [
{ "rule": "Standard follow-up", "outcome": "Email follow-up", "by": "Early-tenure complaints", "evidence": "SOP 4.2" }
],
"evidence": [
{
"rule": "Early-tenure complaints",
"holds": true,
"overridden": false,
"clauses": [
{ "clause": "tenureMonths < 6", "holds": true, "observed": 4 },
{ "clause": "openComplaints >= 2", "holds": true, "observed": 2 }
]
}
]
}
| Field | Type | Description |
|---|---|---|
| version | integer | The deployed version that made the decision. |
| key | string | null | The record's primary key, from the record you sent. |
| known | boolean | Whether the graph holds a record with this key (its other fields and relationships were then read from the graph). |
| decision | array | The rules that hold for the record after precedence, each with its outcome. |
| overridden | array | Rules that held but gave way to a rule that takes precedence (by), with the evidence for the precedence. |
| evidence | array | Every rule that bears on the record, with each clause, whether it holds and the value observed. Fields marked sensitive in the policy are masked or shown as (protected). |
All endpoints
Every path is relative to https://okp.openknowra.ai/api/okp/d/{policyId} and takes the same API key.
| Method | Path | What it does |
|---|---|---|
| GET | / | Describes the deployed version: its number and who deployed it, the entities it decides on (with their fields, and the allowed values where there are few), the rules per entity with the fields each reads, the webhook events, and every endpoint. |
| POST | /decide | The decision for a record your system holds (above). |
| GET | /decisions | The decisions the deployed version holds for the graph's records, a page at a time. Query: entity, outcome (an outcome or a rule name), limit (1-500, default 50), offset. Returns { version, total, limit, offset, items: [{ entity, key, decision }] }. |
| GET | /decisions/{entity}/{key} | One record's decision with its evidence and the record itself (sensitive fields protected). 404 when the version holds no such record. |
| POST | /events | Tells the policy a record changed in your system: the rules are evaluated again on the live state, the change is written to the decision ledger, and your webhooks fire (see below). |
| GET | /openapi.json | The OpenAPI 3.1 document of this policy's API, for code generators and API gateways. |
Record a change: POST /events
| Name | Type | Required | Description |
|---|---|---|---|
| entity | string | required | The entity of the record that changed. |
| key | string | required | Its primary key. The graph must hold the record. |
| changes | object | required | The fields that changed and their new values: { "openComplaints": 3 }. Numbers are read as numbers, true/yes/1 as true, and listed values case-insensitively. The primary key and reference fields are left as they are. |
{
"ok": true,
"version": 3,
"id": "ev-9b1c2e",
"at": "2026-10-14T09:12:44.120Z",
"entity": "Customer",
"key": "C-1042",
"summary": "Customer C-1042 now holds Retention call. Recorded in the ledger as #12.",
"decision": { "before": [], "after": [{ "rule": "Early-tenure complaints", "outcome": "Retention call" }] },
"changed": [{ "entity": "Customer", "key": "C-1042", "gained": ["Retention call"], "lost": [] }],
"webhooks": { "matched": 1, "sent": 1, "failed": 0, "pending": 0 }
}
400 no_changes and records nothing, so a retried call never writes a decision twice.
Webhooks
Add a webhook under API & integration: a URL, the events to send, and optionally a secret to sign them with. Narrow it to some outcomes or entities if you like.
| Event | Sent when |
|---|---|
| rule_result.gained | A record starts holding a rule's outcome after a change (POST /events). |
| rule_result.lost | A record stops holding a rule's outcome after a change. |
| version.published | A new version is deployed, with what it changes: per rule, the records that gain or lose its outcome. |
Headers
| Header | Value |
|---|---|
| x-ok-event | The event type, as above. |
| x-ok-delivery | The delivery's id (dlv-...). Use it to discard a delivery you have already handled. |
| x-ok-timestamp | When it was sent, in milliseconds since the epoch. |
| x-ok-agent | The policy's id. |
| x-ok-signature | sha256= followed by the hex HMAC-SHA256 of timestamp + "." + body with your secret. Sent only when the webhook has a secret. |
import crypto from "node:crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-ok-timestamp"];
const want = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
const got = headers["x-ok-signature"] || "";
const fresh = Math.abs(Date.now() - Number(ts)) < 5 * 60 * 1000;
return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}
{
"specVersion": "1.0",
"id": "dlv-4f2a9c1e",
"type": "rule_result.gained",
"at": "2026-10-14T09:12:44.180Z",
"useCase": { "id": "uc-3f9a1c", "title": "Retention policy" },
"logic": { "version": 3, "publishedAt": "2026-10-10T08:00:00.000Z" },
"rule": { "id": "r-7c21", "label": "Early-tenure complaints", "outcome": "Retention call", "entity": "Customer" },
"record": { "entity": "Customer", "key": "C-1042", "values": { "customerId": "C-1042", "tenureMonths": 4, "openComplaints": 3 } },
"cause": { "eventId": "ev-9b1c2e", "changes": { "openComplaints": { "before": 2, "after": 3 } } },
"evidence": [{ "clause": "openComplaints >= 2", "observed": 3 }]
}
MCP server
The same policy is an MCP server (Streamable HTTP, JSON-RPC 2.0), so an AI agent can ask it for decisions. It takes the same API key.
{
"mcpServers": {
"retention-policy": {
"type": "http",
"url": "https://okp.openknowra.ai/api/okp/mcp/<policyId>",
"headers": { "Authorization": "Bearer ok_live_xxxxxxxxxxxxxxxxxxxxxxxx" }
}
}
}
| Tool | What it does |
|---|---|
| describe_decisions | The entities, fields and rules of the deployed version. |
| decide | The decision for a record, as POST /decide. |
| get_decision | One record's decision with its evidence. |
| list_decisions | The decisions the version holds, filtered by entity or outcome. |
Pricing
Decisions are not metered: /decide, /decisions, /events, webhooks and
the MCP server carry no per-call charge. A policy reads from a context graph, which is billed under the
Context Compiler. For enterprise terms, see the
Logic Controller pricing.
Errors
Every error is { "error": "<code>", "message": "<what to do>" }.
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_entity | The entity is not one the policy decides on. The message lists the ones it does. |
| 400 | record_required | /decide was sent without a record. |
| 400 | record_not_found · bad_value · value_not_allowed · no_changes | /events: the graph holds no such record, a value is not of the field's type or not one of its allowed values, or nothing changed. |
| 401 | invalid_api_key | Missing, revoked or expired key, or a key for another policy. |
| 404 | not_found | No such policy, or the version holds no such record. |
| 409 | not_published | The policy has no deployed version yet. Deploy one in the console. |
| 413 | - | The body is over 2 MB. |
| 423 | paused | The policy is paused in the console. Calls resume when it is resumed. |
| 5xx | failed | Transient. Retry with exponential backoff. |