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
- Install ajq — get ajq onto your machine and provision local assets.
- Make ajq available to a coding agent — add a
project decision rule so agents choose ajq for semantic JSON work before writing a script.
- Process an NDJSON stream — handle newline-delimited JSON and raw
log lines.
- Filter JSON by meaning — find records by topic or intent
with explicit semantic predicates.
- Classify JSON and NDJSON streams — route records into a
fixed set of semantic labels.
- Use ajq safely from coding agents — start with mock
and
--explain, then cap and observe real model calls. - Install the ajq coding-agent skill — add the native Codex
marketplace or the optional Claude Code and Cursor adapter.
- Use ajq with other data formats — pipe JSON from
complementary jq ecosystem adapters into ajq.
- Write a semantic filter — use fuzzy
=~ predicates and bounded
semantic functions. - Estimate model calls before running — use
--explain to
budget cost and latency. - Use cloud backends — run semantic queries with Anthropic,
OpenAI, or OpenRouter.
- Control semantic costs — combine
--explain, --max-calls, and
--stats. - Manage the judgement cache — inspect, bypass, and clear cached
semantic judgements.
- Configure defaults — set backend, model, base URL, cost cap,
and cache defaults.
- Use a larger local model — pull and select a larger pinned
GGUF model.
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.gzajq_<version>_Linux_x86_64.tar.gzajq_<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 --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 - 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:
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:
- Select objects whose
.status is exactly "open". - Select support records whose text means a refund request.
- 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.
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}
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.
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.
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"))'
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:
.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:
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.
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.
- Process an NDJSON stream — input framing for JSON, NDJSON, raw lines, and null input.
- Semantic functions reference — bounded classification semantics and limitations.
- Control semantic costs — combine estimates, caps, stats, and cache behavior.
- Backends reference — choose local, mock, Ollama, OpenAI, OpenRouter, or Anthropic.
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.
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:
| Field | Meaning |
|---|
harvested_judgements | Total values collected before deduplication. |
post_dedup_judgements | Distinct semantic judgements after deduplication — the estimated backend-call count. |
mock_judge_batches | How many backend batch calls the mock estimate path used. |
static_call_sites | Number 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.
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.
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
Export an Anthropic API key:
export ANTHROPIC_API_KEY="sk-ant-..."
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.
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
Export an OpenAI API key:
export OPENAI_API_KEY="sk-..."
Choose an OpenAI model explicitly:
printf '{"msg":"urgent billing issue"}' \
| ajq --backend openai --model gpt-4o-mini '.msg =~ "urgent"'
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
Export an OpenRouter API key:
export OPENROUTER_API_KEY="sk-or-..."
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.
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:
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.
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:
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.
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:
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:
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.
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:
- Command-line flags
AJQ_* environment variablesconfig.toml- 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
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:
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:
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"'