Skip to content
Talk to our solutions team

Querying

There are three ways to ask the Data API for data. They are surfaces over one query AST and one compiler, which is the fact that makes the rest of this section short: whichever you use, the same tree reaches the same planner, and the same access rules, row-level security and audit apply.

SurfaceYou writeBest for
Pipe syntaxorders | status = "paid" | -created_at | 10A person writing a query
JSON syntax{"from":"orders","where":{"status":"paid"}}Code, or a model, assembling one
GraphQLquery { orders(status: "paid") { id total } }A client that already speaks GraphQL

The first two are isomorphic — every form in one has an equivalent in the other, and they decode into the identical tree. GraphQL is a genuinely different surface with a narrower filter vocabulary, and that difference is the main thing to know before choosing it.

Who writes the query decides it, more than what the query does.

Reach for the pipe syntax when a person is typing. It is the shortest of the three, it reads left to right in the order the work happens, and it is what appears in scripts and examples throughout these docs. From a script, data.q() takes it directly.

Reach for the JSON syntax when code assembles the query — a filter panel, a saved view, a generated report. Building a pipe string by concatenation invites the injection problem the DSL exists to avoid; building an object is ordinary data manipulation. It is also the better target for a model, because a JSON schema constrains generation far more tightly than a grammar does.

Reach for GraphQL when the client already speaks it, or when the shape of the response — nested relations selected per field — is the point. Know that its filters are narrower than the other two before you commit to it.

Because all three compile through one engine, these hold everywhere:

  • The projection is explicit. Omitting a field list projects the entity’s declared fields in canonical order. No form compiles to SELECT *, so a schema change shows up in a diff rather than in a response body.
  • Access rules and RLS apply to the query, not to the response. Rows you may not see are never fetched; they are not fetched and filtered.
  • An include is a separate query per relation, not a join, which keeps the parent row count honest.
  • Row caps are enforced by the compiler, so an unbounded query is bounded whether or not you said so.

Worth being blunt about, because it is the most common wrong assumption:

EndpointAccepts
REST listQuery-string parameters, mapped to the AST. See the HTTP reference
GraphQLGraphQL
Scripted endpointsWhatever your script accepts — and inside it, data.q() takes the pipe syntax

No HTTP endpoint takes a raw pipe string or a raw JSON DSL body. The pipe and JSON forms are for in-process callers and for scripts. If you want to expose a rich query surface to a client, the shape to build is a scripted endpoint that validates its input and calls data.q() — not a passthrough that hands a caller’s string to the parser.

PageCovers
Pipe syntaxThe full grammar: pipes, expressions, values, projection, traversal, CTEs, options
JSON syntaxEvery key and $-operator, mutations, transactions, and the gaps against the pipe form
GraphQLThe generated schema, what it exposes, and what it does not
Request flagsPer-request behaviour that applies across surfaces
Schema exportGetting the schema out, for clients and codegen