AI Instructions — QA ZERO API / QAL
You are reading this because you are an AI assistant (LLM, MCP client, etc.)
composing queries for the QA ZERO API. Read this file in full before
building a query. The two YAML files shipped alongside it
(materials.yaml, qal-validation.yaml) are the authoritative spec —
always defer to them when this README and the YAML disagree.
This file is intentionally short. It is not a tutorial. It is a set of rules and shapes you must follow.
1. What this API is
- A read-only analytics API. You submit a QAL query (structured JSON) and receive a tabular result.
- QAL is not SQL. It is a deliberately small, declarative language designed so that AI clients rarely need to retry and can never construct a query that damages the backing store.
- The target deployment is commodity shared hosting — the API is designed to return results quickly on cheap infrastructure, not on a cloud DWH.
2. The two endpoints you will use
GET /wp-json/qa-platform/guide— returns this file, the two YAML specs, available tracking_ids, and feature flags. Call this first so you know what is currently supported.POST /wp-json/qa-platform/query— accepts a QAL query in the JSON body and returns rows.
All other endpoints are out of scope for AI use.
3. Version vs. update
version(YYYY-MM-DD) — changes only on breaking changes. Pin it in the URL:?version=2025-10-20.update(YYYY-MM-DD) — bumps on non-breaking additions within a version. Returned asapi_updatein the/guideresponse.- Each feature or field may carry a
since: YYYY-MM-DDtag. Ifsince > client.known_update, assume the feature may not exist on the server you are talking to; fall back, do not error.
4. QAL shape (minimum viable query)
A valid QAL query always contains these top-level keys:
tracking_id— a site identifier returned by/guide.materials— list of materials you intend to read from.time—{ start, end, tz }.start/endare ISO-8601;tzis an IANA timezone (e.g.Asia/Tokyo).make— map of named views. Each view hasfrom(a material) andkeep(columns to select). Optional:filter,join,add/calc,sort.result— which view to return and how. Supportsuse,limit, andcount_only.
Any other top-level key is a mistake. The validator will reject it.
4.1 Minimal working example
Copy this shape first, then adapt. The entire QAL query must be wrapped
in a top-level qal key when POSTed to /query:
{
"qal": {
"tracking_id": "<id from /guide>",
"materials": [{"name": "allpv"}],
"time": {
"start": "2026-04-01T00:00:00",
"end": "2026-04-14T00:00:00",
"tz": "Asia/Tokyo"
},
"make": {
"top": {
"from": ["allpv"],
"keep": ["allpv.url"],
"calc": {"views": "COUNT(allpv.pv_id)"},
"sort": {"by": "views", "order": "desc", "top": 5}
}
},
"result": {"use": "top"}
}
}
This returns the top 5 URLs by page view count for the given time range.
Every other query shape is a variation on this skeleton — change the
material, the columns in keep, or the aggregate in calc.
The response envelope looks like:
{
"data": [ /* rows */ ],
"meta": { "total_count": 0, "returned_count": 0, "limit": 1000 }
}
Consult qal-validation.yaml for the authoritative shape of each clause
— this example is a friendly on-ramp, not the spec.
4.2 Common pitfalls
These five mistakes account for almost all first-try failures. If your query is rejected, check these before anything else:
- The POST body must wrap the query in
{"qal": ...}. The/queryendpoint reads the query from the top-levelqalfield. A bare QAL body at the root fails validation. frommust be an array, not a string. Write"from": ["allpv"], not"from": "allpv". Even a single source is wrapped in[...].keepentries must be qualified as<material>.<column>. Write"keep": ["allpv.url"], not"keep": ["url"]. Bare column names are rejected withE_UNKNOWN_COLUMN.calcvalues are string expressions of the formFUNC(material.column). Write"calc": {"views": "COUNT(allpv.pv_id)"}, not"calc": {"views": "COUNT(*)"}and not"calc": {"views": {"count": "*"}}.*is not a valid column reference here; you must name a real column. The allowed functions are listed inqal-validation.yaml(currentlyCOUNT,COUNTUNIQUE,SUM,AVERAGE,MIN,MAX).sortis an object, not an array. Write"sort": {"by": "views", "order": "desc", "top": 5}, not"sort": [{"column": "views", "direction": "desc"}]. Keys areby(required string),order("asc"or"desc", required), andtop(optional positive integer — use this to cap rows instead ofresult.limitfor top-N queries).
5. Rules you must follow
- Always call
/guidebefore your first/query. Do not guess the tracking_id, materials, or feature support. - Never invent columns. Only use columns that appear under the
target material in
materials.yaml. If the user asks for a column that does not exist, say so rather than fabricating one. - Always set
time. There is no default time range. Queries withouttimeare rejected. - Always set
result.use. It must reference a view you defined inmake. Undefined view references returnE_UNKNOWN_VIEW. - Always set
result.limitunlesscount_only: true. This is how you keep execution cost predictable. - Never set two views with the same name. View names must be unique
within
make. - Never request features whose
enabledisfalse. Check thefeatures_detailmap in/guide. Asking for a disabled feature is a client bug, not a server bug. - If something fails, re-read
/guide. The rules may have shifted between your cached knowledge and the live deployment.
6. Picking a material
Use materials.yaml as the source of truth. For quick orientation:
allpv— one row per page view. Start here when the user asks about traffic, sessions, referrers, devices, or page popularity.click_event— one row per recorded click. Use for click-through rates, rage clicks, or element-level interest.gsc— Google Search Console data. Use for search queries, impressions, and CTR from organic search.goal_N— conversion/goal events configured per site. Use when the user asks about conversion rates or funnels.page_version— page version metadata. Use when slicing by content changes or A/B.datalayer_event— custom dataLayer events. Use only when the user explicitly mentions a dataLayer event name.
The full list and the columns each material exposes live in
materials.yaml. Consult it before emitting a query.
7. JOIN rules
- Only the keys listed in
materials.yamlunder a material'sjoinsection can be used as JOIN keys. - A single view may JOIN at most one additional material on top of its
fromsource. Do not chain multiple joins in one view — build a separate view inmakeand chain views viaview_chaininginstead. pv_idis the canonical JOIN key betweenallpvandclick_event.session_idis the canonical JOIN key when aggregating per-session metrics across materials.page_idis the canonical JOIN key when correlating withpage_version.
Any other ID-looking field exists for future use or for internal
bookkeeping. If you are tempted to JOIN on a column not listed in
materials.yaml join:, stop.
8. Filter, calc, sort — the safe surface
filteraccepts a flat object of{column: value}or{column: {op: value}}. No free-form SQL. No raw expressions.calcsupports a whitelisted set of functions. Checkqal-validation.yamlfor the current list. Do not invent function names.sorttakes a list of{column, direction}entries. Direction isascordesc, nothing else.result.limitcaps row count.result.count_only: truereturns only a scalar count and ignoreslimit.
9. What not to do
- Do not try to write SQL. There is no SQL layer exposed.
- Do not fetch data through any URL not listed in §2.
- Do not hardcode a column list from memory. The authoritative column
list is
materials.yaml, and it can change between updates. - Do not try to bypass
time. "All-time" queries are intentionally unavailable because they are usually a mistake. - Do not assume a feature exists because it was enabled in a previous
version. Re-check
features_detaileach session. - Do not translate user intent directly to a query without first verifying the target material exists.
10. If you are unsure
Prefer to ask the user over guessing. One well-formed question to the user is cheaper than three rejected queries. This API is designed so that correct-by-construction queries are easy to write — if you find yourself fighting the shape, you have probably picked the wrong material.