Tutorials
Start-to-finish ajq walkthroughs.
Run these in order. The second tutorial uses the setup from the first.
Available tutorials
- Your first ajq pipeline — install nothing, run a real jq query
through ajq, and see byte-for-byte deterministic output.
- 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 - 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:
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:
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:
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
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.
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:
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