Trail Query Language (TQL)¶
TQL is a powerful query language for defining Trail groups. Instead of configuring groups through the visual editor, you can write declarative queries that specify exactly which relations to traverse, how to filter results, and what to display.
Quick Example¶
group "Project Ancestors"
from up :depth 5
prune status = "archived"
where priority >= 3
when type = "project"
sort :chain, date :desc
display status, priority
This query:
- Creates a group named "Project Ancestors"
- Traverses
uprelations with no depth limit - Skips archived notes and their subtrees during traversal
- Keeps only notes with priority 3 or higher
- Shows the group only when viewing a project note
- Sorts by chain order, then by date descending
- Displays status and priority as badges
Query Structure¶
Every TQL query follows this structure:
group "Name" -- Required: group name
from <relations> -- Required: what to traverse
prune <expression> -- Optional: skip subtrees during traversal
where <expression> -- Optional: filter results after traversal
when <expression> -- Optional: show group only for matching notes
sort <keys> -- Optional: custom sort order
display <properties> -- Optional: show property badges
Clauses¶
GROUP¶
Defines the display name for the group in the Trail pane.
FROM¶
Specifies which relations to traverse and how.
from up -- traverse up, unlimited depth
from up :depth 3 -- traverse up, max 3 levels
from up, down :depth 2 -- multiple relations
from up >> @"Children" -- chain with another group
from up >> next -- chain with a relation
Depth Modifier¶
Controls how many levels to traverse:
| Syntax | Behavior |
|---|---|
| (omitted) | Follow the chain as far as it goes (default, unlimited depth) |
:depth 1 |
Direct connections only |
:depth 2 |
Direct connections + one level deeper |
:depth N |
Up to N levels |
Flatten Modifier¶
Flattens the tree structure into a flat list. Useful for sequential relations or when you want all results at the same level.
| Syntax | Behavior |
|---|---|
:flatten |
Flatten all results into a single list |
:flatten N |
Keep tree structure until depth N, then flatten |
from down :flatten -- all descendants as flat list
from down :depth 5 :flatten 2 -- tree until depth 2, then flatten
Use cases:
- Sequential relations (
next/prev) where hierarchy doesn't matter - Collecting all descendants without nested structure
- Combining with depth limits for partial flattening
Chaining Operator (>>)¶
Chain traversals sequentially. At each leaf node, continue with the next target.
from up >> @"Children" -- traverse up, then continue with Children group
from up >> next -- traverse up, then traverse next from each result
from up >> next >> same -- three-level chain
from up :depth 2 >> @"Siblings" -- traverse up (depth 2), then Siblings group
Chaining vs Parallel:
- Comma (,) = parallel: from up, down traverses both from the start
- >> = sequential: from up >> down traverses up first, then down from each result
Modifier Order
:depth and :flatten can appear in any order: :depth 5 :flatten equals :flatten :depth 5.
PRUNE¶
Filters nodes during traversal. Matching nodes and their entire subtrees are skipped.
prune status = "archived" -- skip archived notes and children
prune hasTag("private") -- skip private notes and children
Key behavior: Pruned subtrees are never traversed—this improves performance for large graphs.
WHERE¶
Filters nodes after traversal. Matching nodes are kept; others are hidden but their children remain visible.
where priority >= 3 -- keep high priority notes
where status != "archived" -- keep non-archived notes
where hasTag("active") -- keep notes with "active" tag
Key difference from PRUNE:
pruneskips entire subtrees (nodes + children)wherehides individual nodes but keeps their children visible
When a node's parent is filtered by WHERE, the UI shows ... to indicate hidden ancestry.
WHEN¶
Controls whether the entire group is visible, based on the active file's properties.
when type = "project" -- show only for project notes
when hasTag("daily") -- show only for daily notes
when file.folder = "People" -- show only in People folder
If the WHEN condition fails, the group doesn't appear in the Trail pane.
SORT¶
Specifies sort order for siblings at each tree level.
sort date :desc -- sort by date, newest first
sort :chain, date :desc -- chain order primary, date secondary
sort priority :asc, file.name -- priority first, then name
The chain Keyword¶
For sequential relations (next/prev), chain preserves the sequence order:
Sort Direction¶
| Direction | Behavior |
|---|---|
:asc |
Ascending (default) |
:desc |
Descending |
DISPLAY¶
Controls which properties appear as badges next to file names.
display status, priority -- show specific properties
display all -- show all frontmatter properties
display all, file.modified -- all frontmatter + file metadata
display all includes:
- All frontmatter properties from files
display all excludes:
- Relation-alias properties
- File metadata (add explicitly:
file.created,file.modified)
Expressions¶
TQL supports rich expressions for filtering and conditions.
Comparison Operators¶
| Operator | Meaning |
|---|---|
= |
Equals |
!= |
Not equals |
< |
Less than |
> |
Greater than |
<= |
Less than or equal |
>= |
Greater than or equal |
=? |
Null-safe equals |
!=? |
Null-safe not equals |
Null-Safe Operators¶
Standard operators return null when comparing with null values. Null-safe operators handle missing properties gracefully:
status = "active" -- excludes notes without status property
status !=? "archived" -- includes notes without status property
Logical Operators¶
priority >= 3 and status = "active"
type = "project" or type = "epic"
not hasTag("private")
!exists(archived) -- ! is alias for not
Arithmetic Operators¶
Property Access¶
Frontmatter Properties¶
Access frontmatter properties directly by name:
Nested YAML Properties¶
Use dot notation for nested YAML structures:
# Frontmatter:
# obsidian:
# icon: star
# color: blue
obsidian.icon -- returns "star"
obsidian.color -- returns "blue"
When both nested and flat keys exist (e.g., obsidian.icon: and obsidian: icon:), the nested structure takes priority.
Explicit Form ($file.properties.*)¶
For clarity or when needed, you can use the explicit form:
Quoted Property Names¶
Use quoted strings for property names with special characters, spaces, or reserved names:
$file.properties."property with spaces"
$file.properties."special!chars"
metadata."due-date" -- hyphens work as identifiers, but quotes also valid
Built-in Properties¶
File Metadata ($file.*)¶
| Property | Type | Description |
|---|---|---|
$file.name |
string | Filename without extension |
$file.path |
string | Full vault path |
$file.folder |
string | Parent folder path |
$file.created |
Date | Creation date |
$file.modified |
Date | Modification date |
$file.size |
number | File size in bytes |
$file.tags |
string[] | Array of tags |
Traversal Context ($traversal.*)¶
| Property | Type | Description |
|---|---|---|
$traversal.depth |
number | Depth from active file |
$traversal.relation |
string | Relation name that led here |
Built-in Functions¶
String Functions¶
| Function | Description |
|---|---|
contains(str, substr) |
Substring check |
startsWith(str, prefix) |
Prefix check |
endsWith(str, suffix) |
Suffix check |
length(str) |
String length |
lower(str) |
Lowercase |
upper(str) |
Uppercase |
trim(str) |
Remove whitespace |
split(str, delimiter) |
Split string into array |
matches(str, pattern) |
Regex match |
matches(str, pattern, flags) |
Regex with flags ("i", "m", "s") |
where contains(title, "Project")
where matches(file.name, "^\\d{4}-\\d{2}-\\d{2}$")
where matches(title, "todo", "i")
File Functions¶
| Function | Description |
|---|---|
inFolder(path) |
Checks if file is in folder |
hasTag(tag) |
Checks if file has tag |
hasExtension(ext) |
Checks file extension |
hasLink(target) |
Checks outgoing links |
tags() |
Returns all tags |
backlinks() |
Returns backlink paths |
outlinks() |
Returns outlink paths |
Array Functions¶
| Function | Description |
|---|---|
len(array) |
Array length |
first(array) |
First element or null |
last(array) |
Last element or null |
isEmpty(value) |
True if null, empty string, or empty array |
Existence Functions¶
| Function | Description |
|---|---|
exists(property) |
True if defined and not null |
coalesce(a, b, ...) |
First non-null value |
ifnull(value, default) |
Value if not null, else default |
Date Functions¶
| Function | Description |
|---|---|
now() |
Current timestamp |
date(str) |
Parse date from string |
year(date) |
Extract year |
month(date) |
Extract month (1-12) |
day(date) |
Extract day of month |
weekday(date) |
Day of week (0=Sunday, 6=Saturday) |
hours(date) |
Extract hours (0-23) |
minutes(date) |
Extract minutes (0-59) |
dateDiff(d1, d2, unit) |
Difference between dates |
format(date, pattern) |
Format date as string |
where weekday(due) = 1 -- due on Monday
where hours(file.modified) < 12 -- modified in morning
where dateDiff(due, today, "d") < 7 -- due within a week
where format(date, "YYYY-MM") = "2024-01"
dateDiff units: "d" (days), "w" (weeks), "m" (months), "y" (years), "h" (hours), "min" (minutes)
format patterns: YYYY, MM, DD, HH, mm, ss
Dates and Durations¶
Date Literals¶
Relative Dates¶
| Keyword | Meaning |
|---|---|
today |
Current date |
yesterday |
One day ago |
tomorrow |
One day ahead |
startOfWeek |
Start of current week |
endOfWeek |
End of current week |
Duration Arithmetic¶
| Unit | Meaning |
|---|---|
d |
Days |
w |
Weeks |
m |
Months |
y |
Years |
where date > today - 7d -- last 7 days
where date < file.created + 1m -- within 1 month of creation
where modified > startOfWeek -- modified this week
Range Expressions¶
The in Operator¶
The in operator has different behaviors based on context:
| Expression | Right Type | Behavior |
|---|---|---|
"tag" in tags |
Array | Membership check |
"sub" in title |
String | Substring check |
x in 1..10 |
Range | Range check |
Complete Examples¶
Project Hierarchy¶
group "Project Tree"
from up, down :depth 3
prune status = "archived"
where priority >=? 3 and hasTag("active")
when type = "project"
sort :chain, priority :desc
display status, priority, file.modified
Daily Notes with Context¶
group "Related Notes"
from up :depth 1, down :depth 2
where file.folder != "Archive"
when matches(file.name, "^\\d{4}-\\d{2}-\\d{2}$")
sort $file.modified :desc
display all
Recent Changes¶
group "Recent Changes"
from up, down :depth 2
where file.modified > today - 7d
sort $file.modified :desc
display file.modified, status
Family Tree¶
Syntax Reference¶
Reserved Keywords¶
group, from, prune, where, when,
sort, display, all, and, or, not, in,
true, false, null, today, yesterday, tomorrow, startOfWeek, endOfWeek
Using Reserved Words as Property Names
For properties with reserved names or special characters, use quoted strings:
$file.properties."from" or metadata."property with spaces".
String Escapes¶
| Escape | Meaning |
|---|---|
\\ |
Backslash |
\" |
Double quote |
\n |
Newline |
\t |
Tab |
Comments¶
Use // for line comments:
group "Project Tree"
// This group shows the full project hierarchy
from up
where status != "archived" // exclude archived items
sort :chain
Comments are ignored by the parser and useful for documenting complex queries.
Whitespace¶
TQL ignores whitespace and allows multiline queries:
PRUNE vs WHERE¶
Understanding when to use each:
| Aspect | PRUNE | WHERE |
|---|---|---|
| When applied | During traversal | After traversal |
| Matching nodes | Skipped entirely | Hidden from display |
| Children of matches | Also skipped | Still visible |
| Performance | Better (skips subtrees) | Normal |
| Use for | Excluding branches | Filtering leaves |
Example:
-- PRUNE: Skip archived projects and all their children
prune type = "project" and status = "archived"
-- WHERE: Hide archived items but show their non-archived children
where status !=? "archived"
Visual vs Query Editor¶
Trail offers two ways to configure groups:
Visual Editor (default):
- Point-and-click configuration
- Limited to simple queries
- Good for getting started
Query Editor (TQL):
- Full expression power
- Complex boolean logic
- Date arithmetic and ranges
- Better for power users
Switch between modes in Settings → Trail → Groups.