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
- Concepts — the model behind what you just saw.
- Security model — what is enforced, and what is merely declared.
- MCP reference — every generated tool, in detail.