Query Guide
The query tool reads EdgeComet data through a JSON relation tree called an ir. This page explains the shape. The authoritative, site-specific version - with your exact field names, custom extraction fields, and segments - comes from get_schema: its grammar key is complete even when a client truncates tool descriptions. Have the assistant call it before writing queries.
The envelope
Every call takes a websiteId, a time window (days, or from + to), and optionally a segmentId to scope the read to one saved page-type segment. The same envelope applies to export, which renders the identical IR to a CSV.
The five grains
| Grain | One row is |
|---|---|
events_page | A URL, with the on-page SEO digest from its latest bot fetches |
events_event | A single bot request: bot, URL, status, cache state, timing |
gsc_page | A URL's Search Console totals for the window |
gsc_query | A date + URL + search query performance row |
gsc_page_trend | A URL's current-versus-prior-window deltas |
Nodes
Every node has a "node" key; the child relation sits under "from":
{"node": "source", "grain": "events_page"}
{"node": "filter", "from": R, "where": P}
{"node": "select", "from": R, "fields": ["url", "title"]}
{"node": "aggregate", "from": R, "group_by": ["url"],
"metrics": [{"fn": "sum", "field": "clicks"}],
"having": {"metric": "sum_clicks", "op": "gt", "value": 9}}
{"node": "order", "from": R, "by": "sum_clicks", "dir": "desc"}
{"node": "limit", "from": R, "n": 50}order and limit are their own nodes wrapped around the tree - never keys on aggregate or select - and by names exactly one field.
Metrics take fn + field only (count, count_distinct, sum, avg, min, max). There is no as: the alias is derived - count stays count, count_distinct on f becomes cd_f, everything else becomes fn_field (for example sum_clicks) - and that derived name is what having and order's by reference.
Predicates
Comparisons use field / op / value (value2 for between; valueless ops like empty, missing, present omit it):
{"field": "bot_type", "op": "in", "value": ["ai_bot"]}Ops are tokens (eq, neq, gt, gte, lt, lte, contains, in, between, ...), never symbols. Combine with {"and": [...]}, {"or": [...]}, {"not": ...}. A membership predicate can reference another relation: {"field": "url_hash", "op": "in", "rel": <select of one field>} - this is how grains join.
Paging
- A top-level
selectover a page grain returns URL-ordered rows paged by thelimit+cursorcall arguments (order/limitnodes are rejected there). - A top-level
aggregatereturns group rows ranked by anorderwrap (default: leading metric, descending) as a bounded top-N: no cursor - if the result is truncated, raise the limit, tightenhaving, or filter earlier. - A raw
events_eventselect returns newest-first and honorsorder/limit.
Worked examples
Top search queries by clicks:
{"node": "limit", "n": 20, "from":
{"node": "order", "by": "sum_clicks", "dir": "desc", "from":
{"node": "aggregate",
"from": {"node": "source", "grain": "gsc_query"},
"group_by": ["query"],
"metrics": [{"fn": "sum", "field": "clicks"}]}}}Keywords with two or more competing URLs (the cannibalization shape):
{"node": "aggregate",
"from": {"node": "source", "grain": "gsc_query"},
"group_by": ["query"],
"metrics": [{"fn": "count_distinct", "field": "url"}],
"having": {"metric": "cd_url", "op": "gte", "value": 2}}Recent AI-bot fetches:
{"node": "select", "fields": ["url", "bot_name", "event_date_time"], "from":
{"node": "filter",
"from": {"node": "source", "grain": "events_event"},
"where": {"field": "bot_type", "op": "in", "value": ["ai_bot"]}}}Field names are exact per grain and per site - when a query fails on an unknown field, the error returns the legal fields, and get_schema lists them all up front.