Hierarchical Scope (Contexts as Notes)
Notes usually belong to both a broad life domain (career, software-dev, personal) and a specific context within it (Builder, Vercel, PKM). The naive way to record both is two fields:
# A task — the redundant wayscope: career # life domaincontext: "[[Builder]]" # specific projectThis is double-entry maintenance. The domain is derivable from the context — Builder is a career project — so scope repeats information the context already implies. Move PKM from software-dev to personal and you have to fix the PKM note and bulk-update every note that hard-coded scope: software-dev.
Bowerbird’s answer needs no new field type. It reuses three things you already have: entity notes with a parent relation, the under operator (see Targeting Model), and relation fields.
The pattern
Section titled “The pattern”Model contexts and domains as real entity notes in a parent hierarchy. A domain is just a root node; a project is a child; a sub-project is a grandchild.
# Contexts/career.md → a root domaintype: context
# Contexts/Builder.md → a project under careertype: contextparent: "[[career]]"
# Contexts/Vercel.md → a sub-project under Buildertype: contextparent: "[[Builder]]"A note then carries only the leaf context — never a separate scope:
# A tasktype: taskstatus: activecontext: "[[Vercel]]" # domain (career) is derivable by walking upSchema
Section titled “Schema”A context type is an ordinary entity with a self-referential parent relation. Tasks get a context relation pointing at it.
{ "types": { "entity": { "output_dir": "Entities", "fields": { "type": { "value": "entity" } }, "field_order": ["type"] }, "context": { "extends": "entity", "output_dir": "Contexts", "recursive": true, "fields": { "type": { "value": "context" }, "parent": { "prompt": "relation", "source": "context" }, "aliases": { "prompt": "list", "alias": true, "list_format": "yaml-array", "default": [] } }, "field_order": ["type", "parent", "aliases"] }, "task": { "output_dir": "Tasks", "fields": { "type": { "value": "task" }, "status": { "prompt": "select", "options": ["backlog", "active", "done"], "default": "backlog", "required": true }, "context": { "prompt": "relation", "source": "context" } }, "field_order": ["type", "status", "context"] } }}Relation storage uses the vault-wide config.link_format (wikilink by
default); field-level format is not part of the version 2 field schema.
Querying at any altitude
Section titled “Querying at any altitude”The leaf is exact; the domain is a subtree walk. Use the under operator (see Targeting Model), which dereferences the context relation and walks the target’s ancestor chain.
# Exact leaf context onlybwrb list --type task --where "context == '[[Vercel]]'"
# Everything in the Builder project (Builder + Vercel + anything deeper)bwrb list --type task --where "under(context, '[[Builder]]')"
# The ENTIRE career domain, at any altitudebwrb list --type task --where "under(context, '[[career]]')"under is inclusive of the direct target: under(context, '[[career]]') matches a note tagged [[career]] directly as well as any descendant. This is exactly what lets you collapse two fields into one — you can still ask “everything in the career domain” without ever storing the domain on the note.
See the hierarchy as a tree
Section titled “See the hierarchy as a tree”Querying tells you which notes are in a domain; rendering the tree tells you how the domain is shaped. Because contexts are real notes in a parent hierarchy, bwrb list --output tree draws that hierarchy directly:
bwrb list --type context --output tree└── career └── Builder └── VercelThis is the fastest way to see — or onboard someone else to — the shape of your context tree. --output tree builds the parent hierarchy whenever the matched notes carry parent links, so it works for any entity type modelling a hierarchy this way, not only types marked recursive. (When the result set has no parent links, --output tree falls back to grouping notes by directory.)
Limit the depth with -L/--depth, and order siblings with --sort/--desc:
# Just the top two levels (domains and their immediate projects)bwrb list --type context --output tree -L 2
# Order siblings by name, descendingbwrb list --type context --output tree --sort name --descYou can also narrow the tree to a single subtree first, then render it:
bwrb list --type context --where "isDescendantOf('[[career]]')" --output treeSee bwrb list for the full --output tree reference.
Why one field beats two
Section titled “Why one field beats two”Collapsing scope + context into a single context tree removes the redundancy and unlocks transitive queries:
- No double entry. A note records one fact (its leaf context). The domain is computed, never stored.
- Reorganize in one place. Re-parent a context note (
PKM.parentfrom[[software-dev]]to[[personal]]) and every dependent query updates automatically — no bulk rewrite of notes. - Transitive queries. “Everything in the career domain” is a single
under(context, '[[career]]'), not an OR over every leaf. - Contexts are first-class notes. Because they’re real notes, they get
unlinked-mentionaudit coverage, backlinks, and graph presence for free, and aliases (see Schema) help with all of those. Rich contexts (Builder, with its own content) and label-like contexts (PKM) cost the same.
- Validation for free. A task pointing at a non-existent context is flagged by the existing relation-source audit (see Validation and Audit), exactly like any other broken relation.
Migration: collapse a redundant scope field
Section titled “Migration: collapse a redundant scope field”If your vault already has the redundant scope select alongside a context relation, migrate in three steps. Nothing is lost, because the domain is derivable from the context tree.
1. Make sure your contexts form a tree. Each context note’s parent should point at its domain (or intermediate project):
# Contexts/Builder.mdtype: contextparent: "[[career]]"2. Confirm the domain is already derivable before deleting anything. Pick a domain and verify the right notes come back through the tree alone:
bwrb list --type task --where "under(context, '[[career]]')"3. Drop the now-redundant field with bulk. Preview first (dry-run is the default), then execute:
# Preview — which notes still carry the redundant scope?bwrb bulk --type task --where "!isEmpty(scope)" --delete scope
# Applybwrb bulk --type task --where "!isEmpty(scope)" --delete scope --executeFinally, remove the scope field from your schema and run a schema migration (see Migrations) so the field is no longer declared:
bwrb schema diffbwrb schema migrate --executeAfter this, every note carries only its leaf context, and the life domain is a query away at any altitude.
See Also
Section titled “See Also”- Targeting Model — the
underoperator and hierarchy functions - bwrb list —
--wherequeries and--output tree - bwrb bulk —
--deletefor dropping fields - Migrations — evolving the schema once
scopeis gone - Validation and Audit — relation-source validation for context notes