CLI reference
Every command accepts --project-dir <path> (default: the current
directory) and --help. The one exception is init, which takes the
directory as a positional argument, since there is no project yet.
ontologiq --version
Authoring
ontologiq init [directory]
Scaffolds a working example project — a small e-commerce ontology on DuckDB
with seed data, so build produces something to look at immediately.
Refuses to overwrite existing files, and refuses a path that exists and is not a directory.
ontologiq init shop
ontologiq new object <name>
Scaffolds objects/<name>.yml with the required fields filled in and the
optional blocks present as comments. The name must be snake_case: it becomes
both a SQL identifier and part of an MCP tool name.
ontologiq new object invoice
ontologiq import dbt <path>
Scaffolds objects/*.yml from a dbt project — a head start, not a
conversion. <path> is a dbt project directory, a compiled
target/manifest.json, or a single schema.yml.
ontologiq import dbt ../analytics # a dbt project directory
ontologiq import dbt ../analytics/target/manifest.json
What it reads, without importing dbt itself (the files are parsed as plain YAML/JSON, so there is no dbt runtime dependency and no version coupling):
| From dbt | Into the scaffold |
|---|---|
models | one objects/<model>.yml each; source.table binds to the model (from a manifest, to the materialized schema.alias) |
columns | properties, with data_type mapped to a property type — unmapped or missing types become string with a TODO comment saying so |
unique + not_null tests | identity candidates, as comments. The active identity: is left empty so validate fails until a human decides — a wrong identity turns get_<object> into a coin flip, so it is never guessed |
relationships tests | commented belongs_to relation suggestions with the join spelled out |
Existing files are never overwritten — re-running after you have edited a scaffold skips it and says so. Models whose names are not snake_case are skipped with a warning rather than renamed: renaming is a decision, not an import.
State and actions are deliberately absent from the scaffold. They are the part dbt cannot express — and writing them is the moment the ontology starts answering what may be done, by whom, with whose approval.
Compiling
ontologiq validate
Parses every file, resolves every cross-reference and checks every expression. Touches no database.
It reports all problems in one run, each with file, line and column, and
exits non-zero if any is an error. Among the things it catches: unknown
relation targets, circular property definitions, state names misspelled
inside a precondition, approval.when referencing something that is not a
parameter, effect URLs referencing a non-identity placeholder, duplicate
YAML keys, and MCP tool-name collisions.
objects/order.yml:32:15: actions.0.requires: 'oepn' is not a declared state
of order (declared: disputed, fulfilled, open)
ontologiq compile
Runs validate, then writes target/:
| Artefact | Contents |
|---|---|
views/<object>.sql | The SELECT for each object, in the adapter’s dialect, every identifier quoted |
catalog.json | Objects, properties (with sensitive), states, resolved relations, actions with their governance |
mcp/manifest.json | The tools an agent will see |
Touches no database. target/ is generated output: never edit it, and keep
it gitignored.
ontologiq docs
Renders the ontology as one self-contained HTML page —
target/docs/index.html, or wherever --output points.
ontologiq docs
ontologiq docs --output site/ontology.html
It shows the entity graph, every object with its properties, its states
with the predicate that computes each one in priority order, its
relations and join keys, the generated SQL view, each governed action with
the roles and approval gate that guard it, and — the part that matters —
the exact set of MCP tools each role would see, with the arguments each
one takes. Pick a role from the selector and the propose_ tools appear or
vanish, mirroring what serve --role exposes.
That last view is why this exists: it is the artefact you hand to whoever has to sign off on what an agent may do, and they will not read YAML.
No adapter, no connection, no credentials, no external requests: the page is
built from the compiled models and describes the ontology, never your
records. So it can be committed, diffed in a pull request — a review that
widens a role or drops an approval: required shows up as a readable change
— and sent to someone with no warehouse access at all.
Output is deterministic: the graph layout is computed at compile time, so regenerating an unchanged ontology produces a byte-identical file.
The page carries the same visual system as this documentation site, and a print stylesheet for the reviewer who wants it on paper.
Applying
ontologiq seed
Applies the SQL files listed under seeds: in ontologiq.yml. These are
dev and demo fixtures, not a migration system: they are your own
dialect-specific SQL, idempotency is your responsibility (write
CREATE OR REPLACE), and they run only when you ask.
Statements are split on top-level semicolons and executed one by one, so a failure names the file and the statement — the script is never re-rendered, so your SQL reaches the database byte for byte.
ontologiq run
Creates or replaces one view per object through the adapter.
run never executes seeds. Routine commands run no user-supplied write
SQL; that is what makes run safe to point at a real warehouse.
ontologiq test
Runs the data checks the ontology implies, against the warehouse:
| Check | What it catches |
|---|---|
<object>.identity_unique | An identity value shared by more than one row. The runtime already refuses ambiguous identities at call time; this finds them before an agent does. |
<object>.identity_complete | Rows with NULL in an identity column — unreachable by get/traverse and by every action. |
<fk_object>.<key>_references_<object> | Non-NULL foreign keys that match nothing: traverse_ would return silence that looks like an answer. A relation declared from both sides produces one check, not two. |
<object>.state_coverage | Only for state blocks without else: rows matching no predicate get a NULL state, and requires: state == '…' silently fails for them. |
Checks count, they never copy rows out — a failing check names how big the problem is without moving warehouse values into CI logs. The connection is read-only, like every serving process. Exit code is non-zero on any failure, so it slots into CI as-is.
ontologiq build
validate → compile → seed → run → test. The one command for the
inner loop. A failing data check fails the build after the views are
applied — the views are not the problem, the data contradicting the
ontology is.
docs is deliberately not part of build: build touches the database,
docs runs anywhere with no credentials, and folding it in would cost docs
that property.
Inspecting
ontologiq show <object> --key <value>
Prints one record: identity, resolved properties, computed state, and the actions the object accepts with their governance.
ontologiq show order --key 102
Single-column identities only, for now.
ontologiq debug
Loads the config and opens an adapter connection. The first thing to run when something is wrong.
Serving
ontologiq serve
Serves the ontology to AI agents over MCP — stdio by default, streamable
HTTP with --http. See the MCP reference for the tools and
security model for what is enforced.
| Flag | Effect |
|---|---|
--role <name> | stdio only: the role asserted for policy checks. Also read from ONTOLOGIQ_ROLE. Without it, propose_ tools are not exposed at all. |
--read-only | Expose only the read tools, even with a role. |
--allow-auto-effects | Permit actions declaring no approval requirement to execute. Off by default. |
--allow-insecure-effects | Permit plain-http effect URLs. Off by default; localhost is always allowed. |
Diagnostics go to stderr, because stdout is the JSON-RPC channel.
ontologiq serve --http
Network serving arrives with OIDC, not with a flag: --http requires an
identity provider, and there is no way to run it without one. Identity is
verified per request from the bearer token — which is why --role is
refused: the transport can finally verify what stdio could only assert.
ontologiq serve --http \
--oidc-issuer https://idp.example.com/realms/shop \
--oidc-audience ontologiq
| Flag | Effect |
|---|---|
--oidc-issuer <url> | The identity provider. Keys come from its published JWKS via OIDC discovery; https required (loopback exempt, for a local mock IdP). |
--oidc-audience <aud> | The audience tokens must carry. Without this check a token minted for any other service of the same issuer would open this one. |
--oidc-role-claim <claim> | Where the caller’s roles live in the token (default roles). The first value the ontology declares wins; a token with no declared role is served read-only, never guessed into one. |
--host, --port | Bind address (default 127.0.0.1:4500). |
--public-url <https://…> | Required when --host is not loopback: the URL your TLS-terminating reverse proxy exposes. Bearer tokens never ride plain http, so the port refuses to open without it. |
Per request: the role comes from the verified token’s claims, the audit trail and every proposal record the token subject — who the issuer says acted, not who launched the process — and sessions are bound to the credential that opened them. The MCP surface still has no approve capability, for any token: approval stays a separate verb.
ontologiq workbench
Serves the visual workbench on http://127.0.0.1:4400
(--port changes it, --open opens a browser): the entity graph, each
object’s definition next to its live records with computed state, an
action console with dry runs, the approval queue and the audit trail.
Read-only plus dry-run, by construction: no route proposes, approves or executes, and the socket binds loopback with no flag to widen it. Serving beyond the machine requires authentication, which is the OIDC milestone — not a flag.
Governing
ontologiq propose <object> <action>
Proposes a governed action as a human, from the terminal — the same path an agent takes over MCP: the role is checked, the precondition is evaluated against live data, and a proposal lands in the same durable queue. Nothing executes; approval is a separate verb, so the propose/approve split holds for humans too.
ontologiq propose order cancel --key 102 \
--param "reason=customer changed mind" --role support
| Flag | Effect |
|---|---|
--key <value> | Identity value, for single-column identities. |
--id name=value | One identity part; repeat for composite identities. |
--param name=value, -p | An action parameter. Repeatable. Values are coerced to the parameter’s declared type before any approval.when condition sees them. |
--role <name> | The role asserted, recorded in the audit trail. Also read from ONTOLOGIQ_ROLE. Required. |
--dry-run | Evaluate role and precondition, report, change nothing. |
--allow-auto-effects | Permit an action declaring no approval requirement to execute. Off by default, as for serve. |
ontologiq approvals list
Pending proposals: what was proposed, by which role, when, and when it expires. Values are rendered as JSON so nothing a model supplied can forge a line on your screen.
ontologiq approvals approve <id>
Approves a proposal and executes its effect. Deliberately outside the MCP surface: an agent must never be able to sign off its own proposal.
Before firing it re-verifies the arguments against the digest taken when the proposal was made, and re-evaluates the precondition — hours pass, and records move.
| Flag | Effect |
|---|---|
--note <text> | Recorded in the audit trail. |
--force | Approve even though the ontology changed since the proposal. Without it, a changed ontology is refused. |
--allow-insecure-effects | As for serve. |
ontologiq approvals reject <id>
Rejects a pending proposal. Terminal: a rejected proposal can never be approved.
ontologiq audit
The audit trail, newest last. --proposal <id> narrows it to one proposal,
--limit <n> bounds the output.
The log is append-only, enforced by database triggers. That stops accidental code, not an operator with write access to the file — see security.