Schema-driven note management for markdown vaults
npm install bwrbShort for bowerbird, pronounced "birb".
Schema-driven note management for markdown vaults.
> Pre-release software. bwrb is under active development. The CLI works and is usable, but the schema format and command surface may change before v1.0. See the roadmap for current status.
bwrb is a CLI tool that creates and edits markdown files based on a hierarchical type schema. It supports:
- Interactive type selection with subtype navigation
- Dynamic frontmatter prompts (select options, text input, vault queries)
- Configurable body sections with various content types
- Edit mode for updating existing files
- List and filter notes by type and frontmatter fields
- Works with any vault via the --vault flag
- Node.js >= 22
``sh`
cd ~/Developer/bwrb
pnpm install
pnpm build
pnpm link --global # Makes 'bwrb' available globally
`sh`
pnpm dev -- new idea # Run without building
Create a .bwrb/schema.json in each vault you want to use with bwrb.
`shVault path resolution (in order of precedence):
1. --vault=
2. Find-up: nearest ancestor with .bwrb/schema.json
3. BWRB_VAULT environment variable
4. Find-down under cwd if not in a vault:
- 1 candidate => auto-select
- multiple => numbered picker (TTY) or error requiring --vault
(non-TTY or --output json)
Schema Structure
The schema file is expected at
. It defines:$3
Hierarchical type definitions. Types can have subtypes for nested categorization:
`json
{
"types": {
"objective": {
"subtypes": {
"task": { / type definition / },
"milestone": { / type definition / }
}
},
"idea": {
"output_dir": "Objectives/Ideas",
"frontmatter": { / ... / }
}
}
}
`$3
Each leaf type requires:
| Field | Required | Description |
|-------|----------|-------------|
|
output_dir | Yes | Directory relative to vault root |
| frontmatter | Yes | Field definitions |
| frontmatter_order | No | Array specifying field order |
| body_sections | No | Array of section definitions |$3
Fields can be static or prompted:
Static value:
`json
{
"type": { "value": "objective" },
"creation-date": { "value": "$NOW" }
}
`Special values:
$NOW (local datetime, YYYY-MM-DD HH:mm), $TODAY (local date, YYYY-MM-DD)Select from options:
`json
{
"status": {
"prompt": "select",
"options": ["raw", "backlog", "planned", "in-flight", "settled"],
"default": "raw"
}
}
`Text input:
`json
{
"deadline": {
"prompt": "text",
"label": "Deadline (YYYY-MM-DD)",
"required": false
}
}
`Dynamic (vault query):
Query notes of a specific type to populate field options:
`json
{
"milestone": {
"prompt": "relation",
"source": "objective/milestone",
"filter": "status != 'settled' && status != 'ghosted'",
"format": "quoted-wikilink"
}
}
`-
source - Type path to query (e.g., "objective/milestone")
- filter - Optional expression to filter results
- format - Output format: plain, wikilink ([[value]]), quoted-wikilink ("[[value]]")$3
Define document structure after frontmatter:
`json
{
"body_sections": [
{
"title": "Steps",
"level": 2,
"content_type": "checkboxes",
"prompt": "list",
"prompt_label": "Steps (comma-separated)"
},
{
"title": "Notes",
"level": 2,
"content_type": "paragraphs",
"children": [
{ "title": "Subsection", "level": 3, "content_type": "bullets" }
]
}
]
}
`Content types:
none, paragraphs, bullets, checkboxesTemplates
Templates provide reusable defaults and body structure for note creation. They're stored in
.bwrb/templates/, organized by type path.$3
`
my-vault/
└── .bwrb/
├── schema.json
└── templates/
├── idea/
│ └── default.md # Default template for ideas
└── objective/
└── task/
├── default.md # Default template for tasks
└── bug-report.md # Bug report template
`$3
Templates are markdown files with special frontmatter:
`yaml
---
type: template
template-for: objective/task
description: Bug report with reproduction steps
defaults:
status: backlog
priority: high
prompt-fields:
- deadline
---Description
[Describe the bug]
Steps to Reproduce
1.
2.
3.
Expected Behavior
Actual Behavior
`$3
| Property | Required | Description |
|----------|----------|-------------|
|
type | Yes | Must be template |
| template-for | Yes | Type path (e.g., objective/task) |
| description | No | Human-readable description |
| defaults | No | Default field values (skip prompting) |
| prompt-fields | No | Fields to always prompt for, even with defaults |
| filename-pattern | No | Override default filename |$3
The template body becomes the note body, with variable substitution:
-
{fieldName} - Replaced with frontmatter value
- {date} - Today's date (YYYY-MM-DD)
- {date:FORMAT} - Formatted date (e.g., {date:YYYY-MM})$3
`sh
Auto-use default.md if it exists
bwrb new taskUse specific template
bwrb new task --template bug-reportRequire default template (error if not found)
bwrb new task --template defaultSkip template system
bwrb new task --no-templateJSON mode with templates
bwrb new task --json '{"name": "Fix bug"}' --template bug-report
`$3
Templates use strict matching - only templates in the exact type path directory are considered:
-
objective/task -> .bwrb/templates/objective/task/*.md
- idea -> .bwrb/templates/idea/*.mdThere is no inheritance from parent types.
$3
Use the
template command to manage templates:`sh
List all templates
bwrb template list
bwrb template list objective/task # Filter by type
bwrb template list idea default # Show specific template detailsValidate templates against schema
bwrb template validate # All templates
bwrb template validate idea # Templates for specific typeCreate new template interactively
bwrb template new idea
bwrb template new objective/task --name bug-reportCreate template from JSON
bwrb template new idea --name quick --json '{"defaults": {"status": "raw"}}'Edit template interactively
bwrb template edit idea defaultEdit template from JSON
bwrb template edit idea default --json '{"defaults": {"priority": "high"}}'Delete a template
bwrb template delete idea quick
`Adding a New Type
1. Add type definition under
types:
`json
{
"types": {
"my-type": {
"output_dir": "My/Output/Dir",
"fields": {
"type": { "value": "my-type" },
"status": { "prompt": "select", "options": ["raw", "active", "done"] }
},
"field_order": ["type", "status"],
"body_sections": []
}
}
}
`2. Validate schema (optional):
`sh
./validate_schema.sh
`Schema Validation
The schema structure is defined by
schema.schema.json (JSON Schema draft-07). To validate:`sh
./validate_schema.sh
`File Structure
bwrb repo:
`
bwrb/
├── src/
│ ├── index.ts # CLI entry point
│ ├── commands/
│ │ ├── new.ts # Create new notes
│ │ ├── edit.ts # Edit existing notes
│ │ └── list.ts # List and filter notes
│ ├── lib/
│ │ ├── schema.ts # Schema loading & validation
│ │ ├── frontmatter.ts # Frontmatter parsing & writing
│ │ ├── query.ts # Filter parsing & evaluation
│ │ ├── vault.ts # Vault operations
│ │ └── prompt.ts # Interactive prompts
│ └── types/
│ └── schema.ts # Zod schema definitions
├── tests/
│ └── ts/ # TypeScript test suite
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── schema.schema.json # JSON Schema for validating vault schemas
└── README.md
`Each vault:
`
my-vault/
└── .bwrb/
└── schema.json # Vault-specific type definitions
`Navigation Commands
$3
Open a note by name or path query. If no query is provided, shows a picker to browse all notes.
`sh
bwrb open # Browse all notes with picker
bwrb open "My Note" # Open with system default (default)
bwrb open "my note" # Case-insensitive
bwrb open "Ideas/My Note" # By path
bwrb open "My Note" --app editor # Open in $VISUAL or $EDITOR
bwrb open "My Note" --app system # Open with system default
bwrb open "My Note" --app print # Just print the resolved path
`App modes:
-
obsidian - Open in Obsidian via URI scheme
- editor - Open in $VISUAL or $EDITOR
- system - Open with system default handler (default)
- print - Just print the resolved pathEnvironment variable: Set
BWRB_DEFAULT_APP to change the default app mode:
`sh
export BWRB_DEFAULT_APP=editor # Always open in $EDITOR by default
`Picker modes (when query matches multiple files or no query):
-
--picker auto - Use fzf if available, else numbered select (default)
- --picker fzf - Force fzf
- --picker numbered - Force numbered select
- --picker none - Error on ambiguity (for scripting)JSON output (implies
--picker none):
`sh
bwrb open "My Note" --app print --output json
`$3
Find notes and generate wikilinks. If no query is provided, shows a picker to browse all notes. Uses shortest unambiguous form:
- Basename if unique across vault:
[[My Note]]
- Full path if ambiguous: [[Ideas/My Note]]`sh
bwrb search # Browse all notes with picker
bwrb search "My Note" --wikilink # Output: [[My Note]]
bwrb search "My Note" # Output: My Note
bwrb search "Amb" --picker none --output json # Scripting mode
`Neovim/scripting example:
`sh
Copy wikilink to clipboard (macOS)
bwrb search "My Note" --wikilink | pbcopyUse in a Lua script
local link = vim.fn.system("bwrb search 'My Note' --picker none")
`Shell Completion
Enable tab completion for commands, types, and paths.
$3
Add to
~/.bashrc:`bash
eval "$(bwrb completion bash)"
`$3
Add to
~/.zshrc:`zsh
eval "$(bwrb completion zsh)"
`$3
Run once to install:
`fish
bwrb completion fish > ~/.config/fish/completions/bwrb.fish
`$3
- Commands:
bwrb shows new, edit, list, open, etc.
- Options: bwrb list - shows --type, --path, --where, etc.
- Types: bwrb list --type shows types from your schema (task, idea, etc.)
- Paths: bwrb list --path shows vault directories (Ideas/, Objectives/, etc.)Running Tests
`sh
pnpm test # Run tests
pnpm test:coverage # Run with coverage report
pnpm typecheck # Type checking
``