# Query syntax

One query language searches everything HarnessLink keeps for you: the **Query** page (Dashboard → Query, `g q`) searches history, memories, runs and session transcripts at once, and the search box of **History**, **Archive**, **Memory** and **Runs** takes the same syntax for that page alone. Plain words keep working: `auth refactor` finds both words.

```text
auth refactor                         both words (full text)
"rate limit"                          the exact phrase
harness:codex                         a field equals a value
workspace:*superpi*                   wildcards: * any run, ? one character
cwd:/\/Workspace\/(api|web)/i         a regex on a field
/fix(ed)? the (cache|queue)/i         a regex on the main text
tokens:>10000 cost:>=1.5              numeric comparisons
created:2026-10-01..2026-10-26        a range (numbers or dates)
after:-7d before:2026-10-20           time bounds, relative or absolute
in:history in:runs                    sources (Query page only)
has:session -has:cwd                  the field is set / not set
-harness:omp NOT "draft"              exclusion
(harness:codex OR harness:omp) AND /timeout/i
```

## Combining terms

Terms side by side must all match (an implicit `AND`). `OR` matches either side; `NOT` or a leading `-` excludes. Parentheses group.

Precedence, tightest first: parentheses, `NOT`, `AND`, `OR`. So `harness:omp OR harness:codex timeout` means "omp, or codex with timeout", while `(harness:omp OR harness:codex) timeout` means "timeout in omp or codex". `NOT harness:omp timeout` means "timeout, not in omp".

`AND`, `OR` and `NOT` are keywords only in capitals: `this or that` searches for three words.

## Text

A bare word matches the page's main text: the prompt on History and Archive, the memory's body on Memory, the run id and session titles on Runs, the session title for transcripts on Query. History and memories use the full-text index: a word matches whole words (`fix` does not match `fixed`), case-insensitively. Add `*` for a prefix: `fix*` matches `fix`, `fixed`, `fixes`. Runs and transcripts match words as case-insensitive substrings.

`"…"` matches a phrase, in order. Inside quotes a backslash makes the next character literal (`\"` is a quote); nothing else is special.

## Fields

`field:value` compares one field. The value may be quoted to include spaces (`harness:"Claude Code"`). Field names are a fixed list per page; an unknown one is an error that points at it.

| Page | Fields |
| --- | --- |
| History, Archive | `workspace` `harness` `device` `session` `cwd` `prompt` `created` |
| Memory | `project` `harness` `type` `device` `temp` `title` `body` `path` `updated` |
| Runs | `harness` `phase` `roster` `workspace` `run` `tokens` `cost` `sessions` `updated` |
| Query | all of the above, plus `in:`. Transcripts have `workspace` `harness` `title` `session` `updated` |

Every page also accepts `has:`, `before:` and `after:`.

How a value matches depends on the field:

- **Labels** (`harness`, `phase`, `roster`, `type`, `workspace`, `project`,
  `device`): equal, ignoring case. Names work as well as ids: `harness:"Claude Code"`, `type:"Work state"`, `workspace:api` (the label) or `workspace:git:github.com/acme/api` (the id), `project:` the project's friendly name or its folder slug, `device:` a hostname or device id.
- **Ids** (`session`, `run`): exactly equal, case-sensitive.
- **Text** (`prompt`, `cwd`, `title`, `body`, `path`): contains the value,
  ignoring case. `prompt:Timeout` matches "fix the timeout".
- **Numbers** (`tokens`, `cost`, `sessions`): see below.
- **Times** (`created`, `updated`): see below.
- **`temp`**: `temp:true` or `temp:false` (also `1`/`0`, `yes`/`no`).

On Runs, a run matches a session field when **any** of its sessions does (`harness:codex`), and `-harness:codex` keeps runs where **none** does. `tokens` (input + output + cache), `cost`, `sessions` (count) and `updated` (last activity) are the run's totals.

### Wildcards

`*` matches any run of characters and `?` exactly one, across the whole value: `workspace:*superpi*`, `cwd:/src/*`, `session:s?`. On text fields without a wildcard the value is a substring already. `\*` and `\?` are a literal star and question mark.

## Numbers, times and ranges

Number and time fields take `N`, `>N`, `>=N`, `<N`, `<=N` or a range `a..b` (both ends included; either end may be left open: `tokens:10000..`, `cost:..0.5`).

A time is one of:

- a date, `2026-10-01` (a whole UTC day), or a month, `2026-10`;
- a date and time in UTC, `2026-10-01T09:30` (that minute) or
  `2026-10-01T09:30:15`;
- unix seconds, `1790812800`;
- relative to now: `-30m`, `-2h`, `-7d`, `-2w`, `-3mo` (30-day months),
  `-1y`; also `-45s`.

A time covers its whole span: `created:2026-10-01` is that day, `created:>2026-10-01` starts the next day, `created:<=2026-10-01` includes it, and `created:2026-10-01..2026-10-26` runs through the end of the 26th. A relative time on its own means "since then": `created:-7d` is the last seven days.

`after:X` keeps rows at or after the start of X, `before:X` rows before the start of X, on the page's time: when a prompt was typed (History, Archive), when a memory file or transcript last changed, a run's last activity. They work beside the date-range picker, which narrows further.

## has:

`has:field` keeps rows where the field is set and not empty (`has:session`, `has:cwd`); `-has:field` keeps rows where it is missing. For numbers it means "not zero" (`has:cost`).

## Regular expressions

`/pattern/flags` is a JavaScript regular expression, on the main text when bare, on a field after `field:`. Flags: `i` (ignore case), `m`, `s`, `u`.

- A word that starts with `/` is a regex only when it ends in `/` plus
  valid flags, so `cwd:/src/api` is still a path. Quote a value to be sure: `cwd:"/tmp/i"`.
- Patterns are at most 256 characters. Backreferences (`\1`) and nested
  quantifiers that can take exponential time, like `(a+)+`, `(.*)*` or `(\w+\s?)*`, are refused with an error.
- The database cannot run regular expressions, so HarnessLink narrows the
  candidates with everything else in the query (and with the literal text a pattern must contain), then tests the rest, newest first. It tests at most 20,000 rows per search (or about 200 ms); past that the page says **Scanned the newest N rows** and the counts, fields and chart cover those rows only. Add a field or a date range to search further back.
- Sorting by relevance does not apply to regex searches; they list newest
  first.

## Query page sources

On the Query page, `in:history`, `in:memories`, `in:runs` and `in:sessions` pick sources; several `in:` terms combine like any other term, so `in:history OR in:runs` searches both. The **Source** filter does the same. A field that one source does not have is simply false there: `tokens:>1000` finds only runs, and `-tokens:>1000` keeps everything else. Memories have no workspace, and runs and transcripts no device.

Results from every source are listed together, newest first, or by relevance when the query has words: each source ranks its own matches and the list interleaves them, so the first page shows the best of each.

## Errors

A query that cannot be read is underlined where the problem is, with a message such as `Unknown field "harnes". Fields here: …`, `OR needs a term after it`, `Unclosed parenthesis` or `created: needs a date …`. Fix that part and search again.

## Limits

A query is at most 2,000 characters, 32 terms and 24 levels of nesting. A query that needs too many separate comparisons is refused as too complex.
