The evaluator is the read path of Tenor. It takes a contract, a set of facts, and entity states, and computes two things: verdicts (what is true) and an action space (what is permissible). It never mutates state — it only computes.
This guide uses the approval.tenor contract from the previous tutorial. If you have not built it yet, go back and do that first.
Start the HTTP Server
Launch the evaluator as an HTTP API:
tenor serve approval.tenor$ tenor serve approval.tenor
✓ Elaboration: 6 passes, 0 errors
✓ Static analysis passed (S1–S8)
Tenor evaluator listening on http://localhost:8080
Endpoints:
GET /elaborate — interchange JSON
POST /evaluate — compute verdicts
GET /actions — action space per persona
POST /simulate — dry-run flow execution
GET /health — health checkThe server loads your contract, elaborates it, runs static analysis, and starts listening. If analysis fails, the server will not start — you cannot serve a contract that does not pass all 8 checks.
To use a different port:
tenor serve approval.tenor --port 3000Evaluate with Facts
Post a fact set and entity states to compute verdicts. Start with the review not yet passed:
curl -s -X POST http://localhost:8080/evaluate \
-H "Content-Type: application/json" \
-d '{
"facts": {
"review_passed": false
},
"entity_states": {
"Document": "draft"
}
}' | jq .{
"verdicts": [],
"entity_states": {
"Document": "draft"
},
"evaluation_metadata": {
"rules_evaluated": 1,
"rules_fired": 0,
"strata_evaluated": 1,
"timestamp": "2026-03-01T10:00:00Z"
}
}No verdicts fired. The rule check_review evaluated review_passed = true against the provided value false and did not produce the review_ok verdict. Without that verdict, the approve_document operation cannot execute.
Now change the fact to true:
curl -s -X POST http://localhost:8080/evaluate \
-H "Content-Type: application/json" \
-d '{
"facts": {
"review_passed": true
},
"entity_states": {
"Document": "draft"
}
}' | jq .{
"verdicts": [
{
"name": "review_ok",
"rule": "check_review",
"stratum": 0,
"payload": { "type": "Bool", "value": true },
"provenance": {
"facts_used": ["review_passed"],
"fact_values": { "review_passed": true },
"timestamp": "2026-03-01T10:00:05Z"
}
}
],
"entity_states": {
"Document": "draft"
},
"evaluation_metadata": {
"rules_evaluated": 1,
"rules_fired": 1,
"strata_evaluated": 1,
"timestamp": "2026-03-01T10:00:05Z"
}
}The review_ok verdict is now active. Its provenance records exactly which facts contributed to it and what their values were. This is not a log entry — it is a structured derivation chain.
Compute the Action Space
The action space is the central output of Tenor evaluation. It answers: given these facts and entity states, what can each persona do right now?
When the review has NOT passed
curl -s "http://localhost:8080/actions?persona=operator" \
-H "X-Facts: {\"review_passed\": false}" \
-H "X-Entity-States: {\"Document\": \"draft\"}" | jq .{
"persona": "operator",
"available_actions": [],
"blocked_actions": [
{
"operation": "approve_document",
"reason": "Precondition not met: verdict review_ok is not present",
"missing_verdicts": ["review_ok"],
"entity": "Document",
"required_state": "draft",
"current_state": "draft"
}
],
"active_verdicts": []
}The operator has zero available actions. The approve_document operation is blocked because the review_ok verdict is not present. The response tells you exactly why — not just "blocked" but which specific verdict is missing.
Check the admin's action space:
curl -s "http://localhost:8080/actions?persona=admin" \
-H "X-Facts: {\"review_passed\": false}" \
-H "X-Entity-States: {\"Document\": \"draft\"}" | jq .{
"persona": "admin",
"available_actions": [
{
"operation": "reject_document",
"effects": [
{ "entity": "Document", "from": "draft", "to": "rejected" }
],
"satisfied_preconditions": ["true"]
}
],
"blocked_actions": [],
"active_verdicts": []
}The admin can reject the document. The precondition is true (always satisfied), and the document is in draft state. This action is available regardless of whether the review passed.
When the review HAS passed
curl -s "http://localhost:8080/actions?persona=operator" \
-H "X-Facts: {\"review_passed\": true}" \
-H "X-Entity-States: {\"Document\": \"draft\"}" | jq .{
"persona": "operator",
"available_actions": [
{
"operation": "approve_document",
"effects": [
{ "entity": "Document", "from": "draft", "to": "approved" }
],
"satisfied_preconditions": ["verdict_present(review_ok)"]
}
],
"blocked_actions": [],
"active_verdicts": [
{
"name": "review_ok",
"rule": "check_review",
"stratum": 0
}
]
}Now the operator has one available action: approve_document. The review_ok verdict is active, satisfying the precondition. The response includes the active verdicts that contributed to making this action available.
After the document is approved
What happens if you query the action space after the document has already been approved?
curl -s "http://localhost:8080/actions?persona=operator" \
-H "X-Facts: {\"review_passed\": true}" \
-H "X-Entity-States: {\"Document\": \"approved\"}" | jq .{
"persona": "operator",
"available_actions": [],
"blocked_actions": [
{
"operation": "approve_document",
"reason": "Invalid entity state: Document is in approved, requires draft",
"entity": "Document",
"required_state": "draft",
"current_state": "approved"
}
],
"active_verdicts": [
{
"name": "review_ok",
"rule": "check_review",
"stratum": 0
}
]
}The verdict is still active (the review still passed), but the operation is blocked because the Document is in approved, not draft. Entity state and verdict state are independent dimensions. Both must be satisfied.
What the Action Space Means
The action space is why Tenor exists for AI agents and automated systems.
Available actions are operations where three conditions are simultaneously satisfied: the persona is authorized, the precondition is met, and the entity is in the correct source state. These are the only actions that can be taken. Nothing outside this set is permissible.
Blocked actions are operations where the persona is authorized but something else is wrong. Each blocked action includes a specific reason — which precondition failed, which entity is in the wrong state, which verdict is missing. This gives agents diagnostic information without granting unauthorized capability.
Active verdicts provide context. They explain why certain actions are available and give agents visibility into the current evaluation state.
An agent operating inside Tenor cannot take an impermissible action — not because it is well-behaved, but because impermissible actions do not exist in the action space. Safety is structural, not behavioral.
Simulate a Flow
The simulate command does a dry-run flow execution. It evaluates every step, applies effects in memory, and produces the full provenance chain — but commits nothing. This is how you test flows before executing them for real.
tenor simulate approval.tenor \
--flow approval_flow \
--persona operator \
--facts '{"review_passed": true}' \
--entity-states '{"Document": "draft"}'$ tenor simulate approval.tenor \
--flow approval_flow \
--persona operator \
--facts '{"review_passed": true}' \
--entity-states '{"Document": "draft"}'
Flow: approval_flow
Snapshot: frozen at initiation
─────────────────────────────────────────────────────
Step 1: step_approve (OperationStep)
Operation: approve_document
Persona: operator ✓ authorized
Precondition: verdict_present(review_ok) ✓ satisfied
Entity: Document (draft → approved) ✓ valid transition
Outcome: approved → Terminal(success)
─────────────────────────────────────────────────────
Flow outcome: success
Steps executed: 1
Entity effects:
Document: draft → approved
Provenance:
approve_document
← verdict review_ok (stratum 0, rule: check_review)
← fact review_passed = true
[source: review_service, protocol: http]The provenance chain traces every decision back to its origin. The operation was authorized because the persona is operator. The precondition was satisfied because review_ok was active. review_ok was active because review_passed was true. review_passed came from review_service via HTTP. This chain is deterministic and reproducible.
Now simulate with the review not passed:
tenor simulate approval.tenor \
--flow approval_flow \
--persona operator \
--facts '{"review_passed": false}' \
--entity-states '{"Document": "draft"}'$ tenor simulate approval.tenor \
--flow approval_flow \
--persona operator \
--facts '{"review_passed": false}' \
--entity-states '{"Document": "draft"}'
Flow: approval_flow
Snapshot: frozen at initiation
─────────────────────────────────────────────────────
Step 1: step_approve (OperationStep)
Operation: approve_document
Persona: operator ✓ authorized
Precondition: verdict_present(review_ok) ✗ not satisfied
Error: "Review verdict is not present. Cannot approve."
─────────────────────────────────────────────────────
Flow outcome: failure
Steps executed: 1 (failed)
Entity effects: none
Failure reason: PreconditionFailed at step_approve
Missing verdict: review_ok
Rule check_review did not fire (review_passed = false)The flow fails at the first step. The error message comes from the operation's error contract — the exact string the contract author declared. No generic error. No stack trace. A domain-specific explanation of what went wrong and why.
Explain a Contract
The explain command produces a natural language summary of what the contract declares, suitable for review by non-technical stakeholders or for inclusion in documentation.
tenor explain approval.tenor$ tenor explain approval.tenor
Contract: approval
─────────────────────────────────────────────────────
This contract governs a document approval workflow with two roles
and one tracked entity.
Roles:
- admin: Can reject documents
- operator: Can approve documents (when review criteria are met)
Document Lifecycle:
A Document starts in "draft" state. It can be approved (moving to
"approved") or rejected (moving to "rejected"). Once approved or
rejected, no further transitions are possible.
Decision Logic:
The contract checks one external fact: whether the document review
has passed (sourced from the review_service via HTTP). When the
review passes, a "review_ok" verdict is produced. This verdict is
required before any approval can proceed.
Approval Rules:
- Only the operator can approve a document
- Approval requires the review_ok verdict (review must have passed)
- Only the admin can reject a document
- Rejection has no preconditions beyond the document being in draft
Workflow:
The approval_flow executes the approval as a single atomic step
with frozen-snapshot semantics. Facts are captured at flow
initiation and cannot change during execution.
Provable Properties:
- All 3 entity states are reachable (no dead states)
- Both operations are structurally satisfiable
- The admin cannot approve; the operator cannot reject
- All flow paths terminate
- Each verdict is produced by exactly one ruleUse --format markdown for Markdown output, or --verbose for a more detailed explanation including the full authority topology and flow path enumeration.
CLI Evaluation (Without the Server)
You do not need the HTTP server for one-off evaluations. The CLI can evaluate directly:
tenor evaluate approval.tenor --facts '{"review_passed": true}'$ tenor evaluate approval.tenor --facts '{"review_passed": true}'
Verdicts:
review_ok (stratum 0, rule: check_review)
payload: Bool = trueAnd compute action spaces:
tenor actions approval.tenor \
--persona operator \
--facts '{"review_passed": true}' \
--entity-states '{"Document": "draft"}'$ tenor actions approval.tenor \
--persona operator \
--facts '{"review_passed": true}' \
--entity-states '{"Document": "draft"}'
Available:
approve_document → Document: draft → approved
precondition: verdict_present(review_ok) ✓
Blocked: none
Active verdicts: review_okAdd --json to any CLI command for machine-readable JSON output instead of the formatted display.
Next Steps
You have evaluated a contract locally, explored the action space, simulated a flow, and explained the contract in natural language. To move from local evaluation to production execution with atomic state transitions and provenance storage, deploy to the platform.