TERSL — the language

Reading with queries

How to read TERSL state with queries, from path expressions to read directives, and how to interpret what a query returns.

Overview#

A query is a line that starts with ?. It names a path into the state, optionally followed by a bracketed tail of read directives, and it returns the matching part of the state as TERSL. A query outside a declaration never changes anything. This page covers path expressions, what a result contains, and each read directive, with the answer each sample returns. After reading it you can retrieve exactly the part of a store you need and tell from the answer what was left out.

Every sample on this page runs against this state:

tersl
# System(online)
## Agents
Accounting
Coding(prefers @System.Models.Astra)
## Models
Astra(Astra_v2_5; cost_M_usd: 1.5)
Zemma 7(Zem7; cost_M_usd: 3.0)
# Runs(by id; in-progress)
## 5783(date: 2026-12-23; completed)
### Result
"The primary answer was 42"

Paths#

A path is a sequence of names separated by dots, from the top level down. Naming a node returns that node and its whole subtree. A bare ? returns the whole state.

tersl
? System.Agents
tersl
# System(*)
## Agents
Accounting
Coding(prefers @System.Models.Astra)

The result is real state at its true depth, with every container above the match included. Those containers are addresses: they show where the match sits and whether they carry attributes, and nothing more. Each container in a result carries one of three spellings:

You see It means
## Agents The node has no attributes.
# System(*) The node has attributes, and you did not ask about this node.
# System(online) The node is one you asked about, shown with its full attribute list.

A result never contains (), because in a declaration an empty list clears every attribute.

Selecting children#

A trailing .* selects the immediate children of the path node, objects and containers alike, and the node you named becomes an address. ? System.* returns Agents and Models with their subtrees under # System(*). The difference matters when a directive follows, because directives run per match: ? System.Models.* [WHERE …] tests each model, while ? System.Models [WHERE …] tests the Models container itself, which has no such attribute and so matches nothing.

? * selects everything at the top level of the store. A star anywhere other than the end of a pure query path, as in ? Places.*.Bucket, is refused with TERSE904. An object has no children, so a trailing .* after an object is a legal, empty answer.

What an answer leaves out#

When an answer omits part of a node, it says so with the same three tokens that declarations use to mean "keep the rest":

Token Position Stands for
* An object line Objects of the node above that were left out.
## * (any depth) A header at the child depth Child containers that were left out.
(*) An attribute list Attributes that exist and are not shown.

These marks appear only where something was omitted, last in their run. An answer that omitted nothing carries no marks. The containers above a match are not marked, so ? System.Agents does not mention Models; the one exception is the container directly above a matched object, whose left-out siblings are always marked.

Results also follow three further rules. Several ? lines in one request are answered as one union, and a node that any line reached in full is shown in full. No node appears twice. Applying a result back to the state it came from changes nothing, byte for byte.

Read directives#

Directives go in one bracketed tail at the end of the query line. Segments are separated by ; and may come in any order. Directive keywords are always in capitals.

DEPTH#

[DEPTH n] bounds the answer. The match is level 0 and is shown with its full attributes, levels 1 to n−1 are shown whole, and level n is the frontier: its headers appear, their attributes collapse to (*), and their contents are replaced by marks.

tersl
? Runs [DEPTH 1]
tersl
# Runs(by id; in-progress)
## 5783(*)
### *

5783 carries attributes and has a child container, so both are marked. ? [DEPTH 1] gives an overview of the whole store, and [DEPTH 0] asks only whether a node exists and what it carries.

WHERE#

[WHERE expr] keeps the matches whose attributes satisfy an expression. Its.name reads an identifier, Its["max hp"] reads one whose name contains a space, and Its HAS name tests whether a flag or identifier with that name exists. Comparisons are ==, !=, <, <=, > and >=, combined with AND, OR, NOT and parentheses. A text literal is quoted.

tersl
? System.Models.* [WHERE Its.cost_M_usd > 2]
tersl
# System(*)
## Models
Zemma 7(Zem7; cost_M_usd: 3.0)
*

The * under Zemma 7 tells you Models has other objects that did not match. Integers and floats compare as numbers, so > 2 matches 3.0. A missing attribute or a type mismatch makes a comparison false, with no error, and a quoted literal matches a bareword or a string with the same text. A predicate that cannot be compiled matches nothing and raises the warning TERSE931. Bare Its is an identity test: [WHERE Its == "Kell"] matches the node named Kell.

CONTAINS#

[CONTAINS "text"] is a case-insensitive substring search. The match and every node under it are tested against their own line and their own TEXT, and a container does not match because one of its descendants does. A matching container is returned without its subtree.

tersl
? Runs [CONTAINS "answer"]
tersl
# Runs(*)
## 5783(*)
### Result
"The primary answer was 42"

The argument is exactly one quoted string. Several CONTAINS segments in one tail must all match.

WHEN#

[WHEN spec] filters matches by their date attributes. The specification accepts a year, a month and year, MM/DD/YYYY, and ISO dates and date-times. ? Runs.* [WHEN 2026-12-23] returns run 5783 with its subtree. A [WHERE] predicate can also compare dates directly, as in [WHERE Its.date >= 2026-12-01].

FIRST n and LAST n#

[FIRST n] and [LAST n] keep the first or last n matches in state order, after any filters. The result is still emitted in state order.

tersl
? System.Models.* [FIRST 1]
tersl
# System(*)
## Models
Astra(Astra_v2_5; cost_M_usd: 1.5)
*

When a match set mixes kinds, objects count before containers. n must be a positive integer, and a tail may carry only one slice. Without a number, [FIRST] and [LAST] are placement directives for declarations, and on a pure query they are an error (TERSE_QUERY_ERR).

CONTAINERS and OBJECTS#

[CONTAINERS] leaves out object runs and shows only the container structure. [OBJECTS] is a pruning view: it drops every subtree that holds no objects, so the containers that remain show where the objects are. The two cannot be combined, and neither marks what it dropped.

tersl
? System [CONTAINERS]
tersl
# System(online)
## Agents
## Models

Composing a query#

Directives on different axes combine in one tail: [WHERE] selects, [DEPTH n] bounds, and the kind directives choose which runs to show.

tersl
? System.Models.* [WHERE Its.cost_M_usd > 1; FIRST 1]
? Runs.* [CONTAINS "answer"; DEPTH 2]

A line may carry only one tail; a second bracket group is refused with TERSE923. To ask several questions at once, write several ? lines. Their answers are merged into one document:

tersl
? System [DEPTH 1]
? System.Models
tersl
# System(online)
## Agents
*
## Models
Astra(Astra_v2_5; cost_M_usd: 1.5)
Zemma 7(Zem7; cost_M_usd: 3.0)

Agents is at the first line's frontier and is marked; Models was reached in full by the second line, so it is shown whole and unmarked.

Queries do not write#

A query outside a declaration is read-only. Mutation directives such as [MOVED] or [APPEND] on such a line are an error, and a capitalised keyword written after the path without brackets, as in ? T.probe MOVED, is TERSE036. The same ? syntax inside a declaration selects state to copy or move; Writing with declarations describes that use.