| estado | Completed |
|---|
Rootline exposes data through a declarative query model.
Queries return records, not rendered documents. Derived and aggregated fields are computed automatically and appear alongside frontmatter in results.
The --where flag accepts expr-lang/expr expressions. Multiple --where flags are combined with AND:
rootline query --where 'status == "published"'
rootline query --where 'tipo in ["lxc", "vm"]' --where 'estado not in ["Completed"]'The --sort flag accepts a comma-separated list of field:direction pairs. Direction defaults to asc if omitted:
rootline query --sort "prioridad:asc,impact_score:desc"
rootline query --where 'estado == "Pending"' --sort "prioridad:asc"Sort type detection per field:
- Enum fields: If the field has a
.stemschema withtype: enumandvalues: [...], sorts by index position in the values list. - Numeric fields: If both values parse as float64, sorts numerically.
- String fields: Fallback to lexicographic comparison.
Missing/nil values always sort last, regardless of direction. Sort applies after filtering and before limit.
Unlike --where, an unknown --sort field is an error, matching the treatment a bad direction already got:
$ rootline query docs/ --sort "estadoo:asc"
Error: unknown sort field "estadoo": no record carries it and no .stem in scope declares it (did you mean "estado"?)An unsortable key cannot produce a defensible result — every comparison falls through, the output is silently in scan order, and it is indistinguishable from a correct sort. The legal names are the same set --where checks against, taken from the corpus before --where narrows it, so a filter that matches nothing does not invalidate a valid sort key.
Query documents based on their link relationships using --has-inbound and --has-outbound:
# Find records with inbound links from documents matching a filter
rootline query --has-inbound 'tipo == "epic"'
# Find records with outbound links matching a filter (empty = any)
rootline query --has-outbound ''
# Restrict to specific link types
rootline query --has-inbound 'tipo == "epic"' --inbound-type 'depends'
rootline query --has-outbound 'estado == "published"' --outbound-type 'blocks'
# Combine with other filters
rootline query --where 'estado == "Pending"' --has-inbound 'true'Link-traversal predicates apply --where filters to linked records and keep records with at least one matching link. When combined with --where, both filters AND together. Predicates support both [[wiki-links]] and [markdown](links) reference styles.
Flags:
--has-inbound "<expr>"— Keep records with inbound links from records matching expr (empty = any record)--has-outbound "<expr>"— Keep records with outbound links to records matching expr (empty = any record)--inbound-type <type>— Restrict--has-inboundto links of this type (e.g.,depends,blocks)--outbound-type <type>— Restrict--has-outboundto links of this type--graph-root <path>— Set the root for edge scanning (default: the query path). The query path must lie inside the graph root.
Link styles: By default, link traversal searches wiki-link ([[target]]) styles only. Configure .stem links.styles as a replacement set, for example [wikilink, markdown], to include markdown links ([text](target)).
The --select flag produces compact output containing only specified fields:
rootline query --select path,estado
rootline query --select path,estado,links --output jsonl
rootline query --where 'estado == "Pending"' --select path,estado --output csv--select accepts a comma-separated list of field names. Supported field names include:
path— document file path- Any frontmatter field name (e.g.,
estado,prioridad,owner) - Any derived field (defined in
.stemderive:section) links— document links extracted from wiki-link references
Without --select, query results include full records with frontmatter, body, sections/body_sections, and derived; links is present only when the record has extracted links.
With --select, only specified fields are included in each row, reducing noise and making output suitable for shell pipelines.
The --output flag supports multiple formats:
| Format | Encoding | Requires --select |
Use case |
|---|---|---|---|
json (default) |
Full JSON objects in a rows array |
No | AI agents, typed languages, complete data |
table |
Human-readable columnar format | No | Terminal inspection, reports |
jsonl |
One JSON object per line | Yes | Streaming processors, log ingestion |
csv |
RFC 4180 CSV with headers | Yes | Spreadsheets, SQL import, awk/cut pipelines |
Constraint:
jsonlandcsvformats require--selectto specify which columns to output. Without--select, they produce an error.
query is the only command that implements all four formats; every other command declares json and table and rejects the rest. An unknown value such as -o sdlkfj is rejected everywhere. See Output Formats.
Examples:
# JSON (default): full records in a rows array
rootline query --select path,estado --output json
# {"version": 1, "kind": "rootline/query", "rows": [{"path": "...", "estado": "..."}, ...]}
# JSONL: one record per line, no outer wrapper
rootline query --select path,estado --output jsonl
# {"path": "...", "estado": "Pending"}
# {"path": "...", "estado": "In Progress"}
# CSV: RFC 4180 with quoted fields and escaping
rootline query --select path,estado --output csv
# path,estado
# "docs/api/auth.md","Pending"
# "docs/api/endpoints.md","In Progress"
# CSV with nil values (rendered as empty)
rootline query --select path,estado,owner --output csv
# path,estado,owner
# "docs/api/auth.md","Pending","alice"
# "docs/api/endpoints.md","In Progress",""Column order in jsonl and csv output follows the order specified in --select.
| Field | Type | Description |
|---|---|---|
frontmatter.<field> |
any | Any frontmatter key, addressed by explicit field name |
body |
string | Full document body text |
sections |
map[string]string | Map of heading → section body (requires AST extraction) |
derived.<field> |
any | Computed derived field addressed by explicit field name |
When AST extraction is enabled, sections exposes a map of heading text to section body content:
rootline query --where 'sections["## Summary"] contains "migration"'
rootline query --where 'sections["## Changelog"] != nil'
rootline query --where 'sections["## References"] contains "[[T001]]"'The heading key must match the heading text exactly, including the # prefix and any leading/trailing spaces.
Universal Filtering: The
--whereflag is not limited toquery. It is also available ontree,stats,graph, andvalidate --all. All transversal commands share the same expr-lang syntax.
Field Warnings: Unknown field names in
--whereexpressions emit warnings to stderr with fuzzy suggestions (e.g.,warning: unknown field "estdo" in where expression (did you mean "estado"?)). Queries still execute — warnings are informational only.
The warning fires on every command that accepts --where: query, stats, tree, graph and validate --all. Without it a misspelled field is indistinguishable from "no records match" — zero results, empty stderr, exit 0 — which reads as a green check in CI.
In an expression type always names the record field, never a function: the expression
language ships a type() builtin of its own, which shadowed the field and made the
documented name unusable, so it is disabled. type(x) is therefore not callable in a
filter. No other builtin is affected.
A field name is considered known when it is a query builtin (path, body, type, sections), a key any record in the scanned corpus carries, or a field the effective .stem chain declares — including derive: and aggregate: names. The union is deliberately generous: a false warning on a name that does work would teach callers to ignore the warning. On a corpus with neither records nor schema there is nothing to check against, and nothing is reported.
It stays a warning, not an error. A field absent from every record is a legal filter that yields zero matches, and pipelines depend on that exit code.
A query resolves a canonical body source through the same effective schema as validation:
notes:
type: string
source: body.section["## Notes"]A frontmatter override wins over the extracted value. An empty-present section resolves to ""; duplicate matching headings fail rather than returning one occurrence. Inherited source bindings stay stable, so a child omission inherits the parent binding and a changed binding is a schema conflict.
Standard expr-lang operators apply:
| Operator | Semantics | Example |
|---|---|---|
== |
Equals | status == "published" |
!= |
Not equals | status != "draft" |
in |
One of | tipo in ["lxc", "vm"] |
not in |
Not one of | estado not in ["draft", "obsoleto"] |
contains |
Substring match | body contains "migration" |
!= nil |
Field exists | tags != nil |
&& |
AND | tipo == "lxc" && estado == "Pending" |
|| |
OR | estado == "Pending" || estado == "Bloqueada" |
{
"version": 1,
"from": "docs/",
"where": {
"and": [
{"eq": ["frontmatter.status", "published"]},
{"exists": "frontmatter.owner"}
]
},
"limit": 50
}The JSON contract uses declarative operators (eq, ne, in, contains, exists, and) for programmatic use. The CLI --where flag uses expr-lang syntax instead.
{
"version": 1,
"kind": "rootline/query",
"meta": {
"count": 1
},
"rows": [
{
"path": "docs/api/endpoints.md",
"type": "markdown",
"frontmatter": {
"title": "Endpoints",
"status": "published"
},
"derived": {
"slug": "endpoints"
}
}
]
}Rows include derived when the effective .stem defines derive: or aggregate: fields.
{
"version": 1,
"kind": "rootline/count",
"meta": { "count": 12 },
"count": 12
}Some AI coding agents (notably Claude Code's Bash tool) pre-escape the ! character to \! in command strings before shell processing. This breaks != expressions at the expr-lang parser level with error unrecognized character U+005C '\'.
Affected operators: !=, != nil, and any expression containing !.
Workarounds (in order of preference):
-
not inoperator — Direct replacement for!=negation:# Instead of: rootline query --where 'estado != "draft"' # Use: rootline query --where 'estado not in ["draft"]'
-
inwith positive list — Enumerate accepted values:rootline query --where 'estado in ["pendiente", "parcial"]' -
JSON + jq pipeline — Query all, filter externally:
rootline query -o json | jq '.rows[] | select(.estado != "draft")'
Field existence check (!= nil): use field not in [nil] when an agent cannot send != nil safely:
rootline query --where 'tags not in [nil]'This issue is not specific to rootline — it affects any CLI tool whose query syntax uses ! when invoked through agents with this escaping behavior.