Skip to main content
After indexing, smolbren builds a directed property graph from your vault: note types (the type frontmatter key) become node labels, and frontmatter keys that contain wikilinks become relationship types. The node ID is the note’s vault-relative path without the .md extension — for example, blogs/context-engineering. You query this graph with Cypher, the same query language used by Neo4j and other graph databases. Every smolbren query call prints a single line of JSON to stdout:
{"columns":["b.id","n.title"],"rows":[{"b.id":"blogs/context-engineering","n.title":"Context engineering"}]}

Discover your schema first

Before writing queries, inspect what node labels and relationship types exist in your vault. List node types and counts:
smolbren types
[
  {"type":"blog","count":3},
  {"type":"journal","count":2},
  {"type":"org","count":1},
  {"type":"project","count":1},
  {"type":"repo","count":1}
]
List relationship types and counts:
smolbren edges
[
  {"edge_type":"about","count":1},
  {"edge_type":"derives_from","count":3},
  {"edge_type":"for","count":3},
  {"edge_type":"mentions","count":6},
  {"edge_type":"merged_from","count":2}
]
These names are what you put inside (:NodeLabel) and -[:EDGE_TYPE]-> in your Cypher queries.

Basic patterns

Match every note with a given node label and return their IDs and titles.
smolbren query "MATCH (n:blog) RETURN n.id, n.title"
{
  "columns": ["n.id", "n.title"],
  "rows": [
    {"n.id": "blogs/context-engineering",           "n.title": "Context engineering"},
    {"n.id": "blogs/context-development-lifecycle", "n.title": "Context development lifecycle"},
    {"n.id": "blogs/context-platform-engineering",  "n.title": "Context platform engineering"}
  ]
}
Follow a specific edge type from a source label to any target. This returns every blog note and the notes it mentions:
smolbren query "MATCH (b:blog)-[:mentions]->(r) RETURN b.title, r.title"
{
  "columns": ["b.title", "r.title"],
  "rows": [
    {"b.title": "Context engineering", "r.title": "Prism"},
    {"b.title": "Context engineering", "r.title": "smolbren"},
    {"b.title": "Context engineering", "r.title": "Context development lifecycle"},
    {"b.title": "Context engineering", "r.title": "Context platform engineering"}
  ]
}
Use a $-prefixed parameter to target a single note by ID. Pass the parameter with --param:
smolbren query \
  "MATCH (n)-[r]->(m) WHERE n.id = \$id RETURN type(r), m.id, m.title" \
  --param id=blogs/context-engineering
{
  "columns": ["type(r)", "m.id", "m.title"],
  "rows": [
    {"type(r)": "for",          "m.id": "orgs/junaid-foo",                    "m.title": "Junaid Foo Org"},
    {"type(r)": "mentions",     "m.id": "projects/prism",                     "m.title": "Prism"},
    {"type(r)": "mentions",     "m.id": "repos/smolbren",                     "m.title": "smolbren"},
    {"type(r)": "mentions",     "m.id": "blogs/context-development-lifecycle", "m.title": "Context development lifecycle"},
    {"type(r)": "mentions",     "m.id": "blogs/context-platform-engineering",  "m.title": "Context platform engineering"},
    {"type(r)": "merged_from",  "m.id": "blogs/context-development-lifecycle", "m.title": "Context development lifecycle"},
    {"type(r)": "merged_from",  "m.id": "blogs/context-platform-engineering",  "m.title": "Context platform engineering"},
    {"type(r)": "derives_from", "m.id": "Journal/2026, June 01",               "m.title": null},
    {"type(r)": "derives_from", "m.id": "Journal/2026, June 04",               "m.title": null}
  ]
}
Aggregate to find the most-mentioned notes across all blogs:
smolbren query \
  "MATCH (n:blog)-[r:mentions]->(m) RETURN m.id, m.title, count(r) AS mentions ORDER BY mentions DESC"
{
  "columns": ["m.id", "m.title", "mentions"],
  "rows": [
    {"m.id": "projects/prism",                        "m.title": "Prism",                        "mentions": 3},
    {"m.id": "repos/smolbren",                        "m.title": "smolbren",                     "mentions": 2},
    {"m.id": "blogs/context-development-lifecycle",   "m.title": "Context development lifecycle", "mentions": 1}
  ]
}
Note is a catch-all label that matches every note in the vault, regardless of its type value. Use it to query across multiple types or when you don’t know the exact label:
smolbren query \
  "MATCH (n:Note) WHERE n.type IN ['blog','journal'] RETURN n.id, n.title, n.type"
{
  "columns": ["n.id", "n.title", "n.type"],
  "rows": [
    {"n.id": "blogs/context-engineering",           "n.title": "Context engineering",           "n.type": "blog"},
    {"n.id": "blogs/context-development-lifecycle", "n.title": "Context development lifecycle",  "n.type": "blog"},
    {"n.id": "blogs/context-platform-engineering",  "n.title": "Context platform engineering",   "n.type": "blog"},
    {"n.id": "Journal/2026, June 01",               "n.title": null,                             "n.type": "journal"},
    {"n.id": "Journal/2026, June 04",               "n.title": null,                             "n.type": "journal"}
  ]
}
Traverse from one specific label to any note (using Note) along a known relationship type. This returns every blog that was merged from another blog:
smolbren query \
  "MATCH (b:blog)-[:merged_from]->(x:Note) RETURN b.id, x.id"
{
  "columns": ["b.id", "x.id"],
  "rows": [
    {"b.id": "blogs/context-engineering", "x.id": "blogs/context-development-lifecycle"},
    {"b.id": "blogs/context-engineering", "x.id": "blogs/context-platform-engineering"}
  ]
}

Using parameters

Pass runtime values into queries with --param key=value. The flag is repeatable:
smolbren query \
  "MATCH (n:blog)-[r:mentions]->(m) RETURN m.id, count(r) AS c ORDER BY c DESC LIMIT \$limit" \
  --param limit=5
smolbren JSON-parses the value first, so:
--param valueCypher type
--param limit=10integer 10
--param active=trueboolean true
--param id=blogs/context-engineeringstring "blogs/context-engineering"
--param tags=["a","b"]JSON array
If JSON parsing fails, the value is passed as a plain string. This means you almost never need to add quotes — just write --param id=blogs/context-engineering, not --param id='"blogs/context-engineering"'.

Output format

Every smolbren query result has the same shape:
{
  "columns": ["b.id", "n.title", "mentions"],
  "rows": [
    {"b.id": "blogs/context-engineering",          "n.title": "Context engineering",          "mentions": 4},
    {"b.id": "blogs/context-platform-engineering", "n.title": "Context platform engineering", "mentions": 2}
  ]
}
  • columns — ordered array of projection names, matching the aliases or expressions in your RETURN clause.
  • rows — array of row objects. Each object’s keys are the column names from columns, and values are the corresponding projected values.
To reshape this in jq:
smolbren query "MATCH (n:blog) RETURN n.id, n.title" \
  | jq '.rows[] | {id: .["n.id"], title: .["n.title"]}'
{"id": "blogs/context-engineering", "title": "Context engineering"}
{"id": "blogs/context-development-lifecycle", "title": "Context development lifecycle"}
{"id": "blogs/context-platform-engineering", "title": "Context platform engineering"}
Note is a catch-all label that matches every note in the vault — typed or not. Use (n:Note) when you want to reach nodes of any type or when the target label of a relationship is unknown. It is always registered automatically, even if no note has type: Note in its frontmatter.
Body text and raw frontmatter JSON are not projected in query results. The graph engine loads only id, path, type, and title per node for performance. If you need the body or arbitrary frontmatter scalars like status, use smolbren get <id> (or smolbren get <id> --body) to fetch the full note record after your query identifies the relevant IDs.