Skip to content

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

GrainOne row is
events_pageA URL, with the on-page SEO digest from its latest bot fetches
events_eventA single bot request: bot, URL, status, cache state, timing
gsc_pageA URL's Search Console totals for the window
gsc_queryA date + URL + search query performance row
gsc_page_trendA URL's current-versus-prior-window deltas

Nodes

Every node has a "node" key; the child relation sits under "from":

json
{"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):

json
{"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 select over a page grain returns URL-ordered rows paged by the limit + cursor call arguments (order/limit nodes are rejected there).
  • A top-level aggregate returns group rows ranked by an order wrap (default: leading metric, descending) as a bounded top-N: no cursor - if the result is truncated, raise the limit, tighten having, or filter earlier.
  • A raw events_event select returns newest-first and honors order/limit.

Worked examples

Top search queries by clicks:

json
{"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):

json
{"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:

json
{"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.