ask_onfire
ask_onfire is the one query surface over OnFire’s data lake. You author a
structured QueryIR object — entity, select, filters, insight filters,
joins, aggregations — and the server compiles it to Snowflake SQL. There is
no raw-SQL tool: everything filterable goes through this grammar.
It replaces the old per-shape search tools (search_people,
search_companies, query_employee_footprint, get_events,
get_personas, get_industries).
When to use it
Section titled “When to use it”- Any deterministic filter over people or companies — title, seniority, persona, technology, country, size band, keywords in a profile.
- Signal entities: job postings, event attendance, GitHub stars and forks, past employment, monthly headcount, and the evidence trail behind any insight.
- Distributions and trends — aggregate mode with
group_by+buckets.
Don’t use it for AI-ranked “who should I approach at this account”
(that’s ai_prospecting) or for identity
resolution from a partial name / email
(match_person /
match_company).
Looking up insight values
Section titled “Looking up insight values”resolve_insights is optional. Call it
when you want the exact persona or technology value before writing an
insight_filter; otherwise ask_onfire resolves the values you pass when
the query is submitted, and returns suggestions for any it can’t match.
It matters because OnFire curates roughly 1,800 insights (personas plus
the technology space), and an agent that hasn’t looked one up tends to
guess a job_title string instead — which searches a different, usually
smaller population. Measured at one company: a devops title guess
matched 25 people; the devops persona matched 159.
Inputs
Section titled “Inputs”| Parameter | Type | Required | Description |
|---|---|---|---|
query | QueryIR | Yes | The structured query object — see the grammar below. |
QueryIR grammar
Section titled “QueryIR grammar”| Field | Type | Description |
|---|---|---|
entity | string | The entity the question is about — contact for people, company for organisations, plus the signal entities in the catalog. |
select | string[] | Attributes / dimensions / measures to return. Must be empty in aggregate mode. |
filters | DimensionFilter[] | Dimension filters — dimension, op, value. |
insight_filters | InsightFilter[] | Curated persona / technology filters — kind, value (a list means OR). |
joins | JoinSpec[] | Constrain the base entity by a related one. Filter-only — you cannot select joined columns. Always a list. |
distinct_by | string | Collapse to one row per distinct value of this field (e.g. one contact per company). |
group_by | string[] | Aggregate mode. Dimension names or buckets names. |
buckets | Bucket[] | Date-truncation keys — name, field, part (month / quarter / year). |
aggregations | Aggregation[] | Metrics per group — count, count_distinct, sum, avg, min, max. |
having | HavingFilter[] | Post-aggregation filters referencing an aggregation name. |
order_by | OrderBy[] | field + direction (asc / desc). |
limit | integer | Exact rows wanted. Unset returns the match count only (free). Hard ceiling 1,000. |
confirmed | boolean | Required true for pulls above the confirmation threshold (50 rows). |
allow_extended_pool | boolean | Opt in to naming a gated extended-pool entity (people_pool, experiences_pool) directly. Rarely needed. |
Unknown keys are rejected (extra="forbid") — a SQL-ism or a guessed field
name comes back as a validation error with the valid names and a
did-you-mean list, having never executed and never billed.
Filter operators: eq, in, gte, lte, contains. The SQL symbols
=, >=, <= are accepted as aliases. contains takes a list to match a
whole keyword family in one filter.
Filtering from a dataset: instead of re-typing hundreds of URLs, a
dimension filter can read its values from a previous result —
from_dataset (a ds_... id) plus column.
Insight filters vs. title filters
Section titled “Insight filters vs. title filters”This is the one rule that changes what you get back:
insight_filterssearch OnFire’s curated signal layer — deduped, active-employment-aware, high precision. This is the default for any role, department, function or seniority the persona catalog covers, including when the query names a single company.- A
job_title contains [...]filter searches the full population as free text. Correct in three cases only: the user named an exact title, the user asked to search titles, or the concept has no curated persona.
Each insight carries a coverage band (common / moderate / thin /
none) telling you what to expect before you bet a query on it.
Returns
Section titled “Returns”JSON with:
dataset— handle to the full result (CSV in S3, 7-day TTL).preview_rows— a small inline sample.total_count— the number of matching rows.coverage— present when the query ran against a different population than the entity you named, saying which and why.
When limit is unset the response is a count only — no rows pulled,
nothing billed — so the volume can be settled with the user first.
Examples
Section titled “Examples”Security leadership at one company, by title (the user named exact titles):
{ "entity": "contact", "select": ["full_name", "job_title", "linkedin_url"], "filters": [ { "dimension": "current_company_url", "op": "eq", "value": "linkedin.com/company/acme" }, { "dimension": "job_title", "op": "contains", "value": ["security", "cyber", "infosec", "ciso"] } ], "limit": 25}Five AppSec people at five different large North American companies:
{ "entity": "contact", "select": ["full_name", "job_title", "current_company_name"], "insight_filters": [{ "kind": "persona", "value": "appsec" }], "joins": [ { "entity": "company", "filters": [ { "dimension": "size_band", "op": "eq", "value": "10001+" }, { "dimension": "location_country", "op": "in", "value": ["united states", "canada"] } ] } ], "distinct_by": "current_company_url", "limit": 5}Engineering hires per month at one company (aggregate mode):
{ "entity": "people_experiences", "filters": [ { "dimension": "company_url", "op": "eq", "value": "linkedin.com/company/acme" }, { "dimension": "title_name", "op": "contains", "value": "engineer" } ], "buckets": [{ "name": "hire_month", "field": "start_date", "part": "month" }], "group_by": ["hire_month", "title_role"], "aggregations": [{ "name": "hires", "fn": "count_distinct", "field": "person_url" }], "order_by": [{ "field": "hire_month", "direction": "desc" }], "limit": 200}Date windows are applied after the query on the experience entities —
start_date / end_date are text attributes there, not filterable
dimensions. Slice the window with
query_datasets.
Notes & limits
Section titled “Notes & limits”- Confirmation threshold: 50 rows. Above it the call returns the match
count and asks for
confirmed=true. - Hard ceiling: 1,000 rows per call, enforced even with
confirmed. Narrow the query (e.g.distinct_by) or split it. - Rows returned consume tenant credits; counts are free.
- Field names are per entity — call
describe_onfire_schemawith the entity ids you plan to query rather than guessing. - Filter only on dimensions flagged
filterable, or viainsight_filters. Attributes (e.g.full_name) are display-only. - A 0-row result is an answer about OnFire’s coverage, not a reason to fall back to another source.
- Full playbook — entity choice, aggregate mode, provenance and hiring
recipes, dataset chaining:
load_onfire_guide("ask_onfire").