Docs · Querying · Cheat sheet

Query cheat sheet

What a query can use without declaring anything: variables of the note a block sits in, parameters the host binds, and the properties every node and edge gets automatically.

Note variables {{…}}

In a graph-query block in Obsidian, {{name}} is replaced by a value of the note holding the block, written as a Cypher literal, before the query is parsed. Values below are for Projects/Atlas/Plan.md with this frontmatter:

---
type: Project
title: Atlas plan
status: active
budget: 1200
draft: false
tags: [graph, ml]
address: {city: Lviv}
---
VariableBecomesNotes
{{path}}"Projects/Atlas/Plan.md"Vault-relative path. The safest way to match this note: (me {path: {{path}}}).
{{name}}"Plan.md"File name with extension.
{{title}}"Plan"File name without extension. It ignores frontmatter title; use {{fm.title}} for that.
{{folder}}"Projects/Atlas""" for a note at the vault root.
{{extension}}"md""adoc" for an AsciiDoc note.
{{fm.status}}"active"Any frontmatter key. {{frontmatter.status}} is the long form.
{{fm.title}}"Atlas plan"
{{fm.budget}}1200Numbers and booleans are written plain.
{{fm.draft}}false
{{fm.tags}}["graph", "ml"]A list of strings, numbers or booleans becomes a list. A nested list inside it becomes null.
{{fm.address.city}}"Lviv"Dots walk into maps.
{{fm.address}}nullA map is not a literal, so it becomes null.
{{fm.owner}}nullA missing key becomes null; so does any unknown name.
  • Strings are quoted and escaped, so a value with quotes cannot break or extend the query.
  • Spaces inside the braces are allowed: {{ fm.status }}.
  • Only names and dots are substituted. Nothing is evaluated: {{fm.budget * 2}} stays in the text and fails to parse. Write {{fm.budget}} * 2 instead.
  • Line numbers are kept, so error positions still point into the block.
  • Substitution happens before the backend is chosen, so variables work with backend: ladybug too.
  • Blocks re-run when the note changes, so a block that uses {{fm.*}} follows edits to its own frontmatter.

Parameters $…

A $name is a real Cypher parameter, bound by the host at run time.

ParameterWhereValue
$activeFileGraph view (full pane)Vault-relative path of the last active Markdown note. It keeps that path when focus moves away from Markdown, and is null before any note has been active.
any $nameSidecar POST /query and the MCP tool cypher_queryWhatever you pass in params.

graph-query blocks and tg cypher bind no parameters. A block that uses $x fails with Missing parameter $x, so use a {{…}} variable there.

curl -s localhost:8765/query \
  -H "Authorization: Bearer $OBSIGRAPH_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"query": "MATCH (n {path: $p})-[r]->(x) RETURN type(r), x.title",
       "params": {"p": "People/Alice.md"}}'

Node properties

Every note node in a vault (Obsidian plugin, sidecar) has these without any schema:

UseValue
n.pathVault-relative path, such as "People/Alice.md". null for a stub.
n.titleFrontmatter title, else an AsciiDoc = Title, else the file name without extension. A stub's title is its link text.
n.stubtrue for a link target with no note behind it.
n.<key>Every frontmatter key as written: n.type, n.tags, n.aliases, n.status, generated ids such as n.uuid.
id(n)The note path; for a stub, its link text.
labels(n)From frontmatter type and types, or the folder and tag fallbacks in settings.
n:PersonLabel test, usable in WHERE.

path and title are always set by Typed Graph, and a frontmatter path key is replaced. Headings and blocks are not nodes.

Edge properties

Written as -distrusts:: [[Mallory]] {since: 2021, id: d-1}:

UseValue
type(r)The edge type without its sign: "distrusts".
r.sign1, or -1 for a type written with a leading -.
r.idThe pinned {id: …}, an edge note's path, or a derived <source>#<type>#<target>#<n>.
r.<key>Each {key: value} property, or an edge note's frontmatter: r.since.
[:knows|likes]Type alternation in a pattern.

With Plain links as links_to edges on, every prose link is also an untyped links_to edge with sign 1. The heading and line an edge was written under are not properties.

Functions over metadata

FunctionReturns
keys(n), keys(r)Property names, including path and title.
properties(n)A map of all properties.
startNode(r), endNode(r)The endpoints of an edge.
labels(n), type(r), id(x)Labels, edge type, node or edge id.
coalesce(n.subtitle, n.title)First non-null value.
x IN n.tagsList membership, for tags or aliases.

The first three are built-in engine only; see Ladybug backend.

tg cypher graph (code projects)

tg cypher and the tg_cypher MCP tool query a graph of lat.md/ sections, spec requirements and, with --code annotated|all, source code. Every node also has path (its id) and title, plus type.

LabelPropertiesId looks like
Section, plus the file's type: labels for sections below the titlesection, title (the heading), file, depth, startLine, endLine, summary (first paragraph)lat.md/cli#CLI#Requirement trace
Requirement, or Decision for an ADR, plus the framework's own label (UserStory, ADR, …)requirement, framework, capability, name, text, file, line, status, change, removedByopenspec:auth#Login, adr:0003
Scenario (spec)scenario, requirement, name, file, lineopenspec:auth#Login#Valid password
CodeFilelang, lines (line count)src/auth/token.ts
CodeSymbolname, kind, symbol, lang, lines ("12-40"), startLine, endLine, signaturesrc/auth/token.ts#refresh
  • Edges: contains (parent section to child, requirement to scenario), references (plain wiki links and openspec:/specs: frontmatter), and every typed edge line with its {properties}.
  • @tg: edges from code carry what you wrote, such as r.since. When the comment sits above a test call they also carry r.test, the test's name.
  • Test-spec sections in a file typed Scenario are labelled Scenario too. Use s.scenario IS NOT NULL to keep only spec scenarios.

Ladybug backend

With backend: ladybug the names are the same and the same text runs. The differences:

  • keys, properties, startNode and endNode are not available.
  • n.title, n.stub, r.id and r.sign are stored columns. Other properties get a typed column per kind, so one property holding numbers in some notes and text in others is read as one kind, with a notice.
  • UNWIND, CASE and other read Cypher outside the built-in subset run there.

Recipes

This note's neighbourhood

```graph-query
MATCH (me {path: {{path}}})-[r]-(x)
RETURN me, r, x
```

Typed backlinks as a table

```graph-query
view: table

MATCH (x)-[r]->(me {path: {{path}}})
RETURN x.title AS from, type(r) AS edge,
       r.sign AS sign
```

Other notes in this folder

```graph-query
MATCH (n)
WHERE n.path STARTS WITH {{folder}} + '/'
  AND n.path <> {{path}}
RETURN n.title ORDER BY n.title
```

For a note at the vault root, {{folder}} is "", so this matches nothing.

Same status as this note

```graph-query
MATCH (p:Project)
WHERE p.status = {{fm.status}}
  AND p.path <> {{path}}
RETURN p.title, p.budget
```

Notes with a tag

MATCH (n) WHERE 'ml' IN n.tags
RETURN n.title

Negative edges

MATCH (a)-[r]->(b) WHERE r.sign = -1
RETURN a.title, type(r), b.title

Links to missing notes

MATCH (a)-[r]->(s) WHERE s.stub
RETURN a.path, type(r), s.title

Graph view: follow the open note

MATCH p = (n {path: $activeFile})
          -[:knows*1..2]->(y)
RETURN p

Sections of one doc file

tg cypher "MATCH (s:Section)
  WHERE s.file = 'lat.md/cli.md'
  RETURN s.title, s.startLine"

Unimplemented requirements

tg cypher "MATCH (q:Requirement)
  OPTIONAL MATCH (c)-[i:implements]->(q)
  WITH q, count(i) AS n WHERE n = 0
  RETURN q.requirement, q.status"