> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nitsor.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Every page in this documentation carries a `plans` list in its frontmatter and a page-details table naming the plans its content applies to: `All plans`, or a subset of `Community`, `Team` and `Enterprise`. A page limited to some plans also opens with a plan marker naming them.
> Use [Capabilities and limits](/product-status) for capability entitlements and documented availability and limits, and [Plans and billing](/plans-and-billing) for plan pricing and billing policy. Those pages are canonical; do not duplicate their policy values here.
> Reading this documentation grants no product access and no permission. No agent can create or approve a Nitsor release.

# Query language (NQL1)

> The complete NQL1 reference: what each query selects, and why the parser refuses one.

NQL1 (Nitsor Query Language 1) is the filter language Nitsor uses to say which Scans or Tasks a query selects. It is a small, strict dialect in the spirit of Lucene, and it is neither Lucene nor SQL. Every rule on this page is the behaviour of Nitsor's NQL1 parser, which reads query text and either builds a query tree from it or refuses it with a code.

> [!NOTE]
>
> The dashboard, public HTTP API and Python SDK do not accept or evaluate NQL1 queries, and do not list query fields. [Capabilities and limits](https://docs.nitsor.com/product-status.md#limits) records this boundary.

For the query tree, the envelope and machine-readable schemas, read [Query language for agents](https://docs.nitsor.com/reference/query-agents.md). For worked examples, read [Query recipes](https://docs.nitsor.com/guides/query-recipes.md).

## A first query

```text nql1 wrap theme={"system"}
source.modality:CT AND source.registered:>=2026-09-01
```

| Part | What it means |
| - | - |
| `source.modality:CT` | Equality: the Source's modality is `CT`. A filter is a field, an operator and a value, with no spaces between them. |
| `AND` | Both sides must hold. Keywords are written in capitals, and `AND` is never assumed. |
| `source.registered:>=2026-09-01` | A comparison. A date-only value is a whole calendar day in the envelope's time zone, so this starts at midnight on 1 September. |

To change it:

* **Add MR.** Write `(source.modality:CT OR source.modality:MR) AND source.registered:>=2026-09-01`. The parentheses keep the date condition on both, because `AND` binds before `OR`. Use `OR` rather than `ANY(...)` here: `source.modality` holds one value, and its field definition does not admit `ANY`.
* **End the window.** Write `source.modality:CT AND source.registered:[2026-09-01 TO 2026-09-30]`. The upper day is included in full.

## What a query is

A query is filter text. Line feeds and carriage returns count as whitespace, so a long query may span several lines. The empty query, or text that is only whitespace, means everything in the authorised, selected scope.

The text never runs on its own. It runs inside an envelope that fixes the universe: Scans or Tasks, one project, a version to read, a time zone, the instant `now` stands for, which label review states count, and the field registry revision the query was checked against. The same text means the same predicate in every envelope; the envelope decides which records it runs over. [The envelope](https://docs.nitsor.com/reference/query-agents.md#the-envelope) lists every field.

Three separate checks stand between text and results:

| Check | It decides | Where it is described |
| - | - | - |
| Language | Whether the text is NQL1, and what tree it means | This page |
| Field registry | Which fields exist, which you may see, their types and which operators each admits | [Read a query field](https://docs.nitsor.com/guides/discover-query-fields.md) |
| Planner | Whether Nitsor can evaluate that shape over that scope | [Outcomes and what to do](https://docs.nitsor.com/reference/query-agents.md#outcomes-and-what-to-do) |

Valid syntax promises nothing about a field or its cost. `colour:blue` is valid NQL1 whether or not any field called `colour` exists.

## Filters

From here on, the examples mix real field names with short synthetic ones such as `tags`, `notes` and `meta.partNumber`. The synthetic names illustrate grammar, and an example is not a promise that a field exists or can be evaluated. [Fields used in the examples](https://docs.nitsor.com/guides/discover-query-fields.md#fields-used-in-the-examples) says which names are real.

A filter, also called a predicate, is one token: a field, an operator and a value with no whitespace between them. Whitespace is allowed only inside the brackets of a range and the parentheses of a set.

| Form | Example | Selects records where the field |
| - | - | - |
| Equality | `source.modality:CT` | equals the value; on a list field, contains it |
| Equality, explicit | `source.modality:=CT` | the same as `:` |
| Not equal | `source.modality:!=CT` | has a value, and no element equals this one |
| Comparison | `source.spacing.z:<=0.5` | compares as stated: `>` `>=` `<` `<=` |
| Range | `source.rows:[256 TO 1024]` | falls inside the bounds |
| Existence | `has:label.provenance.modelVersion` | has a value, including `false` or `0` |
| Any of | `tags:ANY(crack, porosity)` | contains at least one of the values |
| All of | `tags:ALL(crack, porosity)` | contains every one of the values |
| Prefix | `asset.name:blade*` | starts with the literal text |
| Substring | `asset.name~"blade"` | contains the literal text |

```text Refused query wrap theme={"system"}
class: Crack
```

Refused with `whitespace_in_predicate`. Write `class:Crack`.

```text Refused query wrap theme={"system"}
Crack
```

Refused with `expected_colon`. NQL1 has no free-text search: every filter names a field.

### Equality and comparison

`field:value` and `field:=value` are the same equality, and canonical text writes `:` alone. The comparators are `=`, `!=`, `>`, `>=`, `<` and `<=`, written straight after the colon. A comparator takes one value.

```text nql1 wrap theme={"system"}
source.spacing.z:>=0.12 AND source.spacing.z:<0.5
```

```text Refused query wrap theme={"system"}
tags:!=ANY(crack, porosity)
```

Refused with `comparator_with_set`. To exclude several values, write `NOT tags:ANY(crack, porosity)`.

### Ranges

A square bracket includes its bound, a curly brace excludes it, and `*` leaves a side open. The two sides may mix: `{0 TO 1]` excludes 0 and includes 1.

```text nql1 wrap theme={"system"}
source.registered:[2026-09-01 TO 2026-09-30] AND source.rows:{0 TO 1024]
```

`TO` needs whitespace on both sides, even next to a quoted bound, so `["a"TO"b"]` is refused with `invalid_range`. Whitespace just inside the brackets is optional, and this range is the same as `source.rows:[256 TO 1024]`:

```text nql1 theme={"system"}
source.rows:[ 256 TO 1024 ]
```

At least one side needs a bound; `has:field` is the way to ask for any value. Both bounds must be the same kind of value: text, number, Boolean, duration or time. A date and a relative date are both time, so they may bound one range, as in `source.registered:[2026-09-01 TO now/d}`.

```text Refused query wrap theme={"system"}
source.rows:[* TO *]
```

Refused with `range_unbounded`.

```text Refused query wrap theme={"system"}
source.rows:[1 TO 2026-01-01]
```

Refused with `range_kind_mismatch`.

### Existence

`has:field` selects records where the field has a usable value. `false` and `0` are usable values; a missing field and a stored null are not. NQL1 does not settle whether an empty list is a usable value. There is no `null` literal: to find records without a value, write `NOT has:field`.

```text nql1 wrap theme={"system"}
NOT has:label.provenance.modelVersion
```

```text Refused query wrap theme={"system"}
meta.partNumber:null
```

Refused with `null_literal`. Write `NOT has:meta.partNumber`.

### Sets

`ANY(...)` and `ALL(...)` take one or more scalar values, separated by commas, with no trailing comma. Spaces around the values are allowed. Values keep their order and repeats.

```text nql1 wrap theme={"system"}
tags:ANY(crack, porosity) AND tags:ALL(crack, "weld seam")
```

On a list field, `ANY` means the list contains at least one of the values and `ALL` means it contains every one. Every set value in a query counts toward one shared limit of 32.

### Prefix and substring

A prefix is a plain word of at least three characters followed by `*`. It is the only wildcard: there are no leading, infix or `?` wildcards, no regular expressions and no fuzzy matching. The word must itself read as text, so `now*`, `true*`, `null*` and `AND*` are refused with `invalid_prefix`, while `nowhere*` is a prefix.

A substring filter uses `~` and a quoted string, and matches that literal text.

```text nql1 wrap theme={"system"}
asset.name:blade* OR asset.name~"blade"
```

```text Refused query wrap theme={"system"}
asset.name:bl*
```

Refused with `prefix_too_short`.

Both forms are part of the language, and a field admits them only if the registry lists them among its operators. No field in the current field registry lists prefix or substring.

## Combining filters

`AND`, `OR` and `NOT` combine filters, and parentheses group them. Precedence is `NOT`, then `AND`, then `OR`. Nothing is implied: two filters side by side are refused, so every join is written.

| Written | Reads as |
| - | - |
| `label.class:Crack OR label.class:Porosity AND source.modality:CT` | Crack on any Source, or Porosity on a CT Source |
| `(label.class:Crack OR label.class:Porosity) AND source.modality:CT` | Crack or Porosity, and in either case a CT Source |
| `NOT (label.class:Crack OR label.class:Porosity)` | neither Crack nor Porosity |
| `NOT (NOT has:meta.partNumber)` | has a part number |

`NOT NOT a:1` is refused with `unexpected_keyword`; double negation needs its own group, `NOT (NOT a:1)`. Keywords need no spaces next to parentheses or quotes, so `(a:1)OR(b:2)` is valid.

```text Refused query wrap theme={"system"}
label.class:Crack label.reviewState:accepted
```

Refused with `missing_operator`. Join the filters with `AND` or `OR`.

```text Refused query wrap theme={"system"}
label.class:Crack and label.reviewState:accepted
```

Refused with `keyword_case`. Keywords are written in capitals.

## Missing values, lists and negation

Four rules decide what happens when a field is absent or holds a list:

1. A missing value, a null and an empty list satisfy no equality, comparison, range, prefix, set or substring test. `!=` is a comparison, so it fails on them too.
2. `has:field` needs a usable value, as [Existence](#existence) defines it.
3. `field:!=value` needs an existing value and no element equal to `value`.
4. `NOT field:value` is the plain opposite of `field:value`, so it also matches records where the field is missing.

`field:!=value` and `NOT field:value` are different trees. NQL1 never rewrites one into the other, and neither should a tool that edits queries.

> [tab] List field
>
> A list field `tags` on five records:
>
> | Filter | absent | `[]` | `[crack]` | `[crack, porosity]` | `[porosity]` |
> | - | - | - | - | - | - |
> | `tags:crack` | no | no | yes | yes | no |
> | `tags:!=crack` | no | no | no | no | yes |
> | `NOT tags:crack` | yes | yes | no | no | yes |
> | `tags:ANY(crack, porosity)` | no | no | yes | yes | yes |
> | `tags:ALL(crack, porosity)` | no | no | no | yes | no |
> | `NOT tags:ANY(crack, porosity)` | yes | yes | no | no | no |
> | `has:tags` | no | not fixed | yes | yes | yes |
>
> NQL1 fixes that an empty list fails every value test. Whether an empty list counts as a usable value for `has:` is not fixed by the language, so do not depend on `has:tags` to separate `[]` from an absent field.
>
> The `!=` row is the language's meaning, and the field registry does not admit `!=` on a list field. `NOT tags:crack` is not a replacement: it also matches records with no tags, so it asks a different question. To require tags and exclude crack, write `has:tags AND NOT tags:crack`. It agrees with the `!=` row in every column except `[]`, where its answer depends on `has:`, which is not fixed.

> [tab] Boolean field
>
> A Boolean field `inspection.passed` on three records:
>
> | Filter | absent | `false` | `true` |
> | - | - | - | - |
> | `inspection.passed:true` | no | no | yes |
> | `inspection.passed:false` | no | yes | no |
> | `inspection.passed:!=true` | no | yes | no |
> | `NOT inspection.passed:true` | yes | yes | no |
> | `has:inspection.passed` | no | yes | yes |
> | `NOT has:inspection.passed` | yes | no | no |
>
> `NOT inspection.passed:true` and `inspection.passed:false` differ exactly on the records where the field is absent.

> [tab] Number field
>
> A number field `defects.count` on three records:
>
> | Filter | absent | `0` | `3` |
> | - | - | - | - |
> | `defects.count:0` | no | yes | no |
> | `defects.count:>0` | no | no | yes |
> | `defects.count:!=0` | no | no | yes |
> | `NOT defects.count:>0` | yes | yes | no |
> | `has:defects.count` | no | yes | yes |
>
> Zero is a value. A record with no count is not a record with zero defects.

## Related records

A Scan has related records: its labels, its Source and its Tasks. Flat fields such as `label.class` and `label.reviewState` are tested separately, so two flat label filters may be satisfied by two different labels. A relation block asks for one related record that satisfies everything inside it.

```text nql1 wrap theme={"system"}
label(class:Crack AND reviewState:accepted)
```

Language illustration

## Flat label fields versus one label block



| Query form | Label A: Crack, corrected | Label B: Porosity, accepted | The Scan matches |
| - | - | - | - |
| Flat label fields | satisfies the class filter | satisfies the review-state filter | Yes |
| One label block | class only | review state only | No |

The envelope's label policy here reads accepted and corrected labels. Under the default policy, accepted only, Label A is not read at all and neither query matches.

The rules for relation blocks:

* There are three relations: `label(...)`, `source(...)` and `task(...)`. The name is followed directly by `(`.
* Inside a block, field names belong to the related record: `label(class:Crack)`, not `label(label.class:Crack)`.
* Each block may match a different related record. `label(class:Crack) AND label(class:Porosity)` selects Scans with a Crack label and a Porosity label, which may be two labels.
* Blocks do not nest, at any depth. A query holds at most 8 blocks.
* `NOT` works outside and inside a block, with different meanings. `NOT label(class:Crack)` selects Scans with no Crack label. `label(NOT class:Crack)` selects Scans with at least one label that is not Crack.
* Label fields and `label(...)` read only labels whose review state is in the envelope's label policy. The default policy is `accepted` only; `corrected` is a separate state and is read only when the policy names it.
* A relation word followed by `:` is an ordinary field name, so `label:Crack` is a flat filter on a field called `label`.

Nitsor does not evaluate relation blocks; [Capabilities and limits](https://docs.nitsor.com/product-status.md#limits) records this.

```text Refused query wrap theme={"system"}
label (class:Crack)
```

Refused with `relation_space`.

```text Refused query wrap theme={"system"}
label(source(registered:now/d))
```

Refused with `nested_relation`. Write `label(...) AND source(...)`.

```text Refused query wrap theme={"system"}
scan(asset.name:blade-0261)
```

Refused with `unknown_relation`. Only `label`, `source` and `task` group filters on one related record.

## Field names

A field name is a path of segments. The first segment is an identifier: a letter or underscore, then letters, digits and underscores. After it come dotted identifier segments or bracketed keys. A bracketed key is a JSON string in square brackets and may hold any non-empty text.

| Written | Path | Notes |
| - | - | - |
| `source.spacing.z` | `source`, `spacing`, `z` | three identifier segments |
| `meta["machine.name"]` | `meta`, `machine.name` | one key that contains a dot |
| `meta.machine.name` | `meta`, `machine`, `name` | a different field from the one above |
| `meta["machine"]` | `meta`, `machine` | the same field as `meta.machine` |
| `meta["Inspection protocol"]` | `meta`, `Inspection protocol` | a key with a space |
| `dicom["0018,1030"]` | `dicom`, `0018,1030` | a key with a comma |
| `a["1"]` | `a`, `1` | a key that starts with a digit; `a.1` is refused |

The first segment may not be a keyword or `has`; later segments may be any identifier, so `meta.AND` is a valid path. Field names are case-sensitive. Keys use double quotes only.

```text Refused query wrap theme={"system"}
meta['machine.name']:scanner-01
```

Refused with `invalid_field`. Write `meta["machine.name"]:scanner-01`.

## Values

| Kind | Written | Rules |
| - | - | - |
| Text | `Crack`, `weld-ct-v2`, `"front view"` | A bare word is a letter or underscore, then letters, digits, `_` and `-`. Anything else is a JSON string. |
| Number | `48`, `-3`, `0.12` | No exponent, no leading zeros, no leading `.`, no `NaN`. |
| Boolean | `true`, `false` | Lower case. |
| Date | `2026-09-01` | A real calendar day from 1583-01-01 to 9999-12-31. |
| Relative date | `now`, `now-7d`, `now+2h`, `now/d`, `now-7d/d` | Moves the envelope's pinned instant by fixed units; `/d` takes the day. |
| Duration | `15m`, `48h`, `2d`, `1w` | Units `m`, `h`, `d` and `w` only. |
| Timestamp | `"2026-09-01T10:00:00+02:00"` | Quoted RFC 3339 text with an explicit offset. The field registry types it. |

### How an unquoted value is read

An unquoted value runs up to whitespace or one of `( ) [ ] { } , "`. It is then read in this order, and the first match wins: `*` alone (an open range side), a trailing `*` (a prefix), a date, a relative date, a duration, a number, a Boolean, a bare word. A value that is none of these is refused, so quote it.

That order is why text that looks like another kind needs quotes:

| Written | Read as |
| - | - |
| `2026-09-01` | a date |
| `"2026-09-01"` | the text 2026-09-01 |
| `48h` | a duration |
| `"48h"` | the text 48h |
| `true` | a Boolean |
| `"true"` | the text true |
| `1.2.840.10008` | refused with `invalid_value` |
| `"1.2.840.10008"` | the text 1.2.840.10008 |

A run that starts `now+`, `now-` or `now/` must be a valid relative date, so `now-ish` is refused with `invalid_relative_date`; write `"now-ish"`.

### Strings and escapes

A string is a JSON string: double quotes, JSON escapes (`\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`) and no raw control characters. Single quotes are not strings. Quoting a bare word changes nothing: `class:"Crack"` and `class:Crack` are the same filter.

```text nql1 theme={"system"}
meta.note:"contains \"quoted\" text"
```

### Numbers keep their precision

A number is kept as decimal text, never as a floating-point value, so no digit is lost. Canonical form drops trailing fractional zeros and the sign of zero: `1.50` is `1.5` and `-0.0` is `0`.

```text nql1 theme={"system"}
measurement:12345678901234567890.10
```

Its canonical text is `measurement:12345678901234567890.1`.

Units are not part of a value. The field registry declares a field's units, and the value is the bare number in those units.

```text Refused query wrap theme={"system"}
source.spacing.z:<=0.5mm
```

Refused with `invalid_value`. Write `source.spacing.z:<=0.5` for a field the registry declares in millimetres.

### Case and display names

Keywords (`AND`, `OR`, `NOT`, `TO`, `ANY`, `ALL`) are written in capitals. In lower case they are refused with `keyword_case` wherever they stand, as operators or as values; quote one, as in `notes:"and"`, to search for the word. A lower-case keyword followed by `:` is a field name, so `not:1` is valid.

Field paths, IDs and option IDs are case-sensitive. The parser never trims, folds or normalises text; how text values match is the field registry's rule, versioned with it.

A display name such as `Crack` is a text value. Resolving it to a class, option or person ID is the field registry's job, and happens only when the name is unambiguous; the parser never picks a match.

## Dates, durations and time zones

Time in NQL1 comes from the envelope: its `timeZone` (default `UTC`) and its `asOf`, the pinned instant that `now` stands for.

* **A date-only value is a calendar day in the envelope's time zone.** An inclusive upper day bound includes the whole day, so `[2026-09-01 TO 2026-09-30]` ends before the start of 1 October in that zone. Around a clock change a day can last 23 or 25 hours; its duration depends on the zone.
* **`now` is `asOf`.** `now-7d` is exactly seven 24-hour periods earlier, whatever the clocks did. Units are minutes, hours, days and weeks. Calendar months, years and seconds are refused with `unsupported_time_unit`.
* **`/d` takes the day.** It turns the instant into the calendar day that contains it in the zone, and that day then behaves exactly like a date-only value. The offset is applied first: `now-7d/d` is the day containing the instant 7 × 24 hours before `asOf`.
* **Without `/d`, a relative date is an instant.**

With `timeZone` set to `Europe/Berlin` and `asOf` at `2026-10-25T12:00:00.000Z`, a day when Berlin clocks go back:

| Filter | Selects instants (UTC) | Why |
| - | - | - |
| `source.registered:now/d` | from 2026-10-24 22:00 to before 2026-10-25 23:00 | today, a 25-hour day |
| `source.registered:<=now/d` | before 2026-10-25 23:00 | an inclusive day bound includes all of today |
| `source.registered:<now/d` | before 2026-10-24 22:00 | ends before today's midnight |
| `source.registered:>now/d` | from 2026-10-25 23:00 | starts tomorrow |
| `task.completed:>=now-1d` | from 2026-10-24 12:00 | exactly 24 hours |
| `task.completed:>=now-1d/d` | from 2026-10-23 22:00 | the start of yesterday in Berlin |

A duration compares with a duration field such as time waited, and a range of durations uses duration bounds:

```text nql1 wrap theme={"system"}
task.waited:>48h AND task.waited:[0m TO 2w}
```

A timestamp is quoted text with an explicit offset:

```text nql1 wrap theme={"system"}
task.completed:>"2026-09-01T10:00:00+02:00"
```

> [accordion] Days that clocks split or skip
>
> A day is every instant whose local date is that day. Where the clocks went back over midnight, as in America/Goose\_Bay on 7 November 2010, the day is two separate periods that no pair of bounds can express. Resolving it is refused with `calendar_day_not_contiguous`, for a date-only value and for a `/d` day alike.
>
> A day the clocks skipped entirely, as in Pacific/Apia on 30 December 2011, is the empty interval at the jump.
>
> Because `/d` is applied after the offset, `[now-7d/d TO now/d]` usually covers the seven days before today and today. If the clocks changed during the week, the instant 7 × 24 hours back falls at a different time of day than `asOf`. When `asOf` is close to midnight, that can move the first day by one, giving seven or nine local days.

> [accordion] Supported range of days and zones
>
> Days from 1583-01-01 to 9999-12-31, and instants within them, can be resolved; the Gregorian calendar is in force everywhere from 1583. An earlier day is refused with `date_out_of_range`. A relative date that lands outside the range is refused with `relative_date_out_of_range`, and an offset longer than the whole range is refused as soon as it is parsed.
>
> The envelope's `timeZone` is an IANA zone name written exactly as the pinned tz database table spells it (tzdata 2026c), or `UTC`. `Etc/UTC`, `Etc/GMT`, `europe/berlin` and `+02:00` are refused; links such as `Asia/Calcutta` are accepted as written, so two spellings of one zone are two different envelopes. A zone the evaluating runtime cannot resolve is refused with `time_zone_unavailable`.

## Canonical form

Every valid query has one canonical text. Query builders, agents and anyone who stores a query should store and compare canonical text, because two spellings of the same filter produce the same tree and the same canonical text.

Canonical text uses single spaces around keywords, `, ` between set values, capital keywords, `:` for equality, the fewest parentheses precedence allows, and `NOT (...)` around a group. A text value is written bare when it reads back as the same text and as a JSON string otherwise. Identifier segments are written `.segment` and other keys `["key"]`. An open range side is written `[*` or `*]`.

| Written | Canonical |
| - | - |
| `class:"Crack"` | `class:Crack` |
| `source.spacing.z:<=0.50` | `source.spacing.z:<=0.5` |
| `source.registered:{* TO 2026-09-30}` | `source.registered:[* TO 2026-09-30}` |
| `tags:ANY( porosity ,crack )` | `tags:ANY(porosity, crack)` |
| `((a:1)) AND (b:2 AND (c:3 AND d:4))` | `a:1 AND b:2 AND c:3 AND d:4` |
| `(a:1 AND b:2) OR c:3` | `a:1 AND b:2 OR c:3` |
| `NOT(NOT a:1)` | `NOT (NOT a:1)` |
| `meta["machine"]:scanner-01` | `meta.machine:scanner-01` |
| `meta["Inspection protocol"]:"weld-ct-v2"` | `meta["Inspection protocol"]:weld-ct-v2` |
| `source.registered:>=now+0d/d` | `source.registered:>=now/d` |

Canonical form keeps operand order, duplicate set values and the exact spelling of every text value. Parsing canonical text returns the same tree, and serialising that tree returns the same text.

## Limits

Limits apply to each query. A limit refuses the query; it never truncates it, and NQL1 never expands a query into a larger equivalent form.

| Limit | Value | Refusal code |
| - | - | - |
| Canonical text | 4 KiB of UTF-8 | `query_too_long` |
| Raw input, whitespace included | 16 KiB of UTF-8 | `query_too_long` |
| Nesting depth of the tree, and separately of parentheses in the text | 8 | `depth_exceeded` |
| Filters | 32 | `predicates_exceeded` |
| Relation blocks | 8 | `relations_exceeded` |
| Set values, summed across every `ANY` and `ALL` | 32 | `set_values_exceeded` |
| OR alternatives, summed across every OR group | 8 | `or_branches_exceeded` |

OR alternatives are summed: `(a:1 OR a:2) AND (b:1 OR b:2)` has four. A tree or envelope sent as JSON is also bounded before any schema reads it: [Trees sent as data](https://docs.nitsor.com/reference/query-agents.md#trees-sent-as-data) lists those bounds. Execution budgets are separate and belong to the planner.

## Refusals

A refused query returns the first refusal only, with a stable code and a sentence a person can act on. A text refusal carries a span: a half-open range of UTF-16 code unit offsets into the text, never past its end. The parser checks text in this order: the raw input ceiling, then syntax, then the limits on the canonical tree, then the length of the canonical text. Within syntax, the first error wins.

Each rule above shows its own refusal. [Repairable refusals](https://docs.nitsor.com/reference/query-agents.md#repairable-refusals) maps the common codes to their fixes, and [Refusal codes](https://docs.nitsor.com/reference/query-agents.md#refusal-codes) lists every code with its message. Codes are part of the language version: a code is never renamed or reused for another meaning within NQL1.

## Versioning

NQL1 is frozen. A change to what any accepted text means is a new language version, never a reinterpretation of a stored query. A stored query records its language version separately from the field registry revision it was checked against. Nitsor refuses an unknown language version rather than guessing.

The language has deliberate boundaries. It has no free-text search, no `null`, no calendar months or years, no days before 1583, no regular expressions, no fuzzy matching and no leading or infix wildcards. Unquoted values are deliberately narrow, so `CT-2.5`, `1.2.840.10008` and words that start with a digit are quoted.

## Full grammar

> [accordion] Grammar in EBNF
>
> ```text grammar theme={"system"}
> query        = [ expression ] ;
> expression   = orExpr ;
> orExpr       = andExpr , { "OR" , andExpr } ;
> andExpr      = unaryExpr , { "AND" , unaryExpr } ;
> unaryExpr    = [ "NOT" ] , primary ;
> primary      = "(" , expression , ")"
>              | relation , "(" , expression , ")"      (* no space before "(" *)
>              | predicate ;
> relation     = "label" | "source" | "task" ;
>
> (* A predicate is one token: no whitespace between its parts, except inside
>    range brackets and set parentheses. *)
> predicate    = "has:" , field
>              | field , ":" , operand
>              | field , "~" , string ;
> operand      = scalar
>              | comparator , scalar
>              | range
>              | prefix
>              | set ;
> comparator   = "=" | "!=" | ">" | ">=" | "<" | "<=" ;
> range        = ( "[" | "{" ) , [ ws ] , bound , ws , "TO" , ws , bound , [ ws ] , ( "]" | "}" ) ;
> bound        = scalar | "*" ;
> prefix       = bare , "*" ;
> set          = ( "ANY" | "ALL" ) , "(" , setValue , { "," , setValue } , ")" ;
> setValue     = [ ws ] , scalar , [ ws ] ;
> ws           = whitespace , { whitespace } ;            (* see whitespace below *)
>
> field        = identifier , { "." , identifier | "[" , string , "]" } ;
> scalar       = string | date | relativeDate | duration | number | boolean | bare ;
> relativeDate = "now" , [ ( "+" | "-" ) , unsigned , timeUnit ] , [ "/d" ] ;
> duration     = unsigned , timeUnit ;
> timeUnit     = "m" | "h" | "d" | "w" ;
> boolean      = "true" | "false" ;
> ```
>
> The grammar describes structure. The lexical rules below, and the relation rule that blocks do not nest, narrow it further.

> [accordion] Lexical rules
>
> | Token | Rule |
> | - | - |
> | `identifier` | `[A-Za-z_][A-Za-z0-9_]*`, at the start and after a dot. A field's first identifier may not be a keyword or `has`. |
> | `bare` | `[A-Za-z_][A-Za-z0-9_-]*`, excluding keywords in either case, `true`, `false`, `null` and any word that spells a relative date. |
> | `string` | A JSON string: double quotes, JSON escapes, no raw control characters. |
> | `number` | An optional `-`, then `0` or a digit 1 to 9 followed by digits, then optionally `.` and one or more digits. No exponent, no leading zeros. |
> | `date` | `YYYY-MM-DD`, a real calendar day from 1583-01-01 to 9999-12-31. |
> | `unsigned` | `0`, or a digit 1 to 9 followed by digits. |
> | keywords | `AND OR NOT TO ANY ALL`, in capitals, as whole words only. |
> | whitespace | Space, tab, line feed and carriage return. Other space characters are refused with `unexpected_character`. |

## Next step

> [card] [Query language for agents](https://docs.nitsor.com/reference/query-agents.md)
>
> The tree and envelope as JSON, with schemas to download.

> [card] [Query recipes](https://docs.nitsor.com/guides/query-recipes.md)
>
> Worked queries for common questions, with what each one selects.

[Read a query field](https://docs.nitsor.com/guides/discover-query-fields.md) explains how a field's definition limits what you can write.

<table aria-label="Page details">
  <tbody>
    <tr><th scope="row">Plans</th><td>All plans</td></tr>
    <tr><th scope="row">Last verified</th><td>2026-10-25</td></tr>
  </tbody>
</table>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.