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 dbtInto the scaffold
modelsone objects/<model>.yml each; source.table binds to the model (from a manifest, to the materialized schema.alias)
columnsproperties, with data_type mapped to a property type — unmapped or missing types become string with a TODO comment saying so
unique + not_null testsidentity 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 testscommented 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/:

ArtefactContents
views/<object>.sqlThe SELECT for each object, in the adapter’s dialect, every identifier quoted
catalog.jsonObjects, properties (with sensitive), states, resolved relations, actions with their governance
mcp/manifest.jsonThe 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 pagetarget/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:

CheckWhat it catches
<object>.identity_uniqueAn 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_completeRows 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_coverageOnly 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

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

FlagEffect
--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-onlyExpose only the read tools, even with a role.
--allow-auto-effectsPermit actions declaring no approval requirement to execute. Off by default.
--allow-insecure-effectsPermit 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
FlagEffect
--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, --portBind 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
FlagEffect
--key <value>Identity value, for single-column identities.
--id name=valueOne identity part; repeat for composite identities.
--param name=value, -pAn 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-runEvaluate role and precondition, report, change nothing.
--allow-auto-effectsPermit 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.

FlagEffect
--note <text>Recorded in the audit trail.
--forceApprove even though the ontology changed since the proposal. Without it, a changed ontology is refused.
--allow-insecure-effectsAs 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.