jevql docs
playgroundGitHub ↗

jevql serve and the protocol

The local HTTP transport the SDKs use, and the JSON documents it speaks.

jevql serve runs the same executor as the CLI behind a tiny JSON API. It is what the TypeScript and Python SDKs talk to. By default they start a private one themselves; run it by hand when you want one shared engine, one connection and one warm cache for a whole team.

jevql serve --listen 127.0.0.1:7433 --token secret

Connection and TypeSafe settings come from the usual flags, environment variables and config file. --token (or JEVQL_TOKEN) requires Authorization: Bearer <token> on every request.

Endpoints

GET /v1/health

{"ok": true, "version": "0.1.0", "model": "jev-latest"}

POST /v1/query

{"sql": "SELECT name FROM people WHERE jev(people, 'could work from home')", "threshold": 0.7, "explain": false, "max_rows": 500}

Only sql is required. threshold and max_rows override the server defaults for that request. explain: true returns the plan without calling TypeSafe.

QueryResult

{
  "columns": ["name", "p"],
  "rows": [["Ada Fernandes", 0.93], ["Miguel Costa", 0.94]],
  "row_count": 2,
  "tag": "SELECT 2",
  "jev": true,
  "stats": {"collect_rows": 6, "judged": 6, "requests": 1, "cache_hits": 0, "input_tokens": 1200, "output_tokens": 40, "usd": 0.00005, "elapsed_ms": 1350},
  "explain": null
}

tag is the Postgres command tag, so row-less statements return INSERT 0 1 or CREATE TABLE with empty columns. stats is null for plain SQL. Values use the same canonical encoding as the cache: numbers as numbers, timestamps as RFC 3339 strings, bytea as base64, json columns as nested JSON.

More endpoints

The complete contract is served by every engine at GET /openapi.json as an OpenAPI 3.1 document. In short:

POST /v1/explain takes the same body as /v1/query and returns the Explain object only. No TypeSafe calls.

POST /v1/judge judges rows you already hold, with no database involved:

{"question": "is about billing", "kind": "noul", "rows": [{"subject": "Charged twice"}, {"subject": "API returns 500"}]}
{"answers": [{"p": 0.93, "pass": true, "confidence": 0.93}, {"p": 0.04, "pass": false, "confidence": 0.96}], "stats": {"judged": 2, "requests": 1, "cache_hits": 0, "usd": 0.00002, "...": "..."}}

kind is noul (default), choice with options, or score with ordered options; answers then carry choice and probabilities, or score and norm. Identical rows are judged once, and answers share the cache with SQL queries. --max-rows and --max-chars apply.

GET /v1/cache and DELETE /v1/cache report and clear the answer cache. They return 403 unless the server enables cache admin.

GET /v1/schema/tables lists user relations; GET /v1/schema/tables/{name} describes one, with columns and indexes, like \d.

Errors

{"error": "relation \"nope\" does not exist", "code": "sql"}
Status Code When
400 sql parse, rewrite or Postgres error
402 budget --max-rows or --max-chars fired; nothing was sent to TypeSafe
502 api TypeSafe returned an error
401 auth bad or missing bearer token
403 auth feature disabled on this server (cache admin)
404 sql relation not found (schema endpoints)

How the SDKs embed it

An SDK starts jevql serve --listen 127.0.0.1:0 --token <random> --ready-json --parent-pid <its pid>. The engine picks a free loopback port, prints one JSON line with the address, and exits by itself if the host process disappears. The SDK finds the binary via JEVQL_ENGINE_PATH, its bundled platform package, or PATH. Full contract in sdk/PROTOCOL.md.

The same documents from the CLI

jevql --json-table -c "SELECT 1; SELECT name FROM people WHERE jev(people, 'x')"

prints one QueryResult per statement, one per line. Errors go to stderr as text, with exit code 1 for SQL and 2 for API or budget errors. The SDKs’ CLI transport wraps exactly this, which is handy for scripts and notebooks where you do not want a server.

Edit this page on GitHub ↗