Skip to content

Targeting Model

Bowerbird uses a unified targeting model across all commands that operate on sets of notes. Once you understand the four selectors, you can use any command.

Core principle: All targeting selectors compose via AND (intersection). Each selector narrows the set of matched files.

Filter by schema type.

Terminal window
bwrb list --type task
bwrb bulk --type reflection --set reviewed=true
bwrb audit --type objective

Behavior:

  • Accepts short names (task) or full paths (objective/task)
  • Short names are unambiguous since types cannot share names
  • When specified, enables strict validation of --where fields

Positional shortcut: Type can be provided as a positional argument:

Terminal window
bwrb list task # Same as: bwrb list --type task
bwrb bulk task --set x=y # Same as: bwrb bulk --type task --set x=y

Filter by file location in the vault.

Terminal window
bwrb list --path "Ideas/"
bwrb bulk --path "Reflections/Daily Notes" --set status=reviewed
bwrb audit --path "Work/**"

Behavior:

  • Accepts directory paths or glob patterns
  • Paths are relative to vault root
  • Supports glob syntax (*, **, ?)

Filter by frontmatter field values.

Terminal window
bwrb list --where "status == 'active'"
bwrb bulk --where "priority < 3 && !isEmpty(deadline)" --set urgent=true
bwrb audit --where "isEmpty(tags)"

Behavior:

  • Supports comparison operators: ==, !=, <, >, <=, >=
  • Supports regex match operator: =~ (e.g., name =~ '^\\[ERROR\\]')
  • Supports boolean operators: &&, ||, !
  • Supports functions: isEmpty(), contains(), startsWith()
  • Multiple --where flags are ANDed together
  • Field names may include hyphens and are treated literally in --where (e.g., creation-date == '2026-01-28').
  • System fields are always available: name (falls back to filename) and id.

Type-checking behavior:

  • With --type: every field reference is validated against the type’s schema, including function arguments such as the relation field passed to under(); unknown fields and invalid select values are errors
  • Without --type: unknown fields are permissive (no unknown-field validation)
  • In all modes: invalid expression syntax and runtime expression errors are hard errors

Hierarchy functions (for parent-child note relationships):

  • isRoot() — note has no parent-like note link of any kind (for example parent, owner, or a singular relation like milestone)
  • isChildOf('[[Note]]')direct child of the specified note (one parent hop)
  • isDescendantOf('[[Note]]') — the note is transitively beneath the specified note via its parent chain (any depth)
  • under(field, '[[Note]]') — the note’s field relation points at Note or any descendant of Note
Terminal window
bwrb list --type task --where "isRoot()"
bwrb list --type task --where "isChildOf('[[Epic]]')"
bwrb list --type task --where "isDescendantOf('[[Q1 Goals]]')" --depth 2
bwrb list --type task --where "under(context, '[[career]]')"
OperatorWhat it readsDirect vs transitive
isChildOf('[[X]]')the note’s own structural parentdirect (one hop)
isDescendantOf('[[X]]')the note’s own structural parent chaintransitive (any depth)
under(field, '[[X]]')a relation field dereferenced to another note, then that target’s parent chaintransitive, inclusive of the direct target

The key split: isChildOf/isDescendantOf walk the parent field of the note itself (the same chain --output tree renders), while under follows an arbitrary relation field to another note and walks its ancestry. under is therefore the operator for relation fields such as context or milestone — those are not treated as a structural parent by isChildOf/isDescendantOf (see the isRoot note below).

isChildOf/isDescendantOf resolve the parent chain over the whole vault, not just the filtered candidate set. So a chain that climbs through a note of a different type — e.g. a task whose parent is a milestone that --type task filters out — is followed all the way to the true ancestor instead of stopping early at the filtered-out note.

isChildOf and isDescendantOf are alias-aware on both sides, just like under (see below): a note whose parent is written as an alias of the real parent (parent: "[[BuilderProject]]" where BuilderProject is an alias of Builder) still matches isChildOf('[[Builder]]') / isDescendantOf('[[Ancestor]]'), and an aliased query node (isChildOf('[[BuilderProject]]')) resolves to the canonical note. Aliases are canonicalized at every step of the walked parent chain. Ambiguous aliases (declared by more than one note) and dangling aliases are left literal, so they simply don’t match rather than guessing; cycles remain safe.

under(field, '[[Node]]') vs isDescendantOf('[[Node]]')

Section titled “under(field, '[[Node]]') vs isDescendantOf('[[Node]]')”

Both ask a hierarchy question, but they walk different chains:

  • isDescendantOf('[[Node]]') walks the filtered note’s own literal parent chain. It answers “is this note structurally beneath Node?” (for example task → milestone → objective, when those links are written as parent).
  • under(field, '[[Node]]') first dereferences a relation field on the note, then walks that target’s parent chain. It answers “does this note’s field point into the subtree rooted at Node?”

Example. Contexts are real notes in a parent hierarchy (Builder.parent = [[career]], Vercel.parent = [[Builder]]), and a task records only the leaf:

# a task
context: "[[Vercel]]"
Terminal window
# Everything in the career domain, at any altitude — Builder, Vercel, deeper.
bwrb list --type task --where "under(context, '[[career]]')"
# Exact context only.
bwrb list --type task --where "context = [[Vercel]]"

Notes:

  • Inclusive of the direct target. under(context, '[[career]]') matches a note whose context is [[career]] itself as well as any descendant of career. (under(field, '[[X]]')field = [[X]] OR field points to a descendant of X.)
  • Any relation field. The first argument is the field to dereference — under(milestone, '[[Q2]]'), under(owner, '[[Platform]]'), etc. When --type is set, the first argument is validated against the schema: an unknown field, or a field that is not a relation, is flagged up front (e.g. under() expects a relation field, but 'status' is a 'select' field) rather than silently matching nothing.
  • Multi-valued fields. If the relation holds a list, the note matches when any target is under the node.
  • Resolution scope. under resolves relation targets and their ancestors across the whole vault, so the target notes do not need to share the filtered type. When the relation field has a source constraint, bare same-name targets are resolved by that source type before walking the target’s ancestor chain; cross-type name collisions are ignored when exactly one target matches the source type. Multiple matching source-type notes remain ambiguous and should be path-qualified.
  • Alias-aware. under canonicalizes aliases (see Schema) on both sides — an aliased relation value (context: "[[BuilderProject]]" where BuilderProject is an alias of Builder) and an aliased query node (under(context, '[[BuilderProject]]')) both resolve to the canonical note, using the same resolution as bwrb list --name <alias>. An ambiguous alias (declared on more than one note) is not auto-resolved — it matches nothing rather than silently picking a winner.
  • Robustness. A dangling (unresolvable) relation target — including a dangling alias — simply doesn’t match; a malformed parent cycle is walked safely and never loops forever.

For the full pattern — modelling life domains and projects as a parent tree of context notes and collapsing a redundant scope field into it — see Hierarchical Scope (Contexts as Notes).

Filter by literal Markdown body content (full-text search via ripgrep). YAML frontmatter is excluded.

Terminal window
bwrb list --body "TODO"
bwrb bulk --body "DEPRECATED" --delete deprecated_field
bwrb list --body "meeting notes" --matches --type task

Behavior:

  • Searches Markdown body content only; use --where for frontmatter fields
  • Detailed matches keep original file line numbers and body-only context
  • Uses ripgrep under the hood for performance
  • Case-insensitive by default

All selectors compose via AND (intersection). Each additional selector narrows the result set.

Terminal window
# Find tasks in Work/ folder with status=active containing "deadline"
bwrb list --type task --path "Work/" --where "status == 'active'" --body "deadline"

Union (OR) is not implicit. To express OR logic, use boolean operators within --where:

Terminal window
bwrb list --where "status == 'draft' || status == 'review'"

For ergonomics, the first positional argument is auto-detected:

InputDetectionEquivalent
bwrb list taskMatches known type--type task
bwrb list "Ideas/"Contains /, matches path--path "Ideas/"
bwrb list "status == 'x'"Contains operators--where "status == 'x'"

Ambiguity handling: Read-only list modes may return multiple rows. A mutation never chooses one silently: ambiguity errors list candidate paths and tell you to retry with one exact vault-relative path (or narrow with explicit selectors).

Command--type--path--where--bodyPicker
editYYYYY
deleteYYYY-
listYYYYY
auditYYYY-
bulkYYYY-

Notes:

  • list is the canonical query, search, picker, and open surface.

Default behavior depends on command destructiveness:

Read-only commands (list, audit without --fix)

Section titled “Read-only commands (list, audit without --fix)”

No selectors = implicit --all (operate on entire vault).

Terminal window
bwrb list # Lists all notes
bwrb audit # Audits all notes

No selectors = prompt with picker.

Two safety gates:

  1. Targeting required: No selectors = error. Must specify at least one selector OR explicit --all.
  2. Execution required: Dry-run by default. Must use --execute to apply changes.
Terminal window
bwrb bulk --set status=settled
# Error: No files selected. Use --type, --path, --where, --body, or --all.
bwrb bulk --type task --set status=settled
# Dry-run: shows what would change, but doesn't apply
bwrb bulk --type task --set status=settled --execute
# Actually applies the changes
bwrb bulk --all --set status=settled --execute
# Works (explicit targeting + explicit execution)

This two-gate model prevents accidental vault-wide mutations.

bwrb audit --fix is a remediation workflow. It still requires explicit targeting (at least one selector or --all). Interactive fixes write by default; auto-fixes require --execute to apply.

  • Targeting required: No selectors = error. Must specify at least one selector OR explicit --all.
  • Interactive preview: Use --dry-run to preview guided fixes without writing.
  • Non-interactive: Use --fix --auto --execute for safe auto-fixes when stdin is not a TTY (omit --execute to preview).

See also: CLI Safety and Flags

Use the long --output <format> option when you want portable command syntax. On note workflows such as new, edit, list, and recent, -o means --open. A few management subcommands, including schema migrate and dashboard run, use -o for --output; check the command’s help before using the short form.

FormatDescription
textDefault human-readable
jsonMachine-readable JSON
pathsFile paths only
linkWikilinks ([[Note Name]])
treeHierarchical tree view (list only)
contentFull file contents (list)
Terminal window
bwrb list --type task --output json
bwrb list --type task --output paths
bwrb list --type task --output link # [[Task 1]], [[Task 2]], ...
bwrb list --type task --output tree # Hierarchical display
bwrb list --name "TODO" --output content # Full resolved file

When a command needs you to pick from multiple results, Bowerbird uses an interactive picker. The pagination keys below apply to the numbered picker (and auto when it falls back to numbered). If you use --picker fzf, fzf provides its own navigation.

  • Page size: 10 items (pagination appears when there are more than 10 options)
  • Keys: - previous page, +/= next page
  • 1-9 selects items 1-9 on the current page
  • 0 selects item 10 on the current page
  • Up/Down (or j/k) moves the highlight
  • Enter confirms the highlighted item
  • Ctrl+C / Escape cancels the picker

--output json is never interactive and never paginates.