This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

Tutorials, how-to guides, reference material, and design notes for semantic jq: fuzzy JSON filters, bounded classification, deterministic jq execution, and explicit model-backed operators.

Everything you need to understand, evaluate, and use ajq.

ajq is usable today. The deterministic jq spine, semantic planning and execution, local inference (--backend local/mock), Ollama/OpenAI-compatible/Anthropic backends, cost controls, persistent judgement cache, local asset provisioning, model management, and release archives with a checksum-verifying install script are shipped. Scale-out/windowing remains a roadmap item. For the full picture, see Project status.

Start here

🎓 Tutorials

Run your first ajq pipeline and inspect how semantic JSON filters are planned.

🛠️ How-to guides

Install ajq, process JSON/NDJSON streams, write fuzzy filters, classify records, use cloud or local models, control costs, manage the cache, and configure defaults.

📚 Reference

CLI flags, configuration, backends, exit codes, I/O modes, semantic functions, and --explain fields.

💡 Explanation

Split execution, the determinism contract, the executor, architecture, and project status.

Not sure where to look?

1 - Tutorials

Start-to-finish ajq walkthroughs.

Run these in order. The second tutorial uses the setup from the first.

Available tutorials

  1. Your first ajq pipeline — install nothing, run a real jq query through ajq, and see byte-for-byte deterministic output.
  2. See how ajq plans a semantic query — write your first fuzzy predicate and use --explain to watch ajq split it into a deterministic part and a semantic part.

For task-focused instructions, see the how-to guides.

1.1 - Your first ajq pipeline

Run a real jq query through ajq and see deterministic, byte-for-byte output.

Run a real jq query through ajq. You’ll process a small JSON document, filter it, and confirm ajq matches jq for ordinary queries. No model, API key, or configuration.

Time: 5 minutes. Requires ajq on your PATH.

Step 1 — Get ajq

Use the checksum-verifying install script, a manual release download, or build from source (see Install ajq for details):

curl -fsSL https://raw.githubusercontent.com/ricardocabral/ajq/main/scripts/install.sh | sh

Check that it works:

ajq --version

This tutorial uses only deterministic jq, so there’s nothing else to set up — no model download, no API key.

Step 2 — Run a query

ajq reads JSON from standard input and applies the query you pass as its argument. Let’s pipe a small document in and pull out one field:

printf '{"user":{"name":"Ada","active":true}}' | ajq '.user.name'

Output:

"Ada"

ajq quotes the string exactly as jq would. It uses gojq under the hood.

Step 3 — Filter a list

Now something more interesting. Here’s a document with a list of users; we want the names of the active ones:

printf '{"users":[{"name":"Ada","active":true},{"name":"Grace","active":false}]}' \
  | ajq -r '.users[] | select(.active) | .name'

Output:

Ada

Two things happened:

  • .users[] | select(.active) | .name is an ordinary jq pipeline — iterate the users, keep the active ones, project the name.
  • The -r flag asked for raw output, so the string came out as Ada without quotes.

Step 4 — Prove it’s deterministic

Run the exact same command again:

printf '{"users":[{"name":"Ada","active":true},{"name":"Grace","active":false}]}' \
  | ajq -r '.users[] | select(.active) | .name'

You get Ada again, byte for byte. Ordinary jq parts are byte-reproducible. Semantic operators, added later, are the only parts that touch a model; everything else stays deterministic. You’ll see that in the next tutorial.

Result

  • ajq runs real jq queries and produces jq-identical output.
  • -r emits raw strings; without it, ajq emits JSON.
  • Ordinary pipelines are deterministic and reproducible.

Next

1.2 - See how ajq plans a semantic query

Write your first fuzzy predicate and watch ajq split it into deterministic and semantic parts with –explain.

Write a fuzzy match and inspect the plan. --explain does not contact a model, so you can inspect and cost any query before running it for real.

Requires ajq on your PATH (see Install ajq).

Step 1 — Write a fuzzy predicate

Ordinary jq can test text with test() or ==, but only literally. ajq adds a fuzzy operator, =~, that asks a model “does this value match this description?”. Here’s a stream of three messages, and a query that keeps the ones that read as urgent:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --explain '.[] | select(.msg =~ "urgent") | .msg'

--explain prints the plan instead of running the query.

Step 2 — Read the plan

You’ll see output like this:

ajq explain v1
query: ".[] | select(sem_match(.msg; \"urgent\")) | .msg"
execution: semantic split plan
deterministic: no
model_calls: input-dependent
backend_calls: input-dependent
byte_reproducible: cache-dependent
stdin: harvested for estimates
planned_call_sites: 1
semantic_predicates: 1
semantic_values: 0
estimates:
  estimate_status: available
  static_call_sites: 1
  input_frames: 1
  harvested_judgements: 3
  post_dedup_judgements: 2
  mock_judge_batches: 1
  over_harvest_bound: post_dedup_judgements == mock distinct judgements; may be a safe superset of execute-needed judgements
subgraphs:
  deterministic: jq outside semantic call sites
  semantic: 1 planned call site(s)
semantic_plan:
  - call_id: 1
    op: "sem_match"
    kind: "predicate"
    value_expr: ".msg"
    specs: ["urgent"]
    source_range: 13:38
    gated: n/a
    execution: 3-phase
    subgraph: semantic

Important lines:

The query was rewritten

query: ".[] | select(sem_match(.msg; \"urgent\")) | .msg"

You wrote .msg =~ "urgent", but the plan shows sem_match(.msg; "urgent"). The =~ operator is surface sugar: ajq desugars it into a plain jq function call before planning. Everything semantic is ultimately a sem_* function.

ajq found the semantic node

semantic_plan:
  - call_id: 1
    op: "sem_match"
    kind: "predicate"
    value_expr: ".msg"
    specs: ["urgent"]

There is exactly one semantic call site — the sem_match on .msg. Everything else in your query (.[], select, .msg projection) is deterministic jq. ajq has split the pipeline into a deterministic majority and a single fuzzy decision.

Deduplication is already visible

harvested_judgements: 3
post_dedup_judgements: 2

Your input had three items, but two of them have the same .msg value ("urgent"). ajq would collect 3 values, deduplicate them to 2 distinct ones, and only ever ask the model about those 2. Identical values cost one decision — this is how ajq keeps cost tied to the number of distinct fuzzy decisions, not the size of your input.

Step 3 — Change the input and watch the estimate move

Add another distinct message and re-run:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"},{"msg":"ping the on-call"}]' \
  | ajq --explain '.[] | select(.msg =~ "urgent") | .msg'

Now harvested_judgements becomes 4 and post_dedup_judgements becomes 3: one new distinct value to judge.

Result

  • =~ is fuzzy matching; ajq desugars it to sem_match(...).
  • --explain shows the plan without contacting a model — safe to run anytime.
  • ajq splits every query into a deterministic part and semantic call sites.
  • Deduplication means identical values are judged once.

Running it for real

Drop the --explain flag and select the deterministic mock backend so the tutorial stays network-free:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock '.[] | select(.msg =~ "urgent") | .msg'

Output:

"urgent"
"urgent"

Use --backend local or a cloud backend after following the matching how-to guide. --explain stays the way to inspect and budget a query before you spend real model calls.

Next

2 - How-to guides

Recipes for specific ajq tasks.

Use these when you know the task. New to ajq? Start with Your first ajq pipeline.

Guides

2.1 - Install ajq

Install ajq and provision the local backend assets.

ajq is a single Go binary. Pure jq queries work immediately after the binary is on your PATH. Semantic queries need an explicitly selected backend such as mock, local, ollama, openai, openrouter, or --cloud.

Install with the script

The install script downloads the matching release archive for your OS/architecture, verifies it against checksums.txt, and installs without sudo into ~/.local/bin (or $AJQ_INSTALL_DIR).

curl -fsSL https://raw.githubusercontent.com/ricardocabral/ajq/main/scripts/install.sh | sh

Pin a version or install directory if needed:

AJQ_VERSION=v0.1.0 AJQ_INSTALL_DIR="$HOME/bin" sh scripts/install.sh

Install with Homebrew

The release workflow publishes the ajq cask to the public ricardocabral/tap tap:

brew install --cask ricardocabral/tap/ajq

Install from a release archive

Download the archive for your platform from GitHub Releases, verify its SHA-256 digest against checksums.txt, then copy the ajq binary somewhere on your PATH.

Release archives are named like:

  • ajq_<version>_Darwin_arm64.tar.gz
  • ajq_<version>_Linux_x86_64.tar.gz
  • ajq_<version>_Windows_x86_64.zip

Windows MSI and WinGet status

Windows MSI packaging is implemented and CI-validated, including an installed-binary semantic smoke test, but the MSI is not yet released. ajq is not available through WinGet; it remains unavailable until a future MSI release completes Microsoft validation and merge and has public clean-install smoke evidence. Use the published Windows ZIP in the meantime.

Verify a download

Checksum verification and provenance verification are separate checks. The install script verifies the downloaded archive against checksums.txt; it does not run GitHub attestation verification for you.

For release assets published after provenance attestations are enabled, install the GitHub CLI and verify a downloaded archive or checksums.txt with:

gh attestation verify <downloaded-archive-or-checksums.txt> --repo ricardocabral/ajq

For manual downloads, also compare the archive SHA-256 digest with the matching entry in checksums.txt.

Build from source

With the Go toolchain (Go 1.26 or newer):

go install github.com/ricardocabral/ajq/cmd/ajq@latest

Or from a checkout:

git clone https://github.com/ricardocabral/ajq.git
cd ajq
go build -o ajq ./cmd/ajq
install -m 0755 ajq /usr/local/bin/ajq   # or ~/bin, etc.

Verify the binary

Run:

ajq --version
ajq --help

ajq --help should list the semantic backends and the shipped cache, daemon, models, and provision subcommands.

Provision local semantic assets

Skip this section if you only use pure jq queries, --backend mock, Ollama, or cloud backends.

--backend local needs a llama-server engine and an installed GGUF model under the ajq cache. Provision them explicitly:

ajq provision
ajq provision --check

ajq provision auto-installs checksum-pinned engine bundles on supported platforms (darwin/arm64, linux/amd64, and linux/arm64) and downloads the checksum-pinned default model. It also reuses assets you already have, including explicit overrides, cache entries, a legacy <cache>/bin/llama-server, or a llama-server on PATH.

Agent readiness probe

Use the status-only JSON probe when an agent needs to decide whether it can use the managed local backend. A missing asset deliberately exits 1 after writing its complete JSON result, so retain stdout instead of using a shell pipeline that discards it:

set +e
ajq provision --check --json > ajq-readiness.json
status=$?
set -e
cat ajq-readiness.json
# status=0 means ready; status=1 means inspect .actions and provision explicitly.

The document includes ready, engine/model presence and local paths, and ordered actions. It only inspects configuration, filesystem, and PATH state: it does not download assets, start a daemon, or contact a backend. --json is valid only with --check; use ajq provision separately to install the requested assets.

Package-manager status

The Homebrew cask is available from the public ricardocabral/tap tap. Windows MSI packaging is implemented and CI-validated, but the MSI is not yet released. WinGet remains unavailable until a future MSI release completes Microsoft validation and merge and has public clean-install smoke evidence.

The install script, manual release archives, and Go-source installation remain available when Homebrew is not the preferred installer.

Next steps

2.2 - Make ajq available to a coding agent

Make ajq discoverable in agent-visible project guidance so coding agents choose it for JSON and NDJSON semantic matching or bounded classification.

Use this guide when a team wants its coding agents to consider ajq before writing a bespoke JSON-processing script. It assumes ajq is installed for the environment where the agent runs and that the team already has a project-instruction mechanism its agents read.

1. Install ajq where the agent can execute it

Install ajq in the same execution environment as the coding agent, then verify that the binary is visible on that environment’s PATH:

command -v ajq
ajq --version
ajq capabilities --json

If command -v does not print an executable path, install ajq or add its install directory to the agent process’s PATH. Follow Install ajq for supported installation methods. ajq capabilities --json is static introspection: it does not load credentials, construct a backend, provision assets, or contact a network.

2. Add a JSON decision rule to agent-visible project guidance

Put the following guidance in the project instruction mechanism that your coding agents load before acting. Keep it with the project’s data-handling and command-execution rules so it is available when an agent is choosing between jq, ajq, and a new script.

### JSON and NDJSON processing

- Use `jq` for exact, structural JSON transformations.
- Use ajq for JSON or NDJSON semantic matching, or for classification into a
  bounded set of labels.
- Do not describe ajq as a general semantic extraction or redaction tool.

Before an ajq semantic run:

1. Run `ajq capabilities --json` and use its versioned contract to confirm the
   required operation and backend are available.
2. Validate the query shape with `--backend mock`; it is deterministic and
   makes no network or model calls.
3. Run `--explain` against representative input and review the semantic plan
   and estimated judgements.
4. Select a real backend explicitly only after review, and set a finite
   `--max-calls` cap. Do not rely on a default backend for a real semantic run.
5. Add `--no-cache` for confidential or one-off input when persistent cache
   reads and writes must both be bypassed.

This guide intentionally does not prescribe a particular agent, plugin, or MCP distribution mechanism. Use the project’s existing instruction surface until ajq selects supported delivery paths through its agent-skill distribution research.

3. Validate the guidance with safe commands

Use the mock backend to confirm that the agent can find ajq and form a semantic query without model, network, or API-key access:

printf '[{"id":1,"message":"refund requested"},{"id":2,"message":"profile updated"}]' \
  | ajq --backend mock -c '.[] | select(.message =~ "refund request") | .id'

The output is:

1

Then use representative, non-confidential input to inspect the plan before authorizing a model-backed run:

printf '[{"message":"refund requested"},{"message":"profile updated"}]' \
  | ajq --backend mock --explain '.[] | select(.message =~ "refund request") | .message'

--explain reports the semantic call site and estimated judgements without executing the query against a model backend. The mock backend checks query shape; it does not evaluate a query’s semantic quality on a production model.

4. Make real model use deliberate

After reviewing the plan and input scope, name the real backend and a finite call cap in the command. For confidential or one-off input, add --no-cache:

# Requires a provisioned local backend. The backend, cap, and cache choice are explicit.
printf '[{"message":"refund requested"},{"message":"profile updated"}]' \
  | ajq --backend local --max-calls 10 --no-cache \
      -c '.[] | select(.message =~ "refund request") | .message'

--no-cache disables persistent cache reads and writes; it is not a call limit and does not replace the data-handling review required for the selected backend. For cloud or other remote backends, follow your organization’s data policy before sending confidential input.

5. Verify the agent can make the right choice

Ask the agent to propose, without executing it, a command for each of these tasks:

  1. Select objects whose .status is exactly "open".
  2. Select support records whose text means a refund request.
  3. Route records into the fixed labels billing, bug, and account.

It should choose jq for the first task, and ajq’s explicit semantic matching or bounded classification for the other two. Before a real semantic run, its proposed command should name a backend and include a finite --max-calls value.

2.3 - Process an NDJSON stream

Handle newline-delimited JSON and raw log lines.

ajq auto-detects input framing. The common shapes:

Single JSON value

A single top-level JSON value is processed as one input frame:

printf '{"a":1}\n' | ajq -c '.a + 1'
# 2

NDJSON / JSON Lines

A stream of top-level JSON values (one per line, or concatenated) is processed one frame at a time — each value flows through the query independently, and results are emitted as they’re produced:

printf '{"a":1}\n{"a":2}\n' | ajq -c '.a, (.a + 10)'
# 1
# 11
# 2
# 12

There’s no whole-stream buffering in this mode, so it handles inputs larger than memory.

Semantic NDJSON windows

For a supported three-phase semantic query, ajq groups complete input frames into byte-budgeted windows before resolving semantic judgements. The default budget is 262144 bytes (256 KiB). ajq harvests every frame in one window, deduplicates and cache-resolves its judgements once, then executes and emits those frames in their original order.

A window never splits a JSON value or raw line. ajq retains at most the current window, one complete-frame lookahead, and bounded framing buffers—not the whole stream. A single record larger than the budget is accepted as a one-frame oversized window, so that record itself can require more memory than the configured budget.

Use a larger budget to find duplicates across more nearby records and reduce backend batches; the trade-off is that output waits until its full window has been harvested and resolved. Use a smaller positive budget when lower first-record latency matters but window-wide batching and deduplication are still useful:

# Keep semantic windows near 64 KiB for this invocation.
producer | ajq --backend local --window-bytes 65536 \
  'select(.message =~ "payment failure")'

Choose --stream for first-frame latency

Use --stream when a supported semantic pipeline must resolve and emit each frame without waiting for a window to fill:

producer | ajq --backend local --stream \
  'select(.message =~ "payment failure")'

--stream selects inline semantic execution for queries that otherwise use three-phase windows. It preserves input order, cache reads/writes and identity, schema validation, cancellation, output/exit behavior, and the run-global --max-calls cap. An uncached judgement resolves inline; a persistent cache hit still makes no backend call.

The latency trade-off is deliberate: inline execution has no window-wide backend batching or cross-frame pre-resolve deduplication. It can therefore make more backend round trips (and, with --no-cache, repeat a judgement in separate frames), while default windows can reduce those calls at the cost of waiting for the window. --stats names the selected mode and trade-off; --explain --stream reports that its harvest/dedup estimate is unavailable without consuming stdin.

Pure-jq queries ignore --stream and remain deterministic. Queries the planner already requires to interleave keep that existing inline behavior; --stream does not change their selection. --window-bytes does not affect either inline path.

Raw lines (awk mode)

To treat each input line as a plain string instead of JSON, use -R / --raw-input. The whole line becomes .:

printf 'error: disk full\nok: healthy\n' | ajq -R 'select(test("error"))'
# "error: disk full"

Combine -R with -r to keep the output unquoted:

printf 'error: disk full\nok: healthy\n' | ajq -R -r 'ascii_upcase'
# ERROR: DISK FULL
# OK: HEALTHY

Raw mode is also where the implicit-. form of semantic operators shines — e.g. select(. =~ "stack trace") matches log lines fuzzily. See Write a semantic filter.

Ignore stdin entirely

Use -n / --null-input to supply a single null frame and build output from scratch:

printf 'whatever' | ajq -n -c '{generated: true}'
# {"generated":true}

2.4 - Filter JSON by meaning

Find JSON records by intent or topic with ajq semantic filters, starting with a no-network mock backend and then switching to a real model.

Use this when exact string matching is too brittle: support tickets that say the same thing in different words, log messages that imply the same failure, or records where the field name is known but the wording is messy.

ajq keeps the surrounding jq pipeline deterministic. Only the explicit semantic predicate (=~, !~, or sem_match) asks a backend for a judgement.

Try the query with no network

The mock backend is deterministic and in-process, so it needs no model, network, or API key. It is useful for checking the jq shape before paying for model calls:

printf '[{"id":1,"msg":"payment failed during checkout"},{"id":2,"msg":"new avatar uploaded"}]' \
  | ajq --backend mock -c '.[] | select(.msg =~ "payment failed") | {id, msg}'

Expected output with the mock backend:

{"id":1,"msg":"payment failed during checkout"}

The predicate .msg =~ "payment failed" is shorthand for sem_match(.msg; "payment failed"). Everything else is ordinary jq.

Run the same filter on a real backend

After the command shape is correct, choose a backend that can make real semantic judgements:

# Managed local backend after `ajq provision`
printf '[{"id":1,"msg":"card declined at checkout"},{"id":2,"msg":"profile photo changed"}]' \
  | ajq --backend local --max-calls 10 -c '.[] | select(.msg =~ "payment failure") | {id, msg}'

# Anthropic cloud backend, using ANTHROPIC_API_KEY from the environment
printf '[{"id":1,"msg":"card declined at checkout"},{"id":2,"msg":"profile photo changed"}]' \
  | ajq --cloud --max-calls 10 -c '.[] | select(.msg =~ "payment failure") | {id, msg}'

Paid backends default to a 100-call cap. Setting a smaller --max-calls while you tune a query makes accidental large runs fail before crossing your budget.

Estimate, cap, and cache the work

Before running against a real model, ask ajq to explain the semantic plan:

printf '[{"msg":"payment failed"},{"msg":"payment failed"},{"msg":"avatar uploaded"}]' \
  | ajq --explain '.[] | select(.msg =~ "payment failure") | .msg'

The value to budget against is post_dedup_judgements: repeated values are judged once per backend/model/spec/value cache key. Successful judgements are stored in the persistent cache, so rerunning the same query can hit the cache instead of the backend. Use --no-cache only when you intentionally want to avoid reading or writing cached judgements.

Keep filters within shipped semantic shapes

For production filtering, prefer =~, !~, or sem_match. Standalone semantic extraction and redaction are registered but currently unsupported, so do not build a filter that assumes ajq can extract arbitrary fields or redact text as a standalone transform.

2.5 - Use ajq with other data formats

Feed ajq JSON produced by CLI, YAML, XML, TOML, or binary adapters.

ajq’s core input modes stay small on purpose: JSON, NDJSON, raw strings, and null input. When the source data is another format, put a format adapter before ajq and let ajq work on the JSON it emits.

This keeps pure jq-compatible ajq pipelines deterministic while still letting you use the existing jq ecosystem around it. Tools such as jc, yq/xq, and fq are commonly used alongside jq for this kind of conversion.

Convert command output with jc

Use jc when a system command prints text that you want to query as JSON:

df -h | jc --df | ajq -c '.[] | {filesystem, size, used_percent}'

Once jc emits JSON, ajq sees ordinary JSON values. Add semantic filters only when you explicitly want model-backed judgement:

ps aux | jc --ps | ajq --backend mock -c '.[] | select(.command =~ "database server")'

Convert YAML, XML, or TOML with yq and xq

Use yq for YAML or TOML files that should flow into an ajq query:

yq -o=json '.services' docker-compose.yml \
  | ajq -c 'to_entries[] | {name: .key, image: .value.image}'

Some yq distributions also provide xq for XML-to-JSON workflows:

xq '.rss.channel.item' feed.xml \
  | ajq -c '.[] | {title, link}'

Check your installed yq/xq command’s flags, because option names vary by implementation. The important contract is that the adapter writes JSON to stdout before ajq starts.

Convert binary-derived data with fq

Use fq when a binary or structured file format can be projected as JSON. For decoded values, use -V or tovalue when you need fq to write the JSON value instead of its display tree.

fq '.frames[0:10] | map(tobytesrange.start)' file.mp3 \
  | ajq -c '{frame_count: length, first_offset: .[0]}'

Keep the format-specific parsing in fq; keep jq and semantic operations in ajq after the JSON boundary.

Choose the ajq input mode after conversion

Most adapters emit a single JSON value, so the default ajq input mode is enough. If an adapter emits one JSON value per line, ajq processes it as NDJSON:

some-adapter --json-lines input.dat \
  | ajq -c 'select(.level == "error")'

If you only have raw text and do not want a JSON adapter, use ajq raw-input mode instead:

some-command | ajq -R -r 'select(test("error"))'

2.6 - Write a semantic filter

Use fuzzy =~ predicates and bounded semantic functions in jq pipelines.

Use a semantic filter when literal matching with test() or == is too brittle. The examples below use --backend mock so you can verify the command shape without a model or API key; switch to --backend local, --cloud, or another backend for real judgement.

Fuzzy match with =~

The =~ operator asks the backend whether a value matches a description. Use it inside select:

printf '[{"id":1,"feedback":"urgent"},{"id":2,"feedback":"other"}]' \
  | ajq --backend mock -c '.[] | select(.feedback =~ "urgent") | .id'

Output with the mock backend:

1

.feedback =~ "urgent" desugars to sem_match(.feedback; "urgent"). Negate with !~:

printf '[{"id":1,"feedback":"urgent"},{"id":2,"feedback":"other"}]' \
  | ajq --backend mock -c '.[] | select(.feedback !~ "urgent") | .id'

Match raw log lines

In raw-input mode, each input line is .:

printf 'panic stack trace\nok\n' \
  | ajq --backend mock -R -r 'select(. =~ "stack trace")'

Output:

panic stack trace

Classify into fixed labels

sem_classify(value; "a"; "b"; …) returns exactly one of the labels you provide:

printf '{"id":1,"text":"billing question"}' \
  | ajq --backend mock -c '{id, route: sem_classify(.text; "billing"; "bug"; "feature")}'

Output with the mock backend:

{"id":1,"route":"billing"}

Use a fixed label set when downstream jq should route records deterministically by shape.

Stay with shipped execution shapes

The current executor fully supports predicate matching (=~ / sem_match) and bounded classification (sem_classify). Unbounded value operators have narrower contracts in 0.0.1:

  • Use sort_by(sem_score(...)) when you need a semantic score as a sort key.
  • Use group_by(sem_norm(...)) when you need semantic normalization as a grouping key.
  • Score or normalization values that feed a pruning gate may run through interleaved fallback instead of the three-phase harvest path.
  • Standalone sem_extract(...) and sem_redact(...) currently report unsupported in three-phase execution.

For production filters, prefer sem_match or sem_classify unless you specifically need one of the limited score/normalization shapes above.

Check the plan first

Before running a semantic query on a paid or local model backend, inspect the estimate:

printf '[{"feedback":"urgent"},{"feedback":"urgent"},{"feedback":"other"}]' \
  | ajq --explain '.[] | select(.feedback =~ "urgent") | .feedback'

Then use Control semantic costs to set a cap and inspect stats.

Account for the persistent cache

Successful semantic judgements are cached by op/spec/model/value. A second run over the same values may skip backend calls and report cache hits. Use Manage the judgement cache when you need to inspect, bypass, or clear those entries.

2.7 - Classify JSON and NDJSON streams

Route JSON or NDJSON records into fixed semantic labels with sem_classify while preserving deterministic jq output shapes.

Use sem_classify when each record needs one label from a small list: support route, incident type, moderation bucket, or review theme. The output is bounded to the labels you write in the query, which makes it safer for downstream jq and shell pipelines than free text generation.

Classify a JSON array with no network

Start with --backend mock to validate the jq expression without a model, provider, or API key:

printf '[{"id":1,"text":"billing question"},{"id":2,"text":"bug report"}]' \
  | ajq --backend mock -c '.[] | {id, route: sem_classify(.text; "billing"; "bug"; "feature")}'

Expected output with the mock backend:

{"id":1,"route":"billing"}
{"id":2,"route":"bug"}

sem_classify returns exactly one of the labels from the call site. Keep labels short, mutually exclusive, and stable because they become part of your output contract.

Classify NDJSON one record at a time

ajq processes newline-delimited JSON as independent input frames, so it can classify streams without buffering the whole input:

printf '{"id":1,"text":"billing question"}\n{"id":2,"text":"bug report"}\n' \
  | ajq --backend mock -c '{id, route: sem_classify(.text; "billing"; "bug"; "feature")}'

This emits one compact JSON object per input record, which is convenient for queues, logs, and shell pipelines.

Route webhook events with bounded labels

If a webhook receiver writes each event body as NDJSON, add a stable route before passing the stream to downstream workers:

printf '{"event":"invoice.payment_failed","data":{"message":"billing payment failed"}}\n{"event":"issue.created","data":{"message":"bug report: export crashes"}}\n' \
  | ajq --backend mock -c '. + {route: sem_classify(.data.message; "billing"; "bug"; "other")}'

The command preserves each webhook event and adds a route value drawn from the three labels in the query. Replace the mock backend with a capped real backend when the event text requires semantic classification.

Switch to a real classifier backend

After the mock command produces the shape you need, run the same query with a real backend:

# Local model after `ajq provision`
printf '{"id":1,"text":"billing question"}\n{"id":2,"text":"bug report"}\n' \
  | ajq --backend local --max-calls 25 -c '{id, route: sem_classify(.text; "billing"; "bug"; "feature")}'

# OpenAI-compatible backend, using OPENAI_API_KEY from the environment
printf '{"id":1,"text":"billing question"}\n{"id":2,"text":"bug report"}\n' \
  | ajq --backend openai --model gpt-4.1-mini --max-calls 25 \
      -c '{id, route: sem_classify(.text; "billing"; "bug"; "feature")}'

For paid backends, keep --max-calls at or below your budget. Paid/cloud backends also have a default 100-call guardrail when you do not set one explicitly.

Account for repeated values and cache hits

Semantic classification is cache-keyed by backend, model, operation, label set, and input value. If many records contain the same text, ajq deduplicates the judgements before it calls the backend, and later runs can reuse the persistent cache.

Estimate the distinct judgement count first:

printf '{"text":"billing question"}\n{"text":"billing question"}\n{"text":"bug report"}\n' \
  | ajq --explain '{route: sem_classify(.text; "billing"; "bug"; "feature")}'

Then add --stats to the real run when you want to see post-dedup backend calls and cache hits on stderr.

2.8 - Estimate model calls before running

Use –explain to budget cost and latency before executing a semantic query.

Semantic queries cost one backend judgement per distinct value they judge. Use --explain with representative input to estimate that number before you run a paid or slow backend.

Run --explain with real input

Pipe a sample of your data into --explain. ajq runs a deterministic mock harvest over the input to estimate work — it never contacts a provider or local model:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --explain '.[] | select(.msg =~ "urgent") | .msg'

Read the estimate block

estimates:
  estimate_status: available
  static_call_sites: 1
  input_frames: 1
  harvested_judgements: 3
  post_dedup_judgements: 2
  mock_judge_batches: 1

The fields you’ll budget against:

FieldMeaning
harvested_judgementsTotal values collected before deduplication.
post_dedup_judgementsDistinct semantic judgements after deduplication — the estimated backend-call count.
mock_judge_batchesHow many backend batch calls the mock estimate path used.
static_call_sitesNumber of semantic call sites in the query, not input size.

post_dedup_judgements is the number to compare with --max-calls. Paid backends default to a 100-call cap; local, Ollama, and mock default to unlimited.

Turn the estimate into a cap

If the estimate is acceptable, set a cap when you run the real backend:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock --max-calls 2 '.[] | select(.msg =~ "urgent") | .msg'

For the full estimate → cap → stats workflow, see Control semantic costs.

Estimate for your whole dataset

The estimate is based on the input you pass to --explain. To project a full run, either pipe in the full dataset or sample representative records and extrapolate from your distinct-value ratio. Because ajq deduplicates by value, repeated values cost less than new values.

Remember the persistent cache

--explain estimates the semantic work implied by the input and query. A real run may issue fewer backend calls if matching judgements are already in the persistent cache. Use --stats on the real run to see cache_hits and post_dedup_backend_calls.

When estimates are unavailable

If the input is empty or invalid, or the query selects a currently unsupported semantic execution shape, --explain keeps the static plan and marks estimate_status unavailable with a stable reason. The plan (call sites, ops, specs) is still shown.

2.9 - Use ajq safely from coding agents

A safe workflow for agents that need semantic jq queries: start with mock and –explain, cap backend calls, and avoid unsupported extraction claims.

Coding agents often discover ajq while trying to answer a concrete data-cleaning question: “find the records that mean X” or “route these JSON lines into labels”. Use this workflow when an agent, script, or CI job needs semantic matching while keeping the jq parts deterministic and reviewable.

1. Keep ordinary jq deterministic

First write the structural jq pipeline without semantic calls. This step should not need a backend:

printf '[{"id":1,"msg":"refund requested"},{"id":2,"msg":"profile updated"}]' \
  | ajq -c '.[] | {id, msg}'

Only add semantic operators once the field selection and output shape are correct.

2. Add semantics with the mock backend

Use --backend mock for the first semantic run. It is deterministic, in-process, and does not use network access, cloud credentials, or local model assets:

printf '[{"id":1,"msg":"refund requested"},{"id":2,"msg":"profile updated"}]' \
  | ajq --backend mock -c '.[] | select(.msg =~ "refund request") | {id, msg}'

Expected output with the mock backend:

{"id":1,"msg":"refund requested"}

For routing tasks, keep outputs bounded with sem_classify:

printf '{"id":1,"msg":"billing question"}\n{"id":2,"msg":"bug report"}\n' \
  | ajq --backend mock -c '{id, route: sem_classify(.msg; "billing"; "bug"; "account")}'

3. Explain before executing with a model

Before a real backend run, inspect the plan and estimated judgement count:

printf '[{"msg":"refund requested"},{"msg":"refund requested"},{"msg":"profile updated"}]' \
  | ajq --explain '.[] | select(.msg =~ "refund request") | .msg'

Review the semantic call sites, specs, and post_dedup_judgements. The estimate path uses the mock backend and does not contact a provider.

4. Cap and observe the real run

When the estimate is acceptable, switch only the backend-related flags:

# Managed local backend after `ajq provision`
printf '[{"msg":"refund requested"},{"msg":"profile updated"}]' \
  | ajq --backend local --max-calls 10 --stats \
      -c '.[] | select(.msg =~ "refund request") | .msg'

# Anthropic cloud backend, using ANTHROPIC_API_KEY from the environment
printf '[{"msg":"refund requested"},{"msg":"profile updated"}]' \
  | ajq --cloud --max-calls 10 --stats \
      -c '.[] | select(.msg =~ "refund request") | .msg'

--max-calls is the hard budget. --stats reports harvested judgements, post-dedup backend calls, cache hits, and elapsed time on stderr after a successful run. Paid/cloud backends default to a 100-call cap, but agents should still set an explicit cap for the current task.

5. Reuse or bypass the cache deliberately

Successful semantic judgements are cached by backend, model, operation, spec, and canonical value. This improves repeated agent runs over the same data, but sensitive or one-off workflows may prefer --no-cache to avoid cache reads and writes:

printf '[{"msg":"refund requested"}]' \
  | ajq --backend mock --no-cache -c '.[] | select(.msg =~ "refund request") | .msg'

Use cache commands when an agent needs to inspect or clear local state:

ajq cache status
ajq cache clear

Guardrails for generated docs and prompts

  • Do say ajq is useful for fuzzy JSON filtering, semantic grep over JSON/NDJSON, and bounded classification.
  • Do not claim standalone semantic extraction or redaction is supported today.
  • Do not hide model calls behind pure jq examples; semantic work should always be visible in the query and backend flags.
  • Do not put API keys in commands, logs, or checked-in files. Paid backend credentials come from environment variables.

2.10 - Use cloud backends

Run semantic queries against Anthropic, OpenAI, or OpenRouter.

Use a cloud backend when you want hosted inference instead of the local llama-server backend. Each provider needs an API key in the environment; ajq does not read API keys from config.toml.

Use Anthropic Claude

  1. Export an Anthropic API key:

    export ANTHROPIC_API_KEY="sk-ant-..."
    
  2. Run a semantic query with --cloud:

    printf '{"msg":"urgent billing issue"}' \
      | ajq --cloud --model haiku '.msg =~ "urgent"'
    

    --cloud is the same as --backend anthropic. If you omit --model, ajq uses claude-haiku-4-5. The shipped aliases are haiku, sonnet, and opus.

  3. If the key is missing, ajq stops before contacting the provider:

    ajq: error: anthropic backend API key is empty; set ANTHROPIC_API_KEY
    

Use OpenAI

  1. Export an OpenAI API key:

    export OPENAI_API_KEY="sk-..."
    
  2. Choose an OpenAI model explicitly:

    printf '{"msg":"urgent billing issue"}' \
      | ajq --backend openai --model gpt-4o-mini '.msg =~ "urgent"'
    
  3. If you use an OpenAI-compatible proxy, pass its API root with --base-url:

    ajq --backend openai --base-url http://127.0.0.1:8000/v1 --model my-model \
      '.msg =~ "urgent"'
    

Use OpenRouter

  1. Export an OpenRouter API key:

    export OPENROUTER_API_KEY="sk-or-..."
    
  2. Choose an OpenRouter model id:

    printf '{"msg":"urgent billing issue"}' \
      | ajq --backend openrouter --model openai/gpt-4o-mini '.msg =~ "urgent"'
    

    OpenRouter uses https://openrouter.ai/api/v1 by default.

Keep a spending guardrail

Paid backends (anthropic, openai, and openrouter) default to a 100-judgement cap. Lower it while testing:

ajq --cloud --max-calls 10 '.[] | select(.msg =~ "urgent")'

See Control semantic costs for the full estimate/cap/stats workflow.

2.11 - Control semantic costs

Estimate semantic work, cap backend calls, and inspect run statistics.

Use this workflow before running semantic queries on a paid backend.

1. Estimate calls with --explain

Run the query with representative input and --explain:

printf '[{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --explain '.[] | select(.msg =~ "urgent") | .msg'

Read post_dedup_judgements in the estimate block:

estimates:
  estimate_status: available
  static_call_sites: 1
  input_frames: 1
  harvested_judgements: 2
  post_dedup_judgements: 2

That number is the count to budget against. Repeated values are deduplicated before the backend is called.

2. Add a hard cap with --max-calls

Set the cap at or below your budget. ajq aborts before issuing the call that would cross it:

printf '[{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock --max-calls 1 '.[] | select(.msg =~ "urgent") | .msg'

With two distinct judgements and a cap of one, the run fails safely:

ajq: error: query ".[] | select(.msg =~ \"urgent\") | .msg" runtime error in frame 1: max calls cap exceeded: cap 1, run needs 2 post-dedup backend judgements; aborting before issuing backend call 2.

Paid backends (anthropic, openai, and openrouter) default to --max-calls 100. Use --max-calls 0 only when you intentionally want no cap. --backend-concurrency changes only how many requests from an already-approved batch can be in flight; it does not change this count or reserve extra calls. Keep paid providers at the sequential default of 1 while establishing a rate-limit budget, then use at most 2 only if the provider’s account limits and retry capacity allow it.

3. Inspect a successful run with --stats

After choosing a cap, run with --stats to print a summary to standard error:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock --stats '.[] | select(.msg =~ "urgent") | .msg'

The data still goes to standard output:

"urgent"
"urgent"

The stats show harvested work, post-dedup backend calls, cache hits, and elapsed time:

ajq stats:
  input_frames: 1
  semantic_call_sites: 1
  harvested_judgements: 3
  post_dedup_backend_calls: 2
  cache_hits: 0
  elapsed: ...

For paid models in ajq’s pricing table, stats also include an estimated USD cost. Treat it as a budgeting estimate, not a provider bill.

2.12 - Install the ajq coding-agent skill

Install the ajq skill through the native Codex marketplace or the optional Claude Code and Cursor adapter.

Install the ajq skill when an agent should route semantic JSON or NDJSON filtering and bounded classification work to ajq. Install ajq itself first so the skill can run its safe discovery and mock checks.

Install for Codex

Add the public ajq marketplace and install its plugin:

codex plugin marketplace add ricardocabral/ajq
codex plugin add ajq@ajq

For a reproducible environment, pin the marketplace to an ajq release tag that contains this plugin. Replace vX.Y.Z with that release tag:

codex plugin marketplace add ricardocabral/ajq --ref vX.Y.Z
codex plugin add ajq@ajq

Start a new Codex task after installation. Confirm that Codex can see the plugin before relying on it:

codex plugin list

Share it in a local workspace

When a team has a checkout of this repository, point Codex at that checkout instead of a remote ref:

codex plugin marketplace add /path/to/ajq
codex plugin add ajq@ajq

The marketplace artifact lives in .agents/plugins/marketplace.json and the plugin itself in plugins/ajq. Keep both paths together when sharing a branch or archive.

Provision it in CI

Use a CI-specific CODEX_HOME, add the marketplace, and install the plugin before starting Codex. Pin a release tag in reproducible jobs:

export CODEX_HOME="$RUNNER_TEMP/codex-home"
codex plugin marketplace add ricardocabral/ajq --ref vX.Y.Z
codex plugin add ajq@ajq
codex plugin list

Installing the skill does not select a semantic backend or grant access to a model. The skill begins with ajq capabilities --json, --backend mock, and --explain; any real backend still needs an explicit choice and finite --max-calls value.

Optional adapter for Claude Code and Cursor

The third-party plugins installer can discover the same package from this repository:

npx plugins add ricardocabral/ajq

Its current targets are Claude Code and Cursor. It is not the native Codex installation path; use the Codex marketplace commands above for Codex. Preview what the adapter finds without installing it:

npx plugins discover ricardocabral/ajq

After installing through either path, use the skill’s workflow: capabilities, mock, explain, then an explicitly capped real backend. See the agent-safe semantic workflow for commands and current operator limits.

2.13 - Manage the judgement cache

Inspect, bypass, and clear ajq’s persistent semantic judgement cache.

ajq stores successful semantic judgements on disk so repeated runs over the same op/spec/model/value identity can skip backend calls.

See where the cache lives

Run:

ajq cache status

Example output:

location: /Users/you/Library/Caches/ajq/judgements
entries: 0
bytes: 0

Set AJQ_CACHE_DIR when you want an isolated cache for a project or test:

AJQ_CACHE_DIR="$PWD/.ajq-cache" ajq cache status

Inspect cache state from an agent

Use the versioned JSON probe instead of parsing the human table-like output:

ajq cache status --json
# {"schema_version":"1","availability":"available","path":".../judgements","entries":0,"bytes":0}

availability: "available" includes a missing cache directory (zero entries). If ajq cannot inspect the configured location, it writes a complete document with availability: "unavailable", zero counts, and error: "status_unavailable", then exits non-zero. Paths are intentional local-state output; the document does not include cache values, prompts, credentials, or raw filesystem errors.

Re-run a semantic query for free

Run a semantic query once with stats:

printf '[{"msg":"urgent"},{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock --stats '.[] | select(.msg =~ "urgent") | .msg'

The first run performs one backend judgement per distinct value:

ajq stats:
  harvested_judgements: 3
  post_dedup_backend_calls: 2
  cache_hits: 0

Run the same command again with the same cache root. The output is the same, but the judgements are cache hits:

ajq stats:
  harvested_judgements: 3
  post_dedup_backend_calls: 0
  cache_hits: 3

Bypass the cache for one run

Use --no-cache when the judged values should not be read from or written to disk:

printf '[{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --backend mock --no-cache --stats '.[] | select(.msg =~ "urgent") | .msg'

You can also set no_cache = true in config.toml; use the flag when you only need the bypass once.

Clear cached judgements

Run:

ajq cache clear

Example output:

location: /Users/you/Library/Caches/ajq/judgements
cleared_entries: 2
freed_bytes: 422

cache clear removes judgement entries only. It does not remove provisioned engines or local GGUF models.

Privacy note

Cache files contain the values that semantic operators judged. Do not share the cache folder if those values are sensitive. Use --no-cache or a disposable AJQ_CACHE_DIR for one-off processing of private data.

2.14 - Configure defaults

Set backend, model, base URL, cost cap, and cache defaults in config.toml.

Use a config file when you do not want to repeat the same semantic-backend flags on every command.

1. Create the config file

ajq reads this path by default:

${XDG_CONFIG_HOME:-~/.config}/ajq/config.toml

Create the directory and file:

mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/ajq"
cat > "${XDG_CONFIG_HOME:-$HOME/.config}/ajq/config.toml" <<'EOF'
backend = "mock"
max_calls = 25
no_cache = false
EOF

For a one-off location, set AJQ_CONFIG to an explicit file path.

2. Add semantic defaults

Recognized keys are:

backend = "local"              # mock, local, ollama, openai, openrouter, anthropic
model = "qwen2.5-1.5b"
base_url = "http://127.0.0.1:8081"
max_calls = 100                 # 0 = unlimited
no_cache = false

For example, to default to the deterministic mock backend with a small call cap:

backend = "mock"
max_calls = 1
no_cache = true

Then a semantic query can omit --backend:

printf '[{"msg":"urgent"},{"msg":"other"}]' \
  | ajq '.[] | select(.msg =~ "urgent") | .msg'

With max_calls = 1, that example stops before making the second distinct judgement.

3. Override a default when needed

Precedence is:

  1. Command-line flags
  2. AJQ_* environment variables
  3. config.toml
  4. Backend defaults

A flag wins for one command:

printf '[{"msg":"urgent"},{"msg":"other"}]' \
  | ajq --max-calls 0 '.[] | select(.msg =~ "urgent") | .msg'

Environment variables are useful for a shell session or CI job:

export AJQ_BACKEND=mock
export AJQ_MAX_CALLS=0

The config file still supplies any setting that the flag or environment did not override.

4. Keep API keys out of config

API keys are environment-only:

export ANTHROPIC_API_KEY="sk-ant-..."
export OPENAI_API_KEY="sk-..."
export OPENROUTER_API_KEY="sk-or-..."

If a config file contains credential-looking keys such as api_key, apikey, or token, ajq rejects it:

ajq: error: config key "api_key" looks like a credential; API keys are env-only

2.15 - Use a larger local model

List, download, and select a larger pinned GGUF model for the local backend.

The local backend defaults to qwen2.5-1.5b. Use ajq models when you want one of the larger shipped catalog models.

1. List catalog models

Run:

ajq models list

Example output before any downloads:

NAME          ACTIVE  INSTALLED  SIZE    RAM
qwen2.5-1.5b  *       no         1.2GiB  ~4 GiB RAM
qwen2.5-3b            no         2.0GiB  ~6 GiB RAM
qwen3-4b              no         2.3GiB  ~8 GiB RAM

ACTIVE shows the model selected by config/env/defaults. INSTALLED shows whether the GGUF file exists in the ajq cache.

2. Download the model you want

Choose a catalog name and pull it:

ajq models pull qwen2.5-3b

The pull uses checksum-pinned public URLs and stores the model under the ajq cache. For a project-local cache, set AJQ_CACHE_DIR before pulling:

AJQ_CACHE_DIR="$PWD/.ajq-cache" ajq models pull qwen2.5-3b

3. Select the installed model

Persist the selected local model:

ajq models use qwen2.5-3b

If the model is not installed yet, ajq tells you what to run:

ajq: error: model qwen2.5-3b is not installed; run `ajq models pull qwen2.5-3b` first

After the model is installed, models use writes the model name to config.toml:

active model set to qwen2.5-3b in /Users/you/.config/ajq/config.toml

The resulting config entry is:

model = "qwen2.5-3b"

models use preserves unrelated config keys, but it does not preserve comments or the original formatting.

4. Run with the local backend

Use the selected model with the local backend:

printf '{"msg":"urgent billing issue"}' \
  | ajq --backend local '.msg =~ "urgent"'

You can override the configured model for one command:

ajq --backend local --model qwen3-4b '.msg =~ "urgent"'

3 - Reference

CLI flags, configuration, backends, functions, and output formats.

Lookup pages for ajq behavior and interfaces.

For procedures, see the how-to guides. For design rationale, see explanation.

3.1 - Command-line interface

ajq invocation, global flags, subcommands, and exit codes.

Synopsis

ajq [query] [flags]
ajq [command]

ajq reads input from standard input, applies one jq/semantic query, and writes results to standard output. Errors are written to standard error and produce a non-zero exit code.

query is a single argument. ajq desugars =~/!~ to sem_match(...), parses with gojq, and executes through the selected backend only when semantic operators require one.

Global flags

FlagShortDescription
--backend <name>Select the semantic backend: anthropic, local, mock, ollama, openai, or openrouter.
--base-url <url>Set the HTTP base URL for backends that use one (local, ollama, openai, openrouter). For openai/openrouter, custom URLs must use HTTPS unless the host is loopback (127.0.0.1, localhost, or [::1]).
--backend-concurrency <N>Maximum in-flight semantic requests in one provider batch. Default 1; maximum 2 for OpenAI-compatible/Anthropic and 4 for Ollama. Must be positive when set.
--cloudSelect the Anthropic cloud backend; equivalent to --backend anthropic.
--compact-output-cEmit compact JSON output.
--exit-status-eSet the exit status based on the last output value.
--explainPrint the deterministic/semantic execution plan and exit without executing the query.
--max-calls <N>Maximum post-dedup backend judgements before aborting; 0 means unlimited. Paid backends default to 100; local/Ollama/mock default to unlimited.
--model <id-or-alias>Semantic model id or alias for the selected backend.
--no-cacheDisable persistent on-disk judgement cache reads and writes for this run.
--null-input-nUse a single null input value instead of reading stdin.
--raw-input-RRead each input line as a string.
--raw-output-rEmit strings without JSON quoting.
--statsPrint run statistics to stderr after a successful run.
--streamRun supported semantic queries inline for low-latency frame output instead of window batching. Does not change backend, model, cache identity, or --max-calls; pure-jq and planner-required inline queries retain their existing behavior.
--window-bytes <N>Maximum source bytes per supported three-phase semantic window; positive integer, default 262144. Has no execution effect for pure-jq or inline queries, including --stream.
--version-vPrint the version and exit.
--help-hPrint usage and exit.

See Input and output modes for framing and formatting details, Backends for backend-specific defaults, and Configuration for env/config precedence.

Help-first agent probe

For machine consumers, begin with ajq capabilities --json, before examples or query execution. It is static introspection: it does not load config or credentials, construct a backend, start a daemon, inspect/provision assets, or contact the network. Its versioned schema_version: "1" document is the stable contract for input/output modes, semantic-function availability, backend defaults, cost/cache/provisioning notes, safety guarantees, and discovery commands. The plain ajq capabilities summary, ajq_version, and human backend descriptions are informational build text; all other documented v1 fields and enum values are stable. Consumers must use schema_version as the contract discriminator and ignore unknown fields and future enum values for compatible v1 builds.

After capabilities discovery, run ajq examples for categorized, copy-pasteable safe workflows. ajq examples [topic] can show one of pure-jq, semantic-filter, explain, classification, or ndjson; its semantic commands explicitly use --backend mock and therefore need no model, network, or API key. It is the source of the detailed snippets; this reference keeps only the workflow overview. The root help shows the same three safe first workflows:

# Pure jq remains deterministic and does not construct a backend.
printf '{"users":[{"name":"Ada"}]}' | ajq -r '.users[].name'

# --backend mock is the safe semantic-agent probe: deterministic, no model,
# no network access, and no API key.
printf '[{"id":1,"msg":"please keep this"}]' \
  | ajq --backend mock -c '.[] | select(.msg =~ "keep") | .id'

# Review the semantic plan and estimated backend calls before execution.
printf '[{"msg":"refund demanded"}]' \
  | ajq --backend mock --explain '.[] | select(.msg =~ "angry/frustrated") | .msg'

The mock backend is appropriate for checking query shape and split-execution behavior; it is not a substitute for evaluating a query’s semantic quality on a production model. --explain exits before query execution and does not contact a model backend.

Discovery command help also includes safe inspection examples: ajq provision --check reports missing local assets without downloading them, ajq models list shows local model availability, ajq cache status inspects the local judgement cache, and ajq daemon status checks daemon state without starting a model.

JSON state probes

Coding agents can request versioned local-state documents with command-local --json flags. Every document has schema_version: "1", is one JSON document on stdout, and has a trailing newline. Consumers should use the schema version, ignore unknown future fields/enums, and keep stdout even when a readiness check has a non-zero exit status.

  • ajq models list --json returns active and ordered models. active.state is catalog, path_like, or unknown; name occurs only for catalog and path only for path_like. Each catalog row contains name, active, installed, filename, path, size_bytes, and ram.
  • ajq cache status --json returns availability (available or unavailable), path, entries, and bytes. An unavailable local cache also has error: "status_unavailable" and exits non-zero; it never exposes a raw filesystem error.
  • ajq provision --check --json returns platform, ready, engine, model, and ordered actions. Assets contain identity, present, path, and source when present; sources include override, bundle, legacy_cache, path, cache, and unknown. Missing readiness produces a complete document then exits 1. Actions are engine-first: provision runs ajq provision, and a selected non-default model can add models_pull with ajq models pull <name>. --json requires --check; ajq provision --json is rejected and never starts provisioning.

These probes only read local configuration, filesystem, and PATH state. They do not construct a backend, contact a provider, download assets, or start a daemon. They intentionally omit credentials, prompts, provider responses, catalog URLs, and checksums.

Subcommands

CommandDescription
ajq cache status [--json]Print persistent judgement cache status, or its versioned machine-readable probe.
ajq cache clearDelete persistent judgement cache entries and report what was freed.
ajq capabilities [--json]Print informational static metadata, or the versioned machine-readable capability contract for agents.
ajq daemon statusPrint the warm local daemon status.
ajq examples [topic]Print categorized safe examples; semantic examples explicitly use --backend mock.
ajq daemon stopStop the local daemon if running; idempotent.
ajq models list [--json]Print the pinned local model catalog, or its versioned machine-readable probe.
ajq models pull <name>Download a checksum-pinned catalog model into the ajq cache.
ajq models use <name>Persist model = "<name>" to config after verifying the model is installed.
ajq provisionDownload or locate the local llama-server engine and default GGUF model.
ajq provision --check [--json]Report provisioning status (or its versioned JSON) and exit non-zero if assets are missing, without downloading.

The root help also includes Cobra’s generated completion and help commands.

Exit codes

CodeMeaning
0Success. With --exit-status, also means the last output value was truthy.
1With --exit-status: the last output value was false or null. Otherwise a general error.
4With --exit-status: the query produced no output.
non-zeroA parse, runtime, configuration, backend, provisioning, or I/O error occurred.

The --exit-status codes match jq’s convention.

Behavior notes

  • If no semantic operators are present, ajq constructs no backend, makes no network calls, and spawns no daemon.
  • The managed local backend starts llama-server on loopback with a per-daemon --api-key; ajq stores the bearer key beside the daemon PID file in the cache directory with 0600 permissions and sends it only to the managed daemon.
  • A healthy process already listening on the default managed local address is treated as a port conflict unless it has ajq’s managed PID/key files. To use a server you started yourself, pass --base-url (or set AJQ_BASE_URL/base_url); that explicit server is trusted by user intent and is not given ajq’s managed daemon key.
  • --cloud with a conflicting explicit --backend is a CLI error.
  • --backend openai and --backend openrouter send API keys on each request, so custom --base-url values must be https://; http:// is accepted only for loopback OpenAI-compatible proxies such as 127.0.0.1, localhost, or [::1].
  • --explain on a pure-jq query validates the query and prints a byte-stable pure-jq report without reading stdin.
  • --explain on a semantic query prints the static plan and, when valid stdin is supplied, mock-path estimates without contacting a real backend. With --explain --stream on a supported semantic plan, it instead identifies user-selected inline execution, does not consume stdin, and reports the harvest/batching/dedup estimate as unavailable.
  • --stats writes only the summary to stderr; query output remains on stdout. Its execution_mode is pure-jq, three-phase-windowed, planner-interleaved, or user-stream; batching_dedup states the matching windowed or inline trade-off. The numeric window_bytes, window_count, and oversized_window_count fields are zero outside three-phase-windowed mode.
  • --stream selects inline, input-order semantic execution only for supported semantic plans that would otherwise be windowed. It gives first-frame latency but disables cross-frame pre-resolve deduplication and window batching. Cache identity, cache hits, schema validation, and the run-global --max-calls cap are unchanged.
  • --window-bytes applies only to supported three-phase semantic execution. It forms complete-frame windows, never buffers the entire input, and accepts a record larger than the budget as a one-frame oversized window.

3.2 - Input and output modes

How ajq frames input and formats output.

Input framing

ajq selects an input framing based on flags and the shape of stdin.

ModeFlagBehavior
Auto (default)Auto-detects a single JSON value or a stream of top-level / NDJSON JSON values.
Null input-n, --null-inputIgnores stdin; supplies one null input frame.
Raw input-R, --raw-inputReads each stdin line as a string, excluding the line terminator. . is the line.

In the default mode, a stream of multiple top-level JSON values (whitespace- or newline-separated, i.e. NDJSON/JSON Lines) is framed independently, with no whole-stream buffering. Pure-jq and planner-required interleaved semantic queries execute and emit one frame at a time. Supported three-phase semantic queries instead group complete frames into configured byte-budgeted windows, resolve each window once, then emit their frames in original order. The window retains only its frames plus bounded framing lookahead; a record larger than the budget forms one oversized window and is never split. This still allows inputs larger than available memory.

For a supported semantic query, --stream selects that same per-frame inline behavior instead of default windows so a result need not wait for a later frame. It trades window-wide batching and cross-frame pre-resolve deduplication for low latency, without changing cache identity or the run-global --max-calls cap. See Process NDJSON for selection guidance and examples.

Output formatting

ModeFlagBehavior
Pretty JSON (default)Indented, human-readable JSON.
Compact JSON-c, --compact-outputSingle-line JSON, no extra whitespace.
Raw output-r, --raw-outputString results are written without surrounding quotes; non-string results are unaffected.

-c and -r can be combined, and both may be combined with any input mode.

Examples

# Single JSON value
printf '{"a":1}\n' | ajq -c '.a + 1'
# 2

# NDJSON, processed independently per frame
printf '{"a":1}\n{"a":2}\n' | ajq -c '.a, (.a + 10)'
# 1
# 11
# 2
# 12

# Null input — build output from scratch
printf '{"ignored":true}\n' | ajq -n -c '{generated: true}'
# {"generated":true}

# Raw input + raw output
printf 'error: disk full\n' | ajq -R -r 'ascii_upcase'
# ERROR: DISK FULL

3.3 - Semantic functions

The shipped semantic vocabulary, =/! sugar, arities, return types, and current limits.

Semantic operators are registered as jq functions, so gojq parses them inside ordinary jq constructs. ajq adds only the =~ and !~ surface sugar before parsing.

Function vocabulary

FunctionFormKindReturnsExecution status
sem_matchsem_match(value; "spec")predicatebooleanShipped
sem_classifysem_classify(value; "a"; "b"; …)bounded valueone label from the given labelsShipped
sem_extractsem_extract(value; "what")unbounded valuestringLimited: supported only in gated/interleaved fallback contexts
sem_scoresem_score(value; "spec")unbounded valuenumberLimited: supported in sort_by(...) three-phase placeholder mode and in gated/interleaved fallback contexts
sem_normsem_norm(value; "canonicalization spec")unbounded valuestringLimited: supported in group_by(...) three-phase placeholder mode and in gated/interleaved fallback contexts
sem_redactsem_redact(value; "redaction spec")unbounded valuestringLimited: supported only in gated/interleaved fallback contexts

The spec is a literal string in the query. Stream data is structurally fenced by the selected backend’s output constraints.

Kinds

KindMeaning
PredicateReturns true or false; typically used in select or if.
Bounded valueReturns one of a finite set declared at the call site.
Unbounded valueReturns a string or number with no finite output set. These shapes need bounded executor contexts or interleaved fallback; unsupported forms fail loudly.

Variadic implicit .

Every operator accepts both an explicit value and an implicit one. When the value argument is omitted, it defaults to .:

sem_match(.field; "spec")   # explicit value
sem_match("spec")           # value defaults to .

The implicit form is what makes raw-line mode ergonomic.

The =~ and !~ sugar

SurfaceDesugars to
X =~ "spec"sem_match(X; "spec")
X !~ "spec"`sem_match(X; “spec”)

The desugaring is performed by a jq-aware lexer, not a regular expression.

.users[] | select(.feedback =~ "angry/frustrated") | .id
# ≡
.users[] | select(sem_match(.feedback; "angry/frustrated")) | .id

Return-type discipline

Each semantic operator has a fixed output type: boolean, enum label, string, or number. Backends constrain model output with GBNF grammar or provider structured-output/schema features, so downstream jq receives a value of the expected shape when execution succeeds.

For sem_classify, the enum is exactly the label arguments at the call site:

sem_classify(.text; "billing"; "bug"; "feature")

That expression returns one of "billing", "bug", or "feature".

Scenario matrix

ScenarioInvocationStatus
Fuzzy filterselect(.msg =~ "auth failure")Shipped
Raw-line fuzzy filterselect(. =~ "stack trace") with -RShipped
Routing / labeling{route: sem_classify(.text; "billing"; "bug"; "feature")}Shipped
Semantic sort keysort_by(sem_score(.review; "positivity"))Limited, supported three-phase placeholder mode
Semantic grouping keygroup_by(sem_norm(.company; "canonical name"))Limited, supported three-phase placeholder mode
Gated semantic scoreselect(sem_score(.review; "positivity") > 0.8)Limited, uses interleaved fallback
Gated typed extractionselect(sem_extract(.raw; "years") != "")Limited, uses interleaved fallback
Gated semantic redactionselect(sem_redact(.notes; "PII") != .notes)Limited, uses interleaved fallback

sem_score and sem_norm are not general-purpose enrichment operators in 0.0.1. Use sem_score as a sort_by(...) key and sem_norm as a group_by(...) key when you want the three-phase executor. sem_extract and sem_redact have no supported three-phase context; use them only in a gated control-flow context. When an unbounded value result is used to prune control flow, such as a score comparison inside select or if, ajq may choose an interleaved fallback instead of the harvest/resolve/execute path. That fallback is bounded by the same backend, cache, and --max-calls controls, but its call count is not available as a three-phase harvest estimate.

Current grammar scope

There is no custom jq grammar. @classify(...)-style @ forms and infix transform operators such as ~> do not parse in gojq and are not shipped. Use the function-core forms above.

3.4 - --explain output

Every field in the ajq explain v1 report.

--explain prints a stable plan report and exits without executing the query. It does not contact a provider or local model.

Pure-jq report

ajq explain v1
query: ".users[] | select(.active) | .name"
execution: pure-jq deterministic
deterministic: yes
model_calls: 0
backend_calls: 0
byte_reproducible: yes
stdin: ignored
FieldMeaning
queryThe query as parsed.
executionpure-jq deterministic — no semantic call sites.
deterministicyes.
model_calls / backend_calls0.
byte_reproducibleyes.
stdinignored — pure-jq explain does not read input.

Semantic report

ajq explain v1
query: ".[] | select(sem_match(.msg; \"urgent\")) | .msg"
execution: semantic split plan
deterministic: no
model_calls: input-dependent
backend_calls: input-dependent
byte_reproducible: cache-dependent
stdin: harvested for estimates
planned_call_sites: 1
semantic_predicates: 1
semantic_values: 0
estimates:
  estimate_status: available
  static_call_sites: 1
  input_frames: 1
  harvested_judgements: 3
  post_dedup_judgements: 2
  mock_judge_batches: 1
  over_harvest_bound: post_dedup_judgements == mock distinct judgements; may be a safe superset of execute-needed judgements
subgraphs:
  deterministic: jq outside semantic call sites
  semantic: 1 planned call site(s)
semantic_plan:
  - call_id: 1
    op: "sem_match"
    kind: "predicate"
    value_expr: ".msg"
    specs: ["urgent"]
    source_range: 13:38
    gated: n/a
    execution: 3-phase
    subgraph: semantic

Header fields

FieldMeaning
executionsemantic split plan — the query has semantic call sites.
deterministicno.
model_calls / backend_callsinput-dependent.
byte_reproduciblecache-dependent — deterministic phases are byte-reproducible; semantic answers depend on backend/cache state.
stdinharvested for estimates when valid stdin is used for an estimate. With --stream on a supported plan, not harvested: inline execution has no cross-frame harvest estimate and explain leaves stdin untouched.
planned_call_sitesTotal semantic call sites found in the static plan.
semantic_predicates / semantic_valuesCounts of predicate-kind and value-kind ops.

estimates block

Present when valid stdin was supplied and the mock harvest path can estimate the query.

FieldUnit / meaning
estimate_statusavailable, or unavailable with a stable reason.
static_call_sitesSemantic call sites in the static plan; not input cardinality.
input_framesNumber of input frames read for the estimate.
harvested_judgementsPre-dedup semantic judgements collected by the mock harvest pass.
post_dedup_judgementsDistinct judgements after deduplication — the estimated backend-call count before considering persistent-cache hits in a later real run.
mock_judge_batchesBackend.Judge batch invocations in the deterministic mock path.
over_harvest_boundNotes that post-dedup judgements may be a safe superset of execute-needed judgements; it never underestimates needed decisions.

semantic_plan entries

FieldMeaning
call_idDeterministic planner call-site ID.
opThe sem_* operator.
kindpredicate or value.
value_exprThe jq expression producing the judged value.
specsOrdered literal spec arguments.
source_rangeByte offsets into the desugared query for the call site, or unavailable.
gatedFor value ops: whether the result flows into a pruning gate; n/a for predicates.
executionExecution mode: 3-phase, planner-required interleaved, or user-stream-inline when --stream selects inline execution for a supported plan.
subgraphsemantic.

When estimates are unavailable

If stdin is empty or invalid, or the query uses a semantic execution shape the current executor cannot safely estimate/execute, the static plan is still printed and estimate_status is marked unavailable with a reason.

--explain --stream is intentionally unavailable without reading stdin. Its report names semantic user-stream inline, says stdin: not harvested, and includes execution_selection: user-selected --stream interleaving, inline per-uncached-judgement batching, and disabled cross-frame pre-resolve deduplication. This is a mode choice, not a change to backend/model/cache identity or the run-global call cap.

The important distinction is execution mode. sem_score in sort_by(...) and sem_norm in group_by(...) can use the three-phase placeholder path. Gated unbounded value shapes, such as select(sem_score(.review; "positivity") > 0.8), can use interleaved fallback; their estimate status is unavailable because ajq resolves values as execution reaches them rather than harvesting a complete up-front set. Standalone sem_extract and sem_redact are registered but currently fail as unsupported in three-phase execution.

3.5 - Configuration

Config files, environment variables, precedence, and credential policy.

ajq resolves semantic-backend settings from flags, environment variables, a TOML config file, and backend defaults.

Precedence

RankSourceNotes
1Command-line flags--backend, --model, --base-url, --backend-concurrency, --max-calls, --window-bytes, and --no-cache.
2Environment variablesAJQ_BACKEND, AJQ_MODEL, AJQ_BASE_URL, AJQ_BACKEND_CONCURRENCY, AJQ_MAX_CALLS, AJQ_WINDOW_BYTES.
3TOML configDefault path or AJQ_CONFIG.
4Backend defaultsFor example, Anthropic’s default model and paid-backend call cap.

Flags override environment variables; environment variables override config values; config values override backend defaults.

Config file path

SettingBehavior
AJQ_CONFIG=/path/to/config.tomlRead this exact file. If it is set and the file is missing or invalid, ajq errors.
${XDG_CONFIG_HOME}/ajq/config.tomlDefault path when XDG_CONFIG_HOME is set and AJQ_CONFIG is not.
~/.config/ajq/config.tomlDefault fallback path when neither AJQ_CONFIG nor XDG_CONFIG_HOME is set.

A missing default config file is ignored. A missing explicit AJQ_CONFIG file is an error.

TOML keys

KeyTypeExampleMeaning
backendstring"local"Semantic backend: mock, local, ollama, openai, openrouter, or anthropic.
modelstring"qwen2.5-3b"Model id or shipped alias for the selected backend.
base_urlstring"http://127.0.0.1:11434"HTTP base URL for backends that accept one.
backend_concurrencypositive integer1Maximum in-flight requests in one provider batch. The selected OpenAI-compatible or Anthropic backend permits at most 2; Ollama permits at most 4.
max_callsinteger100Maximum post-dedup backend judgements; 0 means unlimited. Must be non-negative.
window_bytespositive integer262144Maximum source bytes in a supported three-phase semantic window. A larger value can deduplicate more nearby records but delays output until the window resolves. It has no execution effect for pure-jq or interleaved queries.
no_cachebooleantrueDisable persistent judgement cache reads/writes when true.

Example:

backend = "local"
model = "qwen2.5-3b"
backend_concurrency = 1
max_calls = 50
window_bytes = 262144
no_cache = false

ajq models use <name> writes model = "<name>" after verifying the model is installed. It preserves unrelated keys but not comments or original formatting.

Environment variables

VariableTypeMeaning
AJQ_BACKENDstringSame values as backend.
AJQ_MODELstringSame meaning as model.
AJQ_BASE_URLstringSame meaning as base_url.
AJQ_BACKEND_CONCURRENCYpositive integerSame meaning as backend_concurrency.
AJQ_MAX_CALLSnon-negative integerSame meaning as max_calls.
AJQ_WINDOW_BYTESpositive integerSame meaning as window_bytes; defaults to 262144 when unset.
AJQ_CONFIGpathExplicit config file path.
AJQ_CACHE_DIRpathCache root for provisioning assets, daemon state, local models, and judgement cache files.
OLLAMA_HOSTURL or host[:port]Used by the Ollama backend when no ajq base URL is set.
ANTHROPIC_API_KEYsecretAnthropic credential.
OPENAI_API_KEYsecretOpenAI credential.
OPENROUTER_API_KEYsecretOpenRouter credential.

Provider API keys are not part of the generic AJQ_* config merge. Each provider backend reads only its own credential environment variable. backend_concurrency and window_bytes follow normal flag > environment > TOML > backend-default precedence. An explicit concurrency value must be positive and within the selected provider’s maximum (2 for OpenAI-compatible and Anthropic; 4 for Ollama); invalid flag, environment, or TOML values are rejected when a semantic backend is resolved. Pure-jq execution does not resolve a semantic backend, so its streaming behavior is unchanged by environment or TOML backend settings.

API-key policy

API keys are environment-only. The TOML config rejects credential-looking keys including api_key, apikey, and token:

config key "api_key" looks like a credential; API keys are env-only (use ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY)

Cache paths

DataLocation under cache root
Persistent semantic judgements<cache>/judgements/
Local model files<cache>/models/
Engine bundles / legacy binary cache<cache>/engine/ and legacy <cache>/bin/
Daemon PID/activity filesCache-root daemon state files.

The cache root is AJQ_CACHE_DIR when set; otherwise it is the OS user cache directory joined with ajq (for example ~/Library/Caches/ajq on macOS or ~/.cache/ajq on Linux, with ~/.cache/ajq as a fallback).

3.6 - Backends

Semantic backend names, defaults, required settings, and paid-call caps.

Semantic operators require a backend. Pure jq queries do not construct a backend, spawn a daemon, or make network calls unless a semantic operator is present and a backend is selected by flags, environment, or config.

Backend summary

BackendSelect withModel default / requirementBase URLCredentialsDefault max_calls
mock--backend mockBuilt-in ajq-default-model identitynonenone0 (unlimited)
local--backend localqwen2.5-1.5b unless overriddenManaged loopback http://127.0.0.1:8081; explicit --base-url for user-trusted serversmanaged bearer key (internal)0 (unlimited)
ollama--backend ollamaRequired --model / config / envOLLAMA_HOST or http://127.0.0.1:11434none0 (unlimited)
openai--backend openaiRequired --model / config / envhttps://api.openai.com/v1OPENAI_API_KEY100
openrouter--backend openrouterRequired --model / config / envhttps://openrouter.ai/api/v1OPENROUTER_API_KEY100
anthropic--backend anthropic or --cloudclaude-haiku-4-5; aliases haiku, sonnet, opusprovider default; no user base URLANTHROPIC_API_KEY100

max_calls counts post-dedup backend judgements. 0 means unlimited. For a machine-readable static projection of these registered backends, use ajq capabilities --json; it does not resolve configured credentials or runtime asset state. In that projection Ollama has no default_base_url, because its runtime URL may come from OLLAMA_HOST before the documented loopback fallback.

Mock

The mock backend is deterministic and in-process. It is for examples, tests, fixtures, and --explain estimates. It never contacts a model provider.

Local

The local backend uses a managed llama-server daemon and a GGUF model from the ajq model catalog. ajq provision installs or locates the engine and default model. ajq models list|pull|use manages larger catalog models. Managed daemons are started with --api-key; ajq keeps the generated bearer key beside the PID file in the cache directory with 0600 permissions and sends it on /completion requests.

When ajq owns the managed daemon config, the base URL is an HTTP loopback URL with no path, query, fragment, or userinfo; bracketed IPv6 loopback is accepted, for example http://[::1]:8081. A health-only listener on the default managed address is a port conflict, not silent adoption. An explicit --base-url, AJQ_BASE_URL, or config base_url means you trust that external server; ajq bypasses managed provisioning/daemon startup and does not send its managed daemon key to that server.

Model identity for cache keys is local/<catalog-name> for catalog models, such as local/qwen2.5-3b. Path-like local model overrides use a stable hashed path identity.

Ollama

The Ollama backend uses Ollama’s native structured /api/chat endpoint. --model is required unless supplied by AJQ_MODEL or config. If no ajq base URL is configured, OLLAMA_HOST is honored before falling back to http://127.0.0.1:11434. Host-only forms such as 127.0.0.1:11434 are accepted and normalized.

OpenAI and OpenRouter

OpenAI and OpenRouter use OpenAI-compatible /v1/chat/completions transport with structured output. --model is required unless supplied by config or AJQ_MODEL.

BackendDefault rootAPI-key env var
openaihttps://api.openai.com/v1OPENAI_API_KEY
openrouterhttps://openrouter.ai/api/v1OPENROUTER_API_KEY

A custom --base-url is treated as an OpenAI-compatible API root.

Anthropic

--cloud selects the Anthropic backend and is equivalent to --backend anthropic. The default model is claude-haiku-4-5. Model aliases resolve as follows:

AliasModel id
haikuclaude-haiku-4-5
sonnetclaude-sonnet-5
opusclaude-opus-4-8

Anthropic credentials come only from ANTHROPIC_API_KEY.

The paid/remote backends (anthropic, openai, and openrouter) default to max_calls = 100 unless a flag, env var, or config value overrides it. Local, Ollama, and mock default to unlimited.

Bounded batch concurrency

--backend-concurrency, AJQ_BACKEND_CONCURRENCY, and TOML backend_concurrency set the maximum number of simultaneous requests while one provider resolves a batch of distinct cache misses. The default is 1, which keeps the established sequential path. Explicit values must be positive and are validated for the selected provider before backend construction or a request:

ProviderDefaultMaximumWhy
OpenAI-compatible (openai, openrouter)12A conservative opt-in ceiling for paid requests and their retries.
Anthropic12Bounds concurrent SDK-retried paid Messages API requests.
Ollama14A bounded local-service limit matching the managed local daemon’s default slot count.

A higher value can reduce latency for a multi-judgement batch, but it does not increase a batch’s judgement count, relax max_calls, change deduplication or cache keys, or alter per-request retry policy. For paid providers, start at 1 and increase only after checking the provider’s account rate limits; retries also consume request capacity. Managed-local and mock backend scheduling are unchanged by this setting.

Results always retain their input-batch order even when requests complete out of order. A per-item schema/coercion error stays attached to that item. In contrast, a transport or system failure cancels queued and admitted sibling work, waits for cleanup, and returns no partial batch results.

4 - Explanation

How ajq works and why it’s designed this way.

Design notes for ajq’s execution model, reproducibility guarantees, and architecture.

4.1 - Split execution

The core idea behind ajq — keep jq deterministic, call a model only for fuzzy operators.

The observation

A real-world “AI data” query is mostly not fuzzy. Consider triaging support tickets:

cat data.json | ajq --backend local '.users[] | select(.feedback =~ "angry/frustrated") | .id'
                      └────────┬───────┘  └────────────┬────────────┘  └┬┘
                      deterministic path       semantic predicate       proj
                      (pure gojq)              (LLM, per value)          (pure gojq)

Iterating the array, projecting .id, shaping the output — all of that is exact, mechanical work that jq already does perfectly. Only one operator, =~, needs judgement. The insight behind ajq is to treat that asymmetry as a first-class execution strategy: run the deterministic majority through a real jq engine, and call a model only for the fuzzy operators, on the smallest possible slices of data.

Why it matters

Splitting execution this way has three consequences that a “send each row to an LLM” script cannot match:

  1. Tiny context per decision. The model sees one field value at a time, not the whole record and not your prompt scaffolding. A small (~1.5B-parameter) local model is enough to decide “does this text read as angry?”. That’s what makes a local-by-default, no-API-key tool viable.

  2. Cost scales with fuzzy decisions, not input size. You pay for the number of distinct judgements, not the number of bytes or rows. A 10,000-row file with lots of repeated values costs far fewer than 10,000 decisions (see The three-phase executor).

  3. The deterministic majority is byte-reproducible. Because everything outside the fuzzy operators runs through pure jq, most of your pipeline produces identical bytes on every run. That reproducibility is the actual unique selling point — see The determinism contract.

Keeping jq as the real parser

ajq does not fork the jq grammar. Semantic operators are registered as ordinary jq functions through gojq’s WithFunction seam, so gojq parses and drives them natively:

gojq.WithFunction("sem_match", 2, 2, func(input any, args []any) any {  })

The only surface syntax ajq adds is a thin, jq-aware desugaring of the infix =~ / !~ operators into sem_match(...) calls. Everything else — composing sem_* inside select, sort_by, group_by, |=, arithmetic, reduce — comes for free, because it’s just jq.

This was a deliberate, empirically-tested decision. gojq’s existing grammar already expresses every real scenario, so a custom grammar would add no expressiveness while risking divergence from jq semantics. Richer surface forms like @classify(...) don’t parse in gojq and are deferred rather than forced.

What the model is (and isn’t) asked to do

The model is never asked to produce your output structure. It answers a single, narrow, typed question — a boolean, one of a fixed set of labels, a number, or a short string — and that answer is fenced by a grammar so it cannot be malformed. jq does all the structural work around it. This division is what lets ajq promise structural correctness regardless of how small the model is.

4.2 - The determinism contract

What ajq’s byte-reproducibility guarantees, and what it deliberately doesn’t.

ajq separates deterministic structure from semantic judgement.

The contract

The deterministic pipeline is byte-reproducible. Semantic judgements are structurally guaranteed, but their values are model- and cache-dependent.

Those are different promises.

Deterministic parts are byte-reproducible

Every part of a query that is not a semantic operator runs through pure gojq. Given the same input, it produces the same output bytes. There is no model, no sampling, and no network in that path.

This is why pure-jq queries and pure-jq --explain reports are reproducible, and why the jq skeleton around a semantic query remains stable.

Semantic parts are structurally guaranteed

A model’s answer to “is this urgent?” can differ across model versions or providers. ajq does not promise semantic value reproducibility across those changes. It promises the shape of the answer: sem_match returns a boolean, and sem_classify returns one of the labels declared at the call site.

That shape is enforced by GBNF grammar for the local llama-server backend and by structured-output/schema mechanisms for Ollama, OpenAI-compatible, and Anthropic backends. Downstream jq can rely on the type even when the judgement value changes.

Why caching does not change the contract

ajq caches successful semantic judgements by (op, spec, model-id, canonical-value). The cache has an in-memory front for a run and a persistent on-disk backing when caching is enabled. A repeated value under the same model identity can therefore resolve to a cache hit instead of another backend call.

Caching improves cost and latency, but it does not turn semantic answers into a universal truth. A different model identity is a different cache key, so model upgrades become clean misses rather than silent reuse. --no-cache disables persistent judgement cache reads and writes for a run.

The practical upshot

  • Diff two runs of a pure jq pipeline: the bytes match.
  • Repeated semantic values with the same op/spec/model identity can be served from cache.
  • Change the model or provider: output structure remains valid, but judgement values may change and cache keys miss.
  • Clear or bypass the cache: ajq may ask the backend again, while preserving the same structural contract.

4.3 - The three-phase executor

Harvest, resolve, execute — and why deduplication comes for free.

ajq runs supported semantic queries in three phases. For NDJSON and raw streams, the unit of work is a bounded window of complete frames rather than the complete stream.

The three phases

Phase 1  HARVEST   pure gojq run; semantic ops in COLLECT mode gather every
                   (op, spec, value) they observe. Permissive returns.
Phase 2  RESOLVE   dedup collected values, consult/write the judgement cache,
                   and resolve misses through the selected backend.
Phase 3  EXECUTE   pure gojq run; semantic ops in LOOKUP mode return resolved
                   values from cache. Deterministic given those cached values.

Phases 1 and 3 are pure gojq. Phase 2 is the isolated semantic boundary: it deduplicates, checks cache identity, and calls the backend only for misses.

Complete-frame windows

Supported three-phase streams are grouped by source bytes into windows before phase 1. The default is 262144 bytes (256 KiB), configurable by --window-bytes, AJQ_WINDOW_BYTES, or TOML window_bytes with flag > environment > file > default precedence. Each window harvests all of its frames, resolves its deduplicated cache misses once, then executes and emits frames in source order. Persistent cache entries can be reused by later windows.

The window boundary never splits a JSON value or raw line. The executor retains the current window, one complete-frame lookahead, and bounded framing buffers, rather than the whole input. A frame larger than the budget is processed as one valid oversized window; the configured budget therefore does not cap the memory needed for an individual record.

A larger window can eliminate more nearby duplicate judgements and reduce backend batches, but it waits to harvest and resolve all frames in that window before emitting its first result. Choose a smaller positive budget when first-record latency is more important than cross-frame deduplication.

User-selected inline execution

--stream gives supported semantic plans an explicit low-latency alternative to windows. It resolves an uncached judgement inline as each frame reaches the semantic operation, so a first result can emit before a later frame arrives. This preserves input order, the backend/model/spec/value cache identity, persistent cache reads and writes, schema validation, cancellation, output and exit behavior, and the run-global --max-calls cap. The cap still aborts before backend call N+1; frames successfully completed before that boundary may already have been written.

The price is no window-wide harvest, backend batch, or cross-frame pre-resolve deduplication. Inline runs may require more backend round trips and, with caching disabled, may resolve matching judgements in separate frames more than once. Cache hits still avoid backend calls. Default windows remain the better choice for throughput and batching; choose --stream when latency is more valuable. --stats distinguishes three-phase-windowed from user-stream, and --explain --stream explicitly reports that harvest/dedup estimates are unavailable without reading stdin.

Pure-jq ignores --stream and remains deterministic. Queries that already require interleaving retain their planner-selected inline behavior, so the flag does not override that selection. Neither inline path uses semantic windows.

Why two deterministic passes

Harvesting first, then executing, buys three things:

  • Deduplication. All values a query will judge are known before backend calls, so identical judgements collapse to one decision.
  • Backend-level scheduling. Resolve hands the backend the distinct judgement set. The local daemon and configured OpenAI-compatible, Anthropic, or Ollama backends can process bounded-parallel requests while preserving result ordering. Provider concurrency defaults to sequential (1); it applies only inside this already-reserved batch. A whole-batch transport failure cancels queued and in-flight siblings and yields no partial batch.
  • Reproducibility of structure. Execute is a pure function of input plus resolved semantic judgements, so jq output shaping remains stable.

The cost is running gojq twice, which is negligible compared with model latency.

Cache identity

A semantic judgement is keyed by (op, spec, model-id, canonical-value). The canonical value is a deterministic encoding that preserves scalar types, sorts object keys, keeps array order, and normalizes JSON numbers. The model id is part of the key, so switching models is a clean miss.

The cache has an in-memory front and, unless disabled, a persistent disk backing under <cache>/judgements/. That persistent backing is why a second process can reuse a successful judgement from an earlier run.

Over-harvest: correct, sometimes wasteful

In harvest, a predicate cannot yet know the model’s answer, so it returns permissively so that downstream operators see a superset of possible inputs. This is correct because it never misses a value that should be judged, but it may collect a value that execute later prunes. Deduplication and the persistent cache reduce the cost of repeated over-harvested values.

Gated value operators

A value-producing operator whose result feeds a pruning gate needs special care. Bounded enums such as sem_classify can harvest all possible labels, so downstream survivors are a safe superset. Unbounded values do not have a finite safe set, so ajq does not pretend every shape can be harvested safely.

In 0.0.1, unbounded support is deliberately narrow:

  • sem_score is supported by the three-phase executor as a sort_by(...) key, where ajq can harvest placeholder values for the sort key and then execute with resolved scores.
  • sem_norm is supported by the three-phase executor as a group_by(...) key, using the same placeholder pattern for grouping.
  • When an unbounded value result feeds a gate, such as select(sem_score(.review; "positivity") > 0.8), ajq can choose an interleaved fallback. That mode resolves values as the jq program reaches them instead of providing a harvest/dedup estimate up front.
  • Standalone sem_extract and sem_redact currently fail as unsupported in three-phase execution.

This means docs and scripts should distinguish “unsupported in three-phase harvest” from “always unsupported.” Gated unbounded value operators are not all rejected; supported fallback shapes still obey backend selection, cache identity, and --max-calls, but they do not have the same up-front estimate as the three-phase path.

An execute-phase cache miss is a loud error. That backstop turns planner or cache gaps into failures rather than silent data loss.

Guarding the planner

The executor relies on the static planner finding every semantic call site. ajq asserts fired ⊆ planned at runtime: every semantic operator that fires must appear in the static plan. A violation aborts before spending money and names the missed operation.

4.4 - Architecture

The components of ajq and how data flows between them.

ajq is a small pipeline of components.

Data flow

stdin → [Reader/Framer] → [Desugar] → [gojq.Parse] → [Planner] ──→ [Explain]
                                              [3-phase Executor]
                                               ├─ gojq (deterministic)
                                               └─ Semantic Executor → [Backend iface]
                                                                          ├─ mock
                                                                          ├─ local daemon
                                                                          ├─ ollama
                                                                          ├─ openai / openrouter
                                                                          └─ anthropic cloud
[Grammar/Schema builder] → GBNF / json_schema ─────────────────────────────┘
stdout ← [Assembler / schema-invariance guard]

The components

ComponentResponsibility
Reader / FramerStream framing: single JSON, NDJSON, and raw lines (awk-mode). Supported three-phase semantic input is grouped into complete-frame byte-budgeted windows; pure-jq and interleaved paths remain streaming.
DesugarRewrites =~ / !~ into sem_match calls with a jq-aware lexer.
PlannerExhaustive AST walk that discovers every semantic call site → a Plan used for --explain, validation, and cost estimation.
ExecutorRuns the plan in three phases (harvest / resolve / execute), with interleaved fallback for gated unbounded value ops.
Semantic ExecutorDeduplicates judgements, consults the persistent judgement cache when enabled, builds each op’s prompt and schema, and records run stats.
BackendPluggable inference. Shipped backends: mock, local (llama-server daemon), ollama, openai, openrouter, and anthropic/--cloud.
Grammar / schema builderTurns an op’s return type / enum into a JSON Schema and, for the local engine, GBNF grammar constraints.
AssemblerReassembles the output stream and enforces schema invariance.

Windowed semantic execution

For supported three-phase semantic queries, the executor retains one complete-frame window at a time, harvests its semantic judgements, resolves deduplicated cache misses in one backend batch, then executes frames in source order. The default source-byte budget is 256 KiB and can be overridden with --window-bytes, AJQ_WINDOW_BYTES, or window_bytes in TOML. A record that exceeds the budget is a valid one-frame oversized window, so the budget is not a rejection limit. The executor does not retain the complete stream: it keeps the current window, one lookahead frame, and bounded framing buffers.

The Backend seam

Inference sits behind a deliberately small interface, so backends are swappable without touching the planner or executor:

type Backend interface {
    Judge(ctx, batch []Judgement) ([]Result, error) // grammar/schema-constrained
    Warm(ctx) error
}

A judgement carries the op, kind, return type, schema/enum constraints, specs, model ID, and the canonical value. Results carry the value plus a per-item error. Resolve enforces stable ordering, len(results) == len(batch), return/schema validation, and only caches successful items. The deterministic mock backend powers tests and --explain estimates; local, Ollama, OpenAI-compatible, and Anthropic backends preserve the same structural contract through GBNF or provider JSON-schema/structured-output mechanisms.

Inference model

The local backend runs llama-server (llama.cpp) as a warm, lazily-spawned daemon — chosen because ajq stays a pure-Go binary (no CGo, clean cross-compilation) and avoids the 1.5–3 s cold model-load per invocation that would make a “filter” unusable. Outputs are constrained by a JSON Schema / GBNF grammar derived from each op’s schema, so structural correctness is a cross-backend invariant (see the determinism contract).

Remote and alternate-local backends share that same seam: Ollama uses its native /api/chat endpoint with format JSON schema, OpenAI/OpenRouter use OpenAI-compatible /v1/chat/completions, and Anthropic uses Messages API structured outputs. Backend selection, model identity, base URL, cost caps, and persistent-cache behavior are resolved by the CLI/env/config layer before execution starts.

Where to go next

4.5 - Project status

The capabilities that ship today, and how they fit together.

ajq has shipped its deterministic jq spine, semantic planning and supported semantic execution, local inference, bounded-parallel local daemon transport, Phase 3 backend/cloud controls, first-run local asset provisioning, local model management, the persistent judgement cache, byte-budgeted semantic windows, an explicit low-latency semantic stream mode, and checksummed release archives with a no-sudo install script. Further streaming optimizations remain planned. The iterative-harvest experiment is an internal-only no-go prototype, not a shipped mode; default windowed execution is unchanged.

What ajq does today

  • Deterministic stream framing — single JSON, NDJSON, and raw-line (-R) input; -n, -c, -r, and -e follow jq conventions.
  • Pure-jq execution through gojq — pure jq queries are deterministic and byte-oriented.
  • --explain — the byte-stable ajq explain v1 report for pure-jq and semantic queries, including harvest/dedup estimates when input is supplied.
  • Semantic query planning — an AST walk discovers sem_* call sites and is guarded by the runtime fired ⊆ planned invariant.
  • =~ / !~ desugaring — a jq-aware lexer rewrites fuzzy match syntax into sem_match(...) calls.
  • Supported semantic executionsem_match and bounded sem_classify run through the split executor. sem_score is supported as a sort_by(...) key, sem_norm is supported as a group_by(...) key, and gated unbounded value use can run through interleaved fallback. Standalone sem_extract and sem_redact are registered but currently fail as unsupported in three-phase execution.
  • Three-phase executor and stream selection — harvest / resolve / execute with deduplication and cache identity based on op, spec, model, and canonical value. Supported semantic NDJSON and raw streams default to complete-frame byte-budgeted windows (256 KiB by default), preserving source order without retaining the complete stream. --stream selects low-latency inline execution instead when first-frame latency outweighs window batching and cross-frame pre-resolve deduplication; identity and --max-calls semantics remain unchanged. Pure-jq and planner-required inline paths stay streaming.
  • Local inference — a lazy llama-server daemon with idle timeout and ajq daemon status|stop; local requests use bounded parallelism while preserving result ordering.
  • First-run provisioning and model managementajq provision installs or locates the local engine and default model; ajq models list|pull|use manages larger pinned catalog models.
  • Additional backends — Ollama, OpenAI, OpenRouter, and Anthropic (--cloud) are registered with structured-output constraints, provider-specific credential rules, and bounded ordered batch concurrency. Provider requests remain sequential by default; explicit OpenAI-compatible/Anthropic concurrency is capped at two and Ollama at four.
  • Config and cost controls — backend/model/base-url selection, TOML config, env vars, --backend-concurrency, --max-calls, and --stats are shipped. Paid backends default to a 100-call cap.
  • Persistent judgement cache — successful semantic judgements are stored under <cache>/judgements/, can be bypassed with --no-cache, inspected with ajq cache status, and removed with ajq cache clear.
  • Release packaging — GoReleaser builds checksummed archives and the install script verifies checksums.txt. The release workflow publishes the Homebrew cask to the public ricardocabral/tap tap. Windows MSI packaging is implemented and CI-validated, but the MSI is not yet released. WinGet remains unavailable until a future MSI release completes Microsoft validation and merge and has public clean-install smoke evidence.

Roadmap

Development proceeds by dependency. Phases 0–3 have shipped, along with selected Phase 4 and Phase 5 work pulled forward. Remaining roadmap items are scale and distribution polish.

PhaseFocusStatus
0 — Deterministic spineCLI, framing, pure-jq wrapper, --explain, golden harness.✅ Shipped
1 — Split-execution corePlanner, desugar, semantic predicates, bounded classification, guarded executor.✅ Shipped with explicit unbounded value-op limits
2 — Local inferencellama-server backend, daemon lifecycle, GBNF/schema constraints, provisioning.✅ Shipped
3 — Backends & cloudOllama, OpenAI/OpenRouter, Anthropic, config/env selection, cost controls.✅ Shipped
4 — Scale & chunkingByte-budgeted complete-frame windows and explicit --stream low-latency inline execution for supported semantic streams, persistent cache, and bounded local parallelism are shipped. The iterative-harvest experiment is an internal-only no-go: it has no user flag and does not change the default executor. Further streaming optimizations remain planned.🟡 Partial
5 — Polish & distributionModels subcommand, release archives/install script, and the Homebrew cask published to the public ricardocabral/tap tap are shipped. Windows MSI packaging is implemented and CI-validated but remains unreleased; WinGet is unavailable until a future MSI release completes Microsoft validation and merge and has public clean-install smoke evidence. Standalone build, GPU auto-detect, richer vocabulary, and additional package managers remain planned.🟡 Partial

Follow along

Development happens in the open at github.com/ricardocabral/ajq. Issues and PRs are welcome.

4.6 - Iterative-harvest prototype evaluation

Reproducible evidence and the no-go decision for the non-default iterative-harvest prototype.

Decision: no-go for productionization

The iterative-harvest implementation is an internal test and benchmark prototype. It is not a CLI mode, is not selected by default, and must not be enabled for users. The existing three-phase-windowed executor remains the default and unsupported query shapes continue to use their existing windowed or planner-required interleaved fallback.

This is a no-go even though the pruning workload clears its judgement-saving target. A reachable error case found during characterization has different behavior from the current windowed executor: after an earlier gate rejects a value, a backend result/schema/transport error for that pruned descendant is dispatched and returned by permissive windowed harvest, but is intentionally not dispatched by iterative harvest or interleaved execution. That changes the reachable error, cache, and exit contract. Reducing calls by dispatching the unreachable descendant would defeat the prototype’s purpose. Until a separate design resolves that semantic/error boundary, the required current-window parity gate is not met.

The deterministic no-prune control also exceeded both overhead limits on the recorded machine, so performance alone would not support productionization.

Reproduce the fake evidence

Run the fixed, network-free paired protocol from the repository root:

make bench-iterative-evidence
# equivalent: go test -count=1 -run TestIterativeFakeEvidence -v ./internal/bench

The command uses 21 timed repetitions after one discarded warm-up per executor and rotates the order of three-phase-windowed, iterative-harvest, and the user-stream interleaved reference. Every sample checks selected executor mode, output parity, and the controlled backend’s expected post-dedup call and batch oracle before accepting timing. It emits a schema-versioned JSON record containing every sample and its min/median/max latency summary.

The controlled fake corpus has fixed input, query, window, and answers:

WorkloadPurposeWindowed → iterative post-dedup calls
high-pruneone of eight values survives gate one16 → 9
low-pruneseven of eight values survive gate one16 → 15
no-pruneevery value survives the same two-gate chain16 → 16
repeated-cache-hitduplicate values with a deliberately pre-warmed shared store0 → 0
enum-gateliteral-label sem_classify gate followed by match6 → 4
multi-windowthree NDJSON frames with a one-byte window budget6 → 5

The saving metric is always windowed PostDedupBackendCalls - iterative PostDedupBackendCalls; harvested counts are not compared because staged and permissive harvest deliberately mean different things.

Protocol and acceptance thresholds

The recorded run used the repository at 0b79f89 on an Apple M3 Pro (darwin/arm64, Go go1.26.5). It ran the network-free command above, then the allocation benchmark below; both commands are included so the reported evidence can be recreated without a model or credentials:

make bench-iterative-evidence
go test ./internal/engine -run '^$' -bench BenchmarkIterativeHarvestPaired -benchmem -benchtime=200ms

The paired runner performs one discarded warm-up and 21 alternating samples for each of windowed, staged, and interleaved modes. It rejects a sample unless byte output, result/error outcome, selected mode, and the declared post-dedup call/batch oracle all match. Productionization required every parity/safety check plus all applicable limits:

GateLimitRecorded resultOutcome
high-prune judgement reduction>=25%43.75% (7 of 16 calls avoided)pass
no-prune median latency overhead<=15%72.94%fail
no-prune peak retained-memory overhead<=25%76.60%fail
strict current-window error/cache/exit parityexactpruned descendant error is dispatched only by windowed harvestfail

The final parity row is a semantic limitation, not a noisy benchmark measurement: with a false earlier gate, permissive windowed harvest still resolves a later unreachable judgement and can expose its backend error; staged and interleaved execution correctly avoid that call. Therefore no performance result can make this prototype a go.

Memory and allocation method

For each timed fake sample the runner calls runtime.GC, records the baseline memory statistics, runs the complete engine operation (compile, framing, cache, fake backend, and discarded output), and samples HeapAlloc every 100 microseconds until completion. The reported peak retained memory is the sampled HeapAlloc high-water above that post-GC baseline, not final heap size. The report also records Mallocs and TotalAlloc deltas per sample. This makes the memory unit and reset policy explicit; it is a process-local comparison rather than a claim about a model server’s memory.

Recorded fake result

On 2026-07-13, the fixed protocol reported:

  • high-prune: 7 avoided judgements, 43.75% reduction, passing the required >=25% judgement reduction threshold.
  • no-prune: 72.94% median latency overhead (limit <=15%) and 76.60% median peak-retained-memory overhead (limit <=25%), both failing. Exact timing is expected to vary by machine; the command above is the authoritative repeatable evidence.
  • The allocation-aware engine benchmark independently showed the same fake-overhead shape: at 200ms benchtime, no-prune windowed was about 146µs/op, 217KB/op, 2720 allocs/op; iterative was about 260µs/op, 377KB/op, 4829 allocs/op.

These are fake executor costs, not model latency. Optional local-model evidence is informational only and cannot replace the deterministic gate:

make bench-iterative-real

It requires AJQ_BENCH_REAL=1 plus the existing provisioned llama-server and model, then runs the same chained workload through complete windowed and iterative engine paths with alternating mode order and output-parity checks. It skips cleanly when the explicit opt-in or assets are unavailable. On the recorded machine it was run with the provisioned Qwen 2.5 1.5B Q5 model; model answers happened to pass all first gates, so both paths made 16 calls and its latency is deliberately not used for a threshold decision.

What a separate redesign would need

Do not turn this result into a production flag. A new issue/design would first need to settle the pruned-descendant backend-error/cache/exit contract and restore the required parity. Only after that should it propose a public mode selection policy. It would also need a truthful --stats/--explain surface that names the selected staged mode, stage batches, actual post-dedup calls, avoided judgements, cache hits, and why any query fell back. The supported corpus would remain intentionally narrow: a linear stable-value chain of sem_match gates and bounded literal enum sem_classify == label gates. Control flow, value operators, generators, bindings, nested queries, non-literal inputs, and all other ambiguous shapes must keep their existing executor fallback.