Search Language Guide

A plain-language reference for finding, shaping and retrieving objects across all your connected systems.


1. About this guide

This guide explains the search language that your product uses to look for objects across every connected system (provider) at once. It is written so that anyone — not just developers — can read, understand and write searches.

Wherever a search is shown, it appears in a code block like this:

text
[:status "Active"] SORT BY :createdAt DESC LIMIT 20

2. Very short refresher

You can skip this section if you already know the product, but here is a one-page reminder of the vocabulary used throughout:

A single search is executed against all providers in parallel (by default), so you never need to know which system owns which object.


3. Anatomy of a search

Every search has three logical parts, in this order:

  1. A filter — what you are looking for. Written as a chain of conditions. Example: [:status "Active"].

  2. Zero or more shaping steps — how to transform what you found. Sort, join, group, aggregate, limit, flatten, compute mapped values, return related objects.

Only the filter is required; every shaping step is optional. A search with no filter at all is valid (for example, LIMIT 10 SORT BY :createdAt DESC returns the ten most recent objects of any type).

Spacing and capitalization of keywords (AND, OR, JOIN, SORT BY…) matter.


4. Finding things — the filter

The filter is written inside square brackets when it's a single condition, or as a chain when there are several. The following rules show how to build filters.

4.1 Search for text anywhere

Writing a word by itself searches all text-like properties of every object for that word.

text
laptop

This is a keyword / full-text search by default (see §4.14 to choose semantic search instead).

A quoted phrase searches for the phrase as a whole:

text
"folding laptop stand"

You can combine several bare words; they are treated as AND:

text
laptop stand

is the same as laptop AND stand.

Anywhere text appears in quotes you may escape a double-quote with \" and a backslash with \\. No other escape sequences are supported.

4.2 “Does this property exist?”

Wrapping a property in brackets with nothing else asks: “does this object have any value for this property?”

text
[:email]

matches every object that has an email property. It's a handy way to filter by shape rather than by value.

4.3 Compare a property with a value

Most filters take the form [:property operator value]:

text
[:price < 100]
[:status "Active"]
[:publishedAt >= 2024-01-01]
[:archived FALSE]

Supported comparison operators for numerical and date/time values:

Operator Meaning
= Equals
!= Not equal
< Less than
> Greater than
<= Less than or equal
>= Greater than or equal

There is one more operator, ~=, that applies only to relations. It is described in §7.1.

You can also compare two properties on the same object directly:

text
[:startedAt < :finishedAt]

matches objects where the start date is before the finish date.

4.4 Text inside a specific property

To restrict a text search to one property, put the property first and the text second:

text
[:name laptop]
[:description "folding stand"]

You can also mix wildcards with the property (see §4.6).

4.5 Wildcards

Unquoted text supports two wildcards:

Examples:



Query Matches
lapt* laptop, laptops, lapt
lap?op laptop, lapsop
[:sku ABC-*] any SKU starting with ABC-
   

4.6 Lists and ranges

A property can be compared against a list of values using parentheses:

text
[:status ( "active", "draft", "review" )]
[:size ( 1, 20, 3 )]
[:price ( 10 TO 100 )]

4.7 Dates and times

A date value can be written as one of:

Offsets accept these units:

MILLIS, SECOND(S), MINUTE(S), HOUR(S), DAY(S), WEEK(S), MONTH(S), YEAR(S).

Examples:

text
[:createdAt >= TODAY]
[:dueAt < NOW + 1 WEEK]
[:updatedAt >= 2024-01-01]
[:startsAt < NOW - 24 HOURS]

Anything that is not one of the forms above is treated as a mistake and the search will fail.

The property you compare against must be a date or datetime property.

4.8 Types and inheritance

To match objects of a type or any of its subtypes, use INHERITS:

text
[:types INHERITS "Folder"]
[:types INHERITS ID "54c25070abe654a14867226e@0"]

Both forms match if the object's :types value is of the given type or any type derived from it. The string form uses the type name; the ID form uses the type's object ID.

You can also use INHERITS inside a list, so that a single filter accepts both specific objects and whole type trees:

text
[:parent ( ID "aaa@0", INHERITS ID "bbb@0", INHERITS "Folder" )]

This matches an object whose :parent is either the specific object aaa@0, or any object whose type is bbb@0 (or a subtype), or any Folder (or subtype).

4.9 “Me” and “my groups”

Two shortcuts refer to the current user:

Used with a property, they say “this relation points at me / at one of my groups”:

text
[:assignee ME]
[:members MY GROUPS]

4.10 Arrays — checking length

If a property holds an array, LENGTH compares how many items it has:

text
[:tags LENGTH > 5]
[:path LENGTH = 2]

Only meaningful with >, >=, =, !=, <, <= against a number.

4.11 Nested properties

You can walk into nested data with dots:

text
[:owner.age > 20]
[:address.city "Sofia"]

The same applies in mapped expressions and flatten steps.

4.12 Combining conditions

Use AND, OR and NOT:

text
[:status "Active"] AND [:price < 100]
[:status "Active"] OR [:status "Draft"]
NOT [:archived TRUE]

There are a few shortcuts to keep things short:

See §8 for precedence rules.

4.13 Semantic vs keyword

By default, bare text is a keyword search: it looks for the word in indexed text. You can force keyword search explicitly with KEY, and switch to semantic search with SEM:

text
KEY "user login event"                # keyword / full-text
SEM "user login event"                # semantic

SEM uses whatever embedding engine and model the system is configured with, stored in a vector database, so results are based on meaning, not literal word match. KEY is the ordinary exact-ish match.

SEM and KEY can be used with quoted text or bare words:

text
SEM "failed payment"
KEY failed
SEM error

5. Shaping the result — the clauses

Each of the clauses below is optional and can appear in roughly any order. The sections are ordered by how often you'll use them.

5.1 Sorting — SORT BY

text
[:status "Active"] SORT BY :createdAt DESC
[:status "Active"] SORT BY :priority ASC, :dueAt DESC

5.2 Limiting — LIMIT

text
[:status "Active"] LIMIT 20

Caps the number of results at 20. If LIMIT appears twice, the last one wins.

5.3 Joining — JOIN

A JOIN pulls in related objects that match an expression, and combines each result with each match.

text
[:status "Active"] JOIN :owner [:]

You can have several JOIN clauses, each with its own name:

text
[:status "Active"]
  JOIN :owner    [:role "Manager"]
  JOIN :reviewer [:approved TRUE]

Variants:

5.4 Flatten — FLATTEN

If a property holds an array, FLATTEN expands each element into its own row.

text
[:price >= 1] FLATTEN :tags GROUP BY :tags AGGREGATE count(:tags) AS :n

This turns “one row per object, with N tags each” into “one row per tag”. FLATTEN can be given several properties, and an optional filter that applies to the flattened rows:

text
[:price >= 1] FLATTEN :a, :b [:a > 5]

You can FLATTEN more than once, chaining through nested arrays.

5.5 Grouping and aggregation — GROUP BY / AGGREGATE

Aggregation collapses all rows into a small summary.

text
[:price >= 1] AGGREGATE sum(:price) AS :total

This returns a single row with a :total field.

To get one row per distinct value, use GROUP BY first:

text
[:price >= 1] GROUP BY :status AGGREGATE sum(:price) AS :total AGGREGATE count(:id) AS :n

Each AGGREGATE names an aggregation function, takes one property, and assigns the result to a new property name.

Supported aggregation functions:

Function Meaning
sum Sum of all values
count Number of values
average Arithmetic mean
median Middle value (average of two middles if even)
min Smallest value
max Largest value
first First value in the group
concat A list of all values in the group

These are the only functions supported today; they are not user-extensible.

Grouping by time:

For date/datetime properties, GROUP BY accepts a time unit that snaps each value to a calendar bucket:

text
[:price >= 1] GROUP BY :createdAt MONTHLY AGGREGATE count(:id) AS :n

Available units:

MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY.

The bucket value is returned as a formatted string — e.g. "2020-04" for MONTHLY, "2020-05-15" for DAILY, "2020-05-15 10" for HOURLY.

Groups with zero rows are still returned (for example if a combination of group fields is missing on every object, you will see a group with a count of 0).

5.6 Mapped properties — MAP

MAP adds a computed field to each row. Its syntax is:

text
MAP :newProp AS ( expression )

Examples:

text
MAP :label   AS ( IF :status "Active" THEN "Live" ELSE "Draft" )
MAP :total   AS ( :price + :tax + :shipping )
MAP :ageDays AS ( NOW - :createdAt ) / 86400000

Rules of thumb:

Aggregation functions can also appear inside a MAP, in which case they summarize the joined objects for each original object:

text
[:status "Active"]
  JOIN :child [:kind "Task"]
  MAP :avgChild As ( average(:child:duration) )

CURRENCY_CONVERT is a special map expression:

text
MAP :usd AS ( CURRENCY_CONVERT( :amount AS USD ) )

It converts the numeric value to the given currency using the system's conversion rates. See §5.8.

5.7 Returning related objects — RETURN

RETURN replaces the result set with the objects pointed to by a relation property.

text
[:status "Active"] RETURN :attachments

Here the search that found, say, 10 active documents returns their attachments instead of the documents themselves. If you list several RETURN properties, you get the union of all of them:

text
[:status = "active"] RETURN :attachments, :comments, :owner

RETURN can be combined with subsequent SORT BY, GROUP BY, AGGREGATE and LIMIT, which then operate on the returned objects.

text
[:status "Active"] RETURN :comments SORT BY :createdAt DESC LIMIT 5

5.8 Currency conversion — CURRENCY_CONVERT

text
[:status = "paid"] MAP :usd AS ( CURRENCY_CONVERT( :amount AS USD ) )

6. Options — settings outside the query

Some settings are not part of the search text itself but are supplied alongside it (for example, by a user interface or an API caller). They affect how the same query behaves:

These options apply to any text in the query that is not a strict comparison — i.e., to bare-word searches and to text searches on a property.


7. Advanced

The constructs in this section are supported and available to all users, but are less commonly needed. Administrators will use them frequently; ordinary users may see them generated by UI tools and want to know what they mean.

7.1 The ~= operator

~= is the relation-membership operator. It matches when the sets of related IDs on both sides overlap — that is, when the two sides have at least one common ID.

text
[:x ~= :y]

This says: "property :x and property :y refer to at least one of the same objects." It's most often used after a JOIN, when multiple joined objects may share part of their relation with the original object.

~= is only meaningful on relation properties; it is not a general text operator.

7.2 Comparing an object by ID, and reverse INHERITS

Because relations hold IDs, you can compare a property against a specific object's ID:

text
[:parent = :myFolder]                    # compare with another property
[:author ID "54c25070abe654a14867226e@0"] # compare with a specific object

The ID "…" form works the same way for arrays: [:author ID "…"] matches whether :author is a single reference or a list of references, provided the target is in there.

You can also use inheritance with IDs, which is handy for “who is referenced by this type”:

text
[:owner INHERITS ID "54c25070abe654a14867226e@0"]

And there is a reversed form — read it as “the given type inherits :property”:

text
"Folder" INHERITS :types

This says: “the object's :types list includes a type from which Folder is derived”. It's the mirror image of :types INHERITS "Folder" and is used when the relation points at the parent rather than the child. If you need it in day-to-day work, use INHERITS on the property side; the reversed form is mostly emitted by tools.

7.3 Relation metadata — RELATES TO … TOWARDS ID / FROM ID

Relations can carry per-target metadata — a set of named key-value paris (such as inheritPermissions: ['read'], cascadeDestroy: false, etc). RELATES TO filters by this metadata.

Syntax:

text
RELATES TO ID "<target-id>"
  TOWARDS ID "inheritPermissions"
    NOT "read", "cascadeDestroy" TRUE

ALLOW EMPTY may be added to an entry; it relates to empty relations. Its precise meaning is internal, and you don't need to know it in order to write or read the query — ask your administrator if you need the exact semantics.

7.4 CONTAINS

CONTAINS filters objects based on a sub-expression over one of their related objects, without multiplying results:

text
[:members CONTAINS [:role "Owner" AND :active TRUE]]

This returns the original objects whose :members list contains at least one object matching the sub-expression. Unlike a JOIN, it does not duplicate the result row for each match — it just says "has a match".

7.5 PERMITTED

PERMITTED adds extra rights-based filtering on top of the normal “only what the user can read” behaviour:

text
PERMITTED write, delete FALSE, read:x TRUE ;

Results always contain only objects the user may read; PERMITTED narrows the set further.

7.6 IN PROVIDERS

Search only within specified providers:

text
[:status "Active"] IN PROVIDERS ID "1@0", ID "2@0"

Provider IDs are the number after the @ in an object ID. A list of active providers is available from the system and in the UI, so users can copy the ID of a provider when they need to.

7.7 Empty joins — ALLOW EMPTY and NOT JOIN

text
[:status "Active"] NOT JOIN :comments [:spam TRUE]

This returns active objects whose comments contain no spam — i.e., objects without a matching comment.

7.8 Advanced map expressions

Maps support much more than simple arithmetic. The full grammar is:



Form Meaning
IF <cond> THEN <expr> ELSE <expr> Ternary
<expr> + <expr>, <expr> - <expr> Addition / subtraction
<expr> * <expr>, <expr> / <expr> Multiplication / division
( <expr> ) Grouping
CURRENCY_CONVERT( <prop> AS <CUR> ) Currency conversion
sum(<expr>), count(<expr>), average(<expr>), median(<expr>), min(<expr>), max(<expr>), first(<expr>), concat(<expr>) Aggregation over joined objects within a MAP

Conditions inside IF are the same comparisons as in filters (=, !=, <, >, <=, >=), but written without the surrounding [...]:

text
MAP :tier AS ( IF :spend > 1000 THEN "gold" ELSE "standard" )
MAP :z     AS ( :a + :b * :c - :d / :e )
MAP :avg   AS ( average(:child:duration) )

7.9 Reversed inheritance

Already covered in §7.2 — "Type" INHERITS :property is the mirror of :property INHERITS "Type".


8. Precedence and putting it together

When multiple operators appear in the same filter, they are evaluated in this order (highest first):

  1. Brackets ( ) — always win.

  2. NOT

  3. AND (explicit or implicit — adjacent conditions)

  4. OR

Examples:

text
[:a = 1] OR [:b = 2] AND [:c = 3]

means:

text
[:a = 1] OR ([:b = 2] AND [:c = 3])

Use brackets to change the meaning:

text
([:a = 1] OR [:b = 2]) AND [:c = 3]

Clauses (SORT BY, LIMIT, JOIN, GROUP BY, AGGREGATE, MAP, FLATTEN, RETURN, PERMITTED, IN PROVIDERS) are applied in the order they appear in the query.


9. Cookbook

Here are ready-to-copy examples covering the most common patterns.

Everything that mentions “laptop”:

text
laptop

Active products under 100:

text
[:status "Active"] [:price < 100]

Documents whose owner is a manager (a joined property):

text
[:types INHERITS "Document"]
  JOIN :owner [:role "Manager"]
  SORT BY :owner:name ASC

My tasks that are due this week:

text
[:assignee ME] [:dueAt <= THIS WEEK + 1 WEEK] SORT BY :dueAt ASC

Distinct status counts:

text
[:price >= 0] GROUP BY :status AGGREGATE count(:id) AS :n

Monthly totals for this year:

text
[:createdAt >= THIS YEAR]
  GROUP BY :createdAt MONTHLY
  AGGREGATE sum(:total) AS :sum
  AGGREGATE count(:id) AS :count
  SORT BY :createdAt ASC

A label based on a status:

text
[:status] MAP :label AS ( IF :status "Active" THEN "Live" ELSE "Draft" )

Total and average price, rounded to currency:

text
[:price >= 0]
  MAP :priceUSD AS ( CURRENCY_CONVERT( :price AS USD ) )
  AGGREGATE sum(:priceUSD)   AS :total
  AGGREGATE average(:priceUSD) AS :average

Comments on active documents, newest first:

text
[:status "Active"] RETURN :comments SORT BY :createdAt DESC LIMIT 10

Documents without any review:

text
[:types INHERITS "Document"] NOT JOIN :reviews [:]

Semantic search for “reset my password”:

text
SEM "reset my password"

Keyword search for a specific term:

text
KEY "invoice"

Only returns objects the user can write to (fine-grained rights):

text
[:types INHERITS "Document"] PERMITTED write ;

Search only one provider:

text
[:status "Active"] IN PROVIDERS ID "1@0"

10. Summary cheat-sheet



Want to… Use
Search any text word / "phrase" / word* / w?rd
Search one property's text [:name word]
Force keyword vs semantic KEY word / SEM "some meaning"
Check existence [:property]
Compare [:prop = value], !=, <, >, <=, >=
Compare two properties [:a < :b]
Compare many values [:x ( a, b, c )] / [:x ( a TO b )]
Match a type or subtype [:prop INHERITS "Type" / ID "…@N"]
Reference the current user [:prop ME] / [:prop MY GROUPS]
Count array items [:prop LENGTH > 5]
Date shortcuts TODAY, NOW - 1 MONTH, 2024-03-15
Combine AND (or adjacent), OR, NOT, ( )
Sort / limit SORT BY :a ASC, :b DESC / LIMIT n
Join / anti-join / outer JOIN :n […] / NOT JOIN :n […] / JOIN ALLOW EMPTY :n […]
Flatten an array FLATTEN :arr
Group and summarise GROUP BY :x AGGREGATE sum(:y) AS :total
Compute a new field MAP :z AS ( :x + :y )
Convert currency MAP :z AS ( CURRENCY_CONVERT( :amount AS USD ) )
Return related objects RETURN :attachments
Rights-based filtering PERMITTED write ;
Target specific providers IN PROVIDERS ID "1@0", ID "2@0"
Check specific ID relations RELATES TO ID "…" TOWARDS ID "…" "read" TRUE
Set-of-overlap on relations [:a ~= :b]


Revision #3
Created 9 November 2026 22:51:48 by Pierre
Updated 10 November 2026 13:33:19 by Bruno