Skip to main content

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.

  • Sections 3–5 cover everything a normal user needs day to day.

  • Sections 6–7 cover settings and advanced constructs that admins and power users will occasionally need.

  • Sections 8–9 are a quick reference and a cookbook of examples.

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

text
[:status = "active"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:

  • Object — a single record of data (a document, a product, a user…)user, etc). Every object has an ID.

  • Type — a category of object, such as Document, Word Document or Folder. Types form a hierarchy: a Word Document is a Document and a Document is a File, which is an Object. A subtype inherits all the properties of its parent types.

  • Property — a named field on an object, such as :name, :createdAt or :x. Searches refer to properties with a leading colon: :name.

  • Relation — a property whose value points at another object's ID (or a list of IDs). Relations let objects be connected to each other.

  • Provider — a connected system or database. Each provider has its own numeric ID. An object ID looks like local-id@provider-id; for example, 54c25070abe654a14867226e@0 lives in provider 0.

  • Current user — the person running the search. The language has a shortcut for "me" and "my groups" (see §4.10).

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"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.

  3. Done.

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 capitalisationcapitalization of keywords (AND, OR, JOIN, SORT BY…) do not 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 value.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"Active"]
[:publishedAt >= 2024-01-01]
[:archived = FALSE]

Supported comparison operators: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:

  • * — matches zero or more characters.

  • ? — matches exactly one character.

Examples:



Query Matches
lapt* laptop, laptops, lapt
lap?op laptop, lapsop
[:sku ABC-*] any SKU starting with ABC-
:name "lapt*" text lapt* literally (see note below)

Wildcards only apply to unquoted words. If you put * or ? inside quotes, they are matched literally as characters. Use them unquoted to make them “do something”.

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 )]
  • ( a, b, c ) — matches if the property equals any of the values.

  • ( a TO b ) — matches if the property is between a (inclusive) and b (exclusive).

4.7 Dates and times

A date value can be written as one of:

  • A named date: TODAY, THIS WEEK, THIS MONTH, THIS YEAR, NOW.

  • An offset from any of the aboveNOW: TODAYNOW - 1 MONTH, NOW + 3 HOURS, THIS YEAR - 2 YEARS.

  • An ISO date or datetime: 2024-03-15, 2024-03-15T14:30:00, 2024-03-15T14:30:00.000+02:00.

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]
[:closedAt BETWEEN-THIS-MONTH]
[: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:

  • ME — the current user's own object.

  • MY GROUPS — any group the current user belongs to.

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.nameage => "Alice"]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"Active"] AND [:price < 100]
[:status = "active"Active"] OR [:status = "draft"Draft"]
NOT [:archived = TRUE]

There are a few shortcuts to keep things short:

  • AND is optional when two conditions are adjacent. These two are the same:

    text
    [:status = "active"Active"] [:price < 100]
    [:status = "active"Active"] AND [:price < 100]
  • Brackets group conditions the same way they do in ordinary arithmetic:

    text
    ([:status = "active"Active"] OR [:status = "draft"Draft"]) AND [:price < 100]

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  ":name login"                     # semantic within a property

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 or without a preceding property, and 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"Active"] SORT BY :createdAt DESC
[:status = "active"Active"] SORT BY :priority ASC, :dueAt DESC
  • Multiple sort fields are comma-separated.

  • ASC (ascending, the default) and DESC (descending) are optional. SORT BY :x is the same as SORT BY :x ASC.

  • Sorting can also be supplied via an option (see §6) rather than inside the query.

5.2 Limiting — LIMIT

text
[:status = "active"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"Active"] JOIN :owner [:]
  • :owner is the name you choose for the joined objects.

  • The [:] expression is a filter on the joined objects. Inside this expression, unqualified properties refer to the joined object; use :default:property to refer back to the original.

  • Each match of the joined filter multiplies the original result. An original with two matches appears twice, once for each match.

  • After the join, refer to the joined object's properties as :owner:property.

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

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

Variants:

  • JOIN ALLOW EMPTY :name [...] — outer join. The original object is kept even if no joined object matches. Fields from the joined side are simply missing on those rows. (See also ALLOW EMPTY note in §7.7.)

  • NOT JOIN :name [...] — anti-join. Returns only original objects that have no matching joined object. See §7.7.

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"Active" THEN "Live" ELSE "Draft" )
MAP :total   AS ( :price + :tax + :shipping )
MAP :ageDays AS ( NOW - :createdAt ) / 86400000

Rules of thumb:

  • Inside a MAP, unqualified properties refer to the original object. To refer to a property of a joined object, use :joinedName:joinName:prop.

  • IF cond THEN a ELSE b — ternary.

  • Arithmetic: +, -, *, /.

  • A missing property behaves as the identity value for the operator it's involved in: 0 for +/-, 1 for *//. So :missing * 5 equals 5 and :x + :missing equals :x.

  • You can chain MAPs; a later MAP can refer to a property added by an earlier one:

    text
    MAP :a AS ( :x + :y )
    MAP :b AS ( :a + :z )
  • MAP FOR SORT :prop AS (...) says: "compute this value only for the purpose of sorting; don't add it to the returned object." It behaves identically to MAP for the purposes of sorting. Used only for improved performance.

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

text
[:status = "active"Active"]
  JOIN :child [:kind = "task"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"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. It can also coexist with JOIN.

text
[:status = "active"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 ) )
  • First argument: a numeric property on the current row.

  • Second argument (AS): the target currency, a three-letter code (USD, EUR, BGN, …).

  • The system picks up the object's currency and currencyConversionRate properties to perform the conversion.


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:

  • Case insensitive — treat all text as case-insensitive. "ASD" and "asd" become the same search.

  • Skip system objects — when on, objects whose isSystem flag is true are excluded from results. When off, system objects are included.

  • Search all versions — by default only the latest version of each object is searched. When on, all versions are considered.

  • Diacritic insensitive — find a letter regardless of how it's written with or without accents/diacritics. For example, an accented é and a plain e are treated as the same letter. Useful for languages with many diacritics.diacritics (like French).

  • Use synonym dictionary — when on, the system's global synonym dictionary is consulted, and any of a word's synonyms is also matched.

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 permission datametadata — a set of named rightskey-value paris (such as inheritPermissions: ['read']read, writecascadeDestroy: false, delete, cascadeDestroyetc) that describe what the current user may do to the object through that relation.. RELATES TO filters by this metadata.

Syntax:

text
RELATES TO ID "<target-id>"
  TOWARDS ID "inheritPermissions"
    NOT "read", "cascadeDestroy" TRUE
  • RELATES TO ID "…" — the object has a relation pointing at <target-id>. (You can also write FROM ID "…" to look at the incoming relation direction.)

  • TOWARDS ID "<relation-name>" — restrict to a specific relation property (e.g. inheritPermissions). You can also write FROM ID "…" to look at the incoming relation direction.

  • Then a comma-separated list of permission entries.values.

Each permission entry has the shape:

text
"name"                 # name must be granted (i.e. true)
"name" TRUE            # same as above
"name" FALSE           # name must not be granted
"name" NOT TRUE        # negation: name must not be granted
"name" NOT FALSE       # negation: name must be granted
"name" ALLOW EMPTY     # optional; see below

NOT simply negates the boolean — NOT "read" means read is not granted.

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"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 ;
  • Each comma-separated item names a right (or a right:subright, e.g. read:x).

  • Add FALSE after a right to exclude objects where that right is granted; add TRUE (or nothing) to require it.

  • A PERMITTED clause must end with a semicolon ;.

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"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

  • JOIN ALLOW EMPTY :name [...] — the outer-join variant: keep the original object even when the joined expression matches nothing. On those rows the joined properties will be absent.

  • NOT JOIN :name [...] — the anti-join: keep only original objects that have no matching joined object.

text
[:status = "active"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"Active"] [:price < 100]

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

text
[:types INHERITS "Document"]
  JOIN :owner [:role = "manager"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"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"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"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]