PassQL

PassQL is the textual query language of Synex Lab. You write it in the Lab workspace or submit its AST to the Lab API; it compiles to the same AST the visual Blocks builder produces and runs against the Lab read model (Postgres) — never against the passport store directly. That's why queries are metered as Postgres-only · 0 RCU.

A query is a pipeline: one from clause followed by stages, each introduced by a pipe (|).

A full pipeline

from ontology:"Battery X" depth:C status:published
| where p"Weight" > 10 and (p"Material" in ["steel", "aluminium"] or p"Recycled" = true)
| where p"Dimensions".width <= 40
| group by p"Material"
| agg avg(p"Weight") as avg_weight, count() as n
| sort avg_weight desc
| limit 100

Syntax rules

  • Keywords and property names are case-insensitive (WHEREwhere).
  • Strings use double quotes with backslash escapes: "Fo\"o", "a\\b", \n, \t.
  • # starts a comment that runs to the end of the line.
  • Whitespace is insignificant — the multi-line layout is convention, not requirement.
  • Errors carry the exact byte offset of the offending token; the editor underlines the span and lists what was expected.

from — the source

Selects which passports the pipeline reads. ontology: is required and repeatable (or comma-separated).

from ontology:"Battery X" ontology:"Chargers" depth:C,B status:published,draft
KeyMeaning
ontology:"…"Required, repeatable. The ontology name or its id (ontology:"ontology_0218…"). Names resolve against your team's catalog; an ambiguous name asks for the id.
depth:COptional. Restrict to ontology depth letters (depth:C,B for several).
status:publishedOptional. Filter on the explicit publish status (published, draft, archived). Passports whose status is only inherited are not matched — see resolved status.

Only ontologies belonging to your team resolve; anything else fails at compile time.

Property references

Properties are the typed fields defined on your ontologies.

FormMeaning
p"Weight"By name. If several selected ontologies define that name with the same type, the reference matches all of them — cross-ontology queries just work.
p:id"property_9f2k…"Pinned to one property id. Required when the same name has conflicting types across ontologies; the error lists the candidate ids to pin.

Each property type supports a specific set of operators:

TypeOperators
short_text, long_text= != contains starts_with in exists is_empty
number, measurement= != > >= < <= between in exists is_empty
range> >= < <= between (on the range start) exists is_empty
boolean= != exists is_empty
date= != > >= < <= between exists is_empty
tagscontains (has this tag) in (has any of) exists is_empty
file, image, chartexists is_empty only
compositeall operators; typed by the evaluated result

| where — filtering

| where p"Weight" > 10 and not p"Origin" = "CN"
| where p"Material" in ["steel", "aluminium"] or p"Tags" contains "recycled"

Boolean logic

and, or, not, and parentheses, with the usual precedence (not before and before or). Multiple where stages are AND-ed together — split long filters across stages to keep them readable.

Operators

OperatorExampleNotes
= / !=p"Material" = "steel"equality / inequality
> >= < <=p"Weight" >= 2.5numbers, measurements, dates, ranges
betweenp"Weight" between 1 and 5inclusive bounds
inp"Material" in ["steel", "wood"]any of; max 100 values
containsp"Notes" contains "recall"case-insensitive substring; on tags: has this tag
starts_withp"SKU" starts_with "EU-"case-insensitive prefix
existsp"Weight" existshas a non-empty value
is_emptyp"Weight" is_emptymissing, null, "" or []

Values are numbers (10, -2.5), strings ("steel"), or booleans (true / false). Dates are written as strings ("2026-01-01") and parsed server-side.

JSON paths

Nested values (typically composites) are addressed with dot paths. Numeric comparisons only match values that are actually numeric in the JSON.

| where p"Dimensions".width <= 40

Publish-status conditions

Two status dimensions can be filtered anywhere a property condition can, and compose with and / or / not:

| where publish_status != "published"          # explicit status (null-aware)
| where resolved_status = "draft"              # effective/inherited status
DimensionSemanticsOperators
publish_statusThe explicitly set status. != is null-aware: passports with no explicit status count as "not X".= != in exists is_empty
resolved_statusThe effective status after inheritance, with the series-gate rule applied; default draft, never empty.= != in

resolved_status is computed live by walking the parent chain (≤25 hops), so it's never stale but heavier than other filters — prefer status: or publish_status when explicit status is what you mean.

| select — projection

Row queries project a mix of dimensions and properties. Without a select, the default projection is snowflake, friendly_id, tppmp.

| select friendly_id, tppmp, p"Weight", p"Tags"

Available dimensions: snowflake, friendly_id, tppmp, depth, ontology_id, publish_status, created_at, updated_at. Property values come back in their raw shape (numbers as numbers, tags as lists).

| group by + | agg — analytics

| group by p"Material"
| agg avg(p"Weight") as avg_weight, count() as n
  • Group keys: groupable properties (short_text, long_text, number, boolean, date, measurement, composite) or dimensions.
  • Aggregates: count(), sum(p"X"), avg(p"X"), min(p"X"), max(p"X"), distinct_count(p"X"). sum / avg / min / max need numeric-ish properties. Every aggregate needs an alias (as name).
  • agg without group by computes whole-result aggregates (one row).
  • Grouped results are capped at 1000 groups (the Chart tab, at 50) — beyond that you get a typed error asking you to narrow.

| sort and | limit

| sort avg_weight desc, n
| limit 200
  • Row queries sort by one dimension or property (ties broken by snowflake) and paginate by keyset cursor — pages stay stable even as data changes. limit is the page size, 1500 (default 100).
  • Grouped queries sort by aggregate aliases or group keys (up to two terms).
  • Direction is asc (default) or desc; empty values sort last.

| set — transforms (bulk writes)

Appending a set stage turns the query into a transform: from + where select the passports, set describes the writes. Transforms are the only Lab operation that writes data, so they require the admin/owner role, a mandatory preview, and the most recent one can be rolled back.

from ontology:"Battery X" | where p"Weight" exists
| set p"Weight" = p"Weight" * 2.20462,
      p"Origin" = concat("EU-", p"Plant Code"),
      publish_status = "draft"

Targets — writable property types (short_text, long_text, number, boolean, date, measurement) and publish_status (a quoted status, or null to clear it so the passport inherits again; setting a status pins it).

Expressions

ElementExample
Literals2.20462, "EU", true
Property valuesp"Weight" — the passport's live value at apply time
Arithmetic+ - * / with parentheses
Concatenationconcat("EU-", p"Code") — nulls become ""
Castingcast(p"Code", "number") — types "number", "text", "boolean"

Values are recomputed from live passport data when the transform runs — the read model only steers selection. A passport whose expression fails (non-numeric operand, division by zero, cast failure, type mismatch) is skipped and reported; the run always finishes. Up to 16 assignments per transform.

Limits

CapValue
Filter nesting / conditions8 levels / 64 conditions
in list100 values
Page size500 rows
Groups1000 (chart: 50)
Aggregates / group keys / sort terms16 / 8 / 2
set assignments16
Statement timeout10 s → typed error suggesting narrowing or an export

Results carry a watermark ("data as of …"): the read model syncs from the passport store within seconds of a write, and repeated identical queries are served from a cache that auto-invalidates on any team write. Full result sets are downloaded via exports (CSV/XLSX from the Run history).

Recipes

Common queries

# Heaviest materials, published stock only
from ontology:"Products" status:published
| group by p"Material" | agg avg(p"Weight") as w, count() as n | sort w desc

# Everything that is NOT effectively published (incl. inherited/unset)
from ontology:"Products" | where resolved_status != "published"

# Data quality: missing critical fields
from ontology:"Products" | where p"Weight" is_empty or p"Datasheet" is_empty

# Bulk release a batch's series instances
from ontology:"Products" depth:B | where p"Batch No" = "B-2026-07"
| set publish_status = "published"

# Unit migration with a provenance note
from ontology:"Products" | where p"Weight (lbs)" exists
| set p"Weight" = p"Weight (lbs)" / 2.20462, p"Notes" = concat(p"Notes", " [kg-migrated]")

Ready to run these? Head to Synex Lab for the query, transform, and rollback endpoints.

Was this page helpful?