Query Language

Query Language

<p><strong>Experimental.</strong> The query language is under active development. Syntax, operators, defaults, and CLI flag names may change without warning. It is <strong>not exposed as a public API</strong> — there is no library entry point, no MCP tool that accepts a raw filter document, and no stable on-disk format. The only supported surface today is the CLI subcommands listed below; build automation against them at your own risk.</p>

IWE has a YAML-based, MongoDB-style query language for selecting, shaping, and mutating documents in a workspace by their frontmatter and graph relationships. Today it is reachable only through the CLI subcommands iwe find, iwe count, iwe update, and iwe delete (plus the read-only selectors on iwe retrieve, iwe tree, and iwe export). The MCP server still exposes the legacy structural-selector parameters and does not accept query-language filter documents.

This page is a quick reference. The formal source of truth lives under docs/specs/:

  • docs/specs/query-language-spec.md — operations, filter, projection, sort, limit, update operators
  • docs/specs/query-graph-spec.md$key, $includes, $includedBy, $references, $referencedBy
  • docs/specs/query-cli-spec.md — flag set, lowering rules, deprecation table
  • docs/specs/query-language-grammar.md — BNF grammar

Operations

OperationCLI subcommandWhat it does
findiwe findReturns matched documents (subject to projection).
countiwe countReturns the integer count of matched documents.
updateiwe updateMutates frontmatter on each matched document.
deleteiwe deleteRemoves each matched document and cleans up references.

update and delete require an explicit filter — passing {} on purpose is the only way to operate on the whole corpus.

Filter syntax

A filter document is YAML. A document matches when every top-level key matches; multiple top-level keys are AND-composed.

Bare equality

status: draft

Matches documents whose status field equals draft. For arrays, a bare scalar tests membership: tags: rust matches when rust is in the tags array. Cross-type comparisons are always false (no implicit coercion: priority: "3" does not match an integer field).

Operator expressions

A mapping with $-prefixed keys is an operator expression:

priority: { $gt: 3 }
priority: { $gte: 3, $lte: 7 }       # closed range [3, 7]
status:   { $in: [draft, review] }
status:   { $nin: [archived, deleted] }
reviewed: { $exists: true }
tags:     { $all: [rust, async] }
tags:     { $size: 0 }

Operators in one expression are ANDed together. User frontmatter fields cannot start with $, so an operator and a field name never collide.

Logical composition

$and:
  - status: draft
  - priority: { $gt: 3 }

$or:
  - status: draft
  - status: review

$not:
  status: archived

$nor:
  - status: archived
  - status: deleted

Top-level AND is implicit. Use explicit $and when you need the same field name on multiple sub-clauses (a YAML mapping cannot have duplicate keys).

Nested fields

Nested fields can be addressed via nested mapping or dotted shorthand; both forms are equivalent:

author.name: alice
author:
  name: alice

Field names that themselves contain a literal . are not addressable in v1 — the engine always splits paths on ..

Graph operators

Graph operators live alongside frontmatter predicates inside the same filter. They walk inclusion edges (block-reference inclusion links) or reference edges (inline links).

$key — identity

$key: notes/foo                                # implicit $eq
$key: { $in: [a, b, c] }                       # any of these
$key: { $nin: [drafts/scratch, drafts/temp] }  # none of these

Relational operators

OperatorReads asEdge typeWalk parameters
$includesthis doc includes an anchorinclusionmaxDepth, minDepth
$includedBythis doc is included by an anchorinclusionmaxDepth, minDepth
$referencesthis doc references an anchorreferencemaxDistance, minDistance
$referencedBythis doc is referenced by an anchorreferencemaxDistance, minDistance

Each takes either a scalar key (shorthand for direct edges) or a mapping with match and walk parameters:

# Direct edges only — scalar shorthand fixes maxDepth: 1
$includedBy: projects/alpha

# Walk inclusion edges from a single anchor, bounded
$includedBy: { match: { $key: projects/alpha }, maxDepth: 5 }

# Anchor by frontmatter predicate (every active project)
$includedBy:
  match:
    type: project
    status: active
  maxDepth: 5

# Anchor set is two named documents
$includedBy:
  match:
    $key: { $in: [projects/alpha, projects/beta] }
  maxDepth: 5

# Range bounds — descendants 2 to 5 levels under archive/index
$referencedBy: { match: { $key: archive/index }, minDistance: 2, maxDistance: 3 }

In the full mapping form, omitting maxDepth / maxDistance means unbounded — the walk reaches every transitively-related document. Walks are BFS and de-duplicate via a visited set, so cycles terminate.

A relational operator never matches a document in its own anchor set. To include the anchor, OR it in:

$or:
  - $key: projects/alpha
  - $includedBy: { match: { $key: projects/alpha }, maxDepth: 5 }

Projection (find only)

Inclusion-only in v1: list the fields you want; everything else is omitted. 1, true, and YAML null are all accepted as the include marker.

project:
  title: 1
  modified_at: 1
  author.name: 1

Sort and limit

sort:  { modified_at: -1 }   # 1 = ascending, -1 = descending
limit: 100                   # 0 = no limit

v1 accepts exactly one sort key. Ties (and the no-sort case) are broken by document key in ascending lexicographic order.

Update operators

update:
  $set:
    reviewed: true
    audited_at: 2026-04-26
    "review.reviewer": alice
  $unset:
    draft_notes: ""

$set adds the field if absent, replaces it otherwise. Mapping values replace wholesale; use dotted shorthand to write subset leaves without dropping siblings. $unset removes fields; values are ignored.

Reserved-prefix protection

Frontmatter field names whose first character is _, $, ., #, or @ are reserved by the engine. They are invisible to filters, projections, and sort, and update strips them on writeback. Targeting a reserved-prefix segment in a $set or $unset path — at any depth — is a parse-time error.

CLI lowering

On the CLI, structural anchor flags lower to graph operators. A KEY[:DEPTH] suffix sets maxDepth (or maxDistance) for that anchor; depth 0 is the unbounded sentinel.

CLI flagLowers to
-k KEY$key: KEY (1 key = $eq; 2+ = $in)
--includes KEY$includes: KEY (scalar shorthand, depth 1)
--included-by KEY:5$includedBy: { match: { $key: KEY }, maxDepth: 5 }
--references KEY:0$references: { match: { $key: KEY } } (unbounded)
--referenced-by KEY$referencedBy: KEY
--max-depth Nsession default for --includes / --included-by (default 1)
--max-distance Nsession default for --references / --referenced-by (default 1)
--filter "EXPR"inline YAML filter document
--project f1,f2project: { f1: 1, f2: 1 } (find only)
--sort field:1 / --sort field:-1sort: { field: 1 / -1 }
-l, --limit Nlimit: N
--set FIELD=VALUE$set: { FIELD: VALUE } (update only; repeatable)
--unset FIELD$unset: { FIELD: "" } (update only; repeatable)

All filter flags AND together. For OR or NOT, write the composition inside --filter:

iwe find --filter '$or: [{ status: draft }, { status: review }]'
iwe find --filter '$not: { status: archived }'

Combining -k KEY with a --filter whose top level also contains $key is a parse-time error — pick one source, or use -k a -k b for multi-key match.