Skip to content

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:

bash
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 check

The 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:

bash
tenor serve approval.tenor --port 3000

Evaluate with Facts

Post a fact set and entity states to compute verdicts. Start with the review not yet passed:

bash
curl -s -X POST http://localhost:8080/evaluate \
  -H "Content-Type: application/json" \
  -d '{
    "facts": {
      "review_passed": false
    },
    "entity_states": {
      "Document": "draft"
    }
  }' | jq .
json
{
  "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:

bash
curl -s -X POST http://localhost:8080/evaluate \
  -H "Content-Type: application/json" \
  -d '{
    "facts": {
      "review_passed": true
    },
    "entity_states": {
      "Document": "draft"
    }
  }' | jq .
json
{
  "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

bash
curl -s "http://localhost:8080/actions?persona=operator" \
  -H "X-Facts: {\"review_passed\": false}" \
  -H "X-Entity-States: {\"Document\": \"draft\"}" | jq .
json
{
  "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:

bash
curl -s "http://localhost:8080/actions?persona=admin" \
  -H "X-Facts: {\"review_passed\": false}" \
  -H "X-Entity-States: {\"Document\": \"draft\"}" | jq .
json
{
  "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

bash
curl -s "http://localhost:8080/actions?persona=operator" \
  -H "X-Facts: {\"review_passed\": true}" \
  -H "X-Entity-States: {\"Document\": \"draft\"}" | jq .
json
{
  "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?

bash
curl -s "http://localhost:8080/actions?persona=operator" \
  -H "X-Facts: {\"review_passed\": true}" \
  -H "X-Entity-States: {\"Document\": \"approved\"}" | jq .
json
{
  "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.

bash
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:

bash
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.

bash
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 rule

Use --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:

bash
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 = true

And compute action spaces:

bash
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_ok

Add --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.