Demo

This is Ontologiq end to end: a business object defined in YAML, compiled to SQL, served to an AI agent, and an action the agent can propose but never perform alone. Every output below is real — copy the commands and you will see the same thing.

Start from the example project:

pip install "ontologiq[serve]" ontologiq-duckdb
ontologiq init shop && cd shop && ontologiq build

1. One file describes the object

objects/order.yml, in full:

object: order
description: An order placed by a customer.

source:
  table: orders

identity: [order_id]

properties:
  - name: total
    type: decimal
    column: amount_eur
  - name: placed_at
    type: timestamp
    column: placed_at

# Declaration order is priority: a disputed order stays disputed even if
# it has not shipped yet.
state:
  disputed: dispute_opened_at is not null
  open: fulfilled_at is null
  else: fulfilled

relations:
  customer:
    type: belongs_to
    target: customer

actions:
  - name: cancel
    description: Cancel an order that has not shipped yet.
    requires: state == 'open'
    params:
      - name: reason
        type: string
        required: true
    policy:
      roles: [support]
    approval: required
    audit: true
    effect:
      type: webhook
      url: "{{ env.OPS_API }}/orders/{order_id}/cancel"

Four things are declared there that a table cannot express: a stable identity, a state derived from data rather than stored, a relation to another object, and an action with the rules that govern it.

2. The compiler turns it into SQL

target/views/order.sql, generated:

SELECT
  "t"."order_id" AS "order_id",
  "t"."amount_eur" AS "total",
  "t"."placed_at" AS "placed_at",
  CASE
    WHEN NOT "t"."dispute_opened_at" IS NULL
    THEN 'disputed'
    WHEN "t"."fulfilled_at" IS NULL
    THEN 'open'
    ELSE 'fulfilled'
  END AS "state"
FROM "orders" AS "t"

Ordinary SQL, in your warehouse’s dialect, with every identifier quoted — the demo has an object called order on purpose, which is a reserved word everywhere.

3. Give it to an AI agent

ontologiq serve --role support

The server speaks MCP over stdio. Point Claude Desktop at it by adding this to claude_desktop_config.json:

{
  "mcpServers": {
    "ontologiq": {
      "command": "ontologiq",
      "args": ["serve", "--role", "support", "--project-dir", "/path/to/shop"]
    }
  }
}

The agent now sees seven tools:

get_customer, list_customer, traverse_customer_orders,
get_order, list_order, traverse_order_customer,
propose_order_cancel

Ask it “who is customer 1 and what have they ordered?” and it calls get_customer then traverse_customer_orders:

{
  "rows": [{
    "customer_id": 1,
    "full_name": "Ada Lovelace",
    "country": "UK",
    "lifetime_value": "1240.50",
    "state": "active"
  }],
  "count": 1,
  "redacted_properties": ["email"],
  "note": "Properties marked sensitive in the ontology are not returned. They were never read from the database."
}

Note what is missing. email is marked sensitive: true in the ontology, so it was never selected — the value did not leave the warehouse, and the agent is told the field exists rather than left to hallucinate around a gap.

4. The agent proposes; it cannot act

The user asks to cancel order 102. The agent calls propose_order_cancel, and this is the whole of what it gets back:

{
  "outcome": "pending_approval",
  "proposal_id": "92e207a4-18d2-41b6-a555-dcce94ad3396",
  "expires_at": "2026-08-02T11:01:42.791Z",
  "would": {
    "type": "webhook",
    "method": "POST",
    "template": "{{ env.OPS_API }}/orders/{order_id}/cancel"
  },
  "note": "A human must approve this proposal before anything happens. You cannot approve it yourself; report the proposal id to the user."
}

Nothing happened. There is no tool on the MCP surface that can approve this, so there is no sequence of calls that gets the agent any further.

Before proposing it can also ask what would happen, with dry_run: true — an evaluation against live data that changes nothing.

When the precondition does not hold

Order 101 has already shipped. The agent gets a refusal that names the actual state, so it can tell the user something true instead of guessing:

{
  "error": "precondition_failed",
  "message": "order 101 does not satisfy 'state == 'open'' (state is 'fulfilled')",
  "state": "fulfilled"
}

When the role does not allow it

Start the server with --role analyst and the same call returns:

{
  "error": "denied_role",
  "message": "role 'analyst' may not call order.cancel (allowed: support)"
}

The warehouse was never touched. Start it with no --role at all and the propose_ tools are not even listed — an agent cannot call what it cannot see.

5. A human approves

In a different process, which the model has no way to reach:

ontologiq approvals list
92e207a4-18d2-41b6-a555-dcce94ad3396
  order.cancel  {"order_id": "102"}
  params: {"reason": "customer changed mind"}
  proposed by support (fernando) at 2026-08-01T11:01:42.791Z, expires 2026-08-02T11:01:42.791Z
ontologiq approvals approve 92e207a4-18d2-41b6-a555-dcce94ad3396 --note "checked with the customer"
executed: HTTP 204
The effect was applied.

At that moment, and not before, the webhook fired:

POST /orders/102/cancel
Idempotency-Key: 92e207a4-18d2-41b6-a555-dcce94ad3396
{
  "ontologiq": {
    "object": "order",
    "action": "cancel",
    "proposal_id": "92e207a4-18d2-41b6-a555-dcce94ad3396",
    "actor_role": "support",
    "actor_user": "fernando",
    "ontology_digest": "a43e5805798610ff"
  },
  "identity": {"order_id": "102"},
  "params": {"reason": "customer changed mind"}
}

Your system performs the cancellation. Ontologiq decided that it was allowed to, and recorded that it happened.

6. The trail

ontologiq audit --proposal 92e207a4-18d2-41b6-a555-dcce94ad3396
2026-08-01T11:01:42.791Z  proposed               order.cancel           support    pending_approval
2026-08-01T11:01:51.992Z  approved               order.cancel           support
2026-08-01T11:01:52.038Z  effect_intent          order.cancel           support
2026-08-01T11:01:52.135Z  effect_outcome         order.cancel           support    executed

The intent row is written before the call, so a process that dies mid-request still leaves evidence that something was attempted.

The part that matters most

Suppose the order ships while the proposal waits for a signature. Hours pass between an agent proposing and a human approving, and records move.

ontologiq approvals approve <id>
the precondition 'state == 'open'' no longer holds (state is now 'fulfilled')
— nothing was executed

The precondition is evaluated twice: once when proposing, so the agent gets a useful answer immediately, and again at execution, because the answer may have changed. That gap is where governance usually leaks, and closing it is most of why the runtime exists.

Where to go next