Querying Data with HOQS
HOQS (Henceforth Object Query Syntax) is the query language behind POST /data/query. It lets you filter, sort, aggregate, and traverse records without writing raw SQL. This page explains the mental model behind HOQS – why it works the way it does – rather than walking through every clause. For the exact steps to write a query, see the How-to Guides; for the full grammar, see the HOQS Query Language Reference.
Validate, Then Compile
Section titled “Validate, Then Compile”Every HOQS query is a string. Before that string touches the database, the runtime parses it and validates it against the same compiled metadata that governs the data API – the same object and field definitions from Objects, Records, and Fields. Only after validation succeeds does the query compile to parameterized SQL against the object’s generated view (see From Metadata to Database).
This ordering is the core design decision. Because HOQS knows your schema, it can catch mistakes that a raw SQL client cannot:
- Unknown fields are caught before execution, with a suggestion:
Field 'statux' does not exist on object 'core.Contact'. (at 1:42) Did you mean 'status'? field = NULLis rejected outright, rather than silently compiling to a query that returns zero rows – which is what= NULLdoes in standard SQL, sinceNULL = NULLevaluates toNULL, nottrue. The validator requiresIS NULLinstead.- Type mismatches (LIKE on an integer field, SUM on a text field) are caught at validation time, not as a database error after the fact.
Because queries are strings that get parsed rather than SQL fragments that get concatenated, every user-provided value becomes a bind parameter. There is no string interpolation path – injection is structurally not possible, not merely disciplined against.
Querying Views, Not Tables
Section titled “Querying Views, Not Tables”HOQS queries always run against an object’s generated view, never its underlying table directly. This matters because the view is the join point between hf.object (the universal root, holding id, objectType, createdAt, updatedAt) and the object’s own table – and, for inherited objects, the full table chain from base to leaf. A HOQS query against core.Individual returns Party fields and Individual fields in one flat result, without the query author needing to know that the underlying storage spans two tables. Cross-object formula fields, which are correlated subqueries in the view rather than stored columns, are queryable the same way as any other field for exactly this reason.
Traversal Is Deliberately One Hop
Section titled “Traversal Is Deliberately One Hop”Dot notation (account.industry) lets a query on Contact filter or select a field from the related Account. The first segment must name a lookup field on the object being queried; the second segment names a field on that lookup’s target. This compiles to a LEFT JOIN against the target’s own view.
Multi-hop traversal (account.owner.name) is not supported. This is a deliberate scope limit, not an oversight: each additional hop is another LEFT JOIN against another view, and unbounded traversal depth turns an ordinary query into an unpredictable join plan. The limit keeps query cost legible from the query text alone – if you need a second hop, you fetch it as a separate query or reach for INCLUDE.
INCLUDE Fetches Children, Not Joins
Section titled “INCLUDE Fetches Children, Not Joins”INCLUDE is HOQS’s answer to fetching child records alongside a parent – Accounts with their Contacts, for example. It is conceptually the inverse of dot-notation traversal: dot notation reaches up from a record to the one parent it references; INCLUDE reaches down from a record to the many children that reference it back, via the relationship name declared by the lookup’s inverse (toInverseKey).
INCLUDE is not implemented as a SQL JOIN. A JOIN between a parent and its children multiplies parent rows once per matching child, which is the wrong shape for an API response where each Account should appear once with a contacts array. Instead, each INCLUDE issues its own query per parent row and stitches the results into a nested array. INCLUDEs can nest – Accounts with Contacts with Opportunities – up to three levels deep; deeper nesting is rejected as a safety limit, the same category of guardrail as the single-hop limit on dot notation.
Aggregation Mirrors SQL, with One API-Shape Rule
Section titled “Aggregation Mirrors SQL, with One API-Shape Rule”COUNT, SUM, AVG, MIN, MAX, and GROUP BY / HAVING behave the way they do in SQL: GROUP BY buckets rows, HAVING filters buckets, WHERE filters rows before bucketing. The one HOQS-specific rule is that every aggregate expression in SELECT requires an alias (COUNT(*) AS total). This exists because the API response is a JSON object per row – an aliased aggregate becomes a named field in that object, the same way a regular field does. An unaliased aggregate would have no field name to serialize under.
Time functions (YEAR, QUARTER, MONTH, and so on) extract a date/time component for grouping – GROUP BY YEAR(closedDate) buckets by year without you writing DATE_TRUNC yourself. They exist for the same reason aggregation exists: so that reporting-shaped questions (“revenue by quarter”) don’t require dropping out of HOQS into raw SQL.
What’s Next
Section titled “What’s Next”For the exact syntax to write a filtered query, see Query and filter records. For aggregation recipes, see Aggregate query results. For fetching child records, see Include child records in queries.
Reference
Section titled “Reference”- Reference: HOQS Query Language Reference – complete grammar, operators, predicates, and validation error catalog
- How-to: Create records through the API – the API routes, including the query endpoint