Skip to content

Types and Inheritance

Bowerbird uses strict type inheritance to reduce duplication and ensure consistency.

Types form a single-inheritance tree. Every type extends exactly one parent, and all types ultimately inherit from meta:

meta (global fields)
├── reflection
│ ├── daily-note
│ └── idea
├── objective
│ ├── task
│ └── milestone
└── entity
├── person
└── place

Types are defined with the extends property linking to their parent:

{
"types": {
"meta": {
"fields": {
"status": { "prompt": "select", "options": ["raw", "active", "done"] },
"created": { "value": "$NOW" }
}
},
"objective": {
"fields": {
"deadline": { "prompt": "date" }
}
},
"task": {
"extends": "objective",
"fields": {
"priority": { "prompt": "select", "options": ["low", "medium", "high"] }
}
}
}
}

A task note gets:

  • status and created from meta
  • deadline from objective
  • priority from itself
  1. Single inheritance — Each type has exactly one parent
  2. Unique names — Type names must be unique across the entire schema
  3. No cycles — A type cannot extend its own descendant
  4. Explicit-key field overrides — When a child re-declares an inherited field, every key it declares wins, including structural keys such as prompt, options, multiple, required, and source. Keys it omits stay inherited.

Traits — composition alongside inheritance

Section titled “Traits — composition alongside inheritance”

Inheritance is is-a: a task is an objective. But some field groups cut across unrelated type families — status + due dates, scope, rating — and copying them into every type invites drift. Traits are also-has composition: define a reusable field bundle once and mix it into any type.

{
"traits": {
"actionable": {
"fields": {
"status": { "prompt": "select", "options": ["inbox", "next", "done"] },
"due": { "prompt": "date" }
}
}
},
"types": {
"task": {
"extends": "objective",
"traits": ["actionable"]
}
}
}

A task now gets its inherited fields from objective plus status and due from the actionable trait. A type can compose multiple traits, and the same trait can be reused by any number of unrelated types.

When a field name comes from several sources, the most specific source wins:

own type fields > traits > inherited (parent chain)

The override is full at the trait boundary: a trait field fully replaces an inherited field of the same name (every key, not just default), a later trait in the array fully replaces an earlier one, and an own field fully replaces a trait field — own’s prompt, options, and label all win, with no trait values leaking through.

At the own-vs-parent inheritance boundary (no trait involved), the override is an explicit-key merge. The child keeps every inherited key it omits and replaces every key it declares. This applies equally to metadata and structure: default, description, prompt, options, multiple, required, source, and other declared properties all win locally.

A type composing an unknown trait is a deterministic error.

Traits are flat — they bundle only fields and cannot extend types or compose other traits. See the Schema Reference for the full rules, and run bwrb schema list type <name> to see which trait contributed each field.

Fields define what data each note type collects:

  • Static — Fixed value: { "value": "$NOW" }
  • Text — Free input: { "prompt": "text" }
  • Select — Choose from options: { "prompt": "select", "options": [...] }
  • Relation — Link to other notes: { "prompt": "relation", "source": "milestone" }
  • Number, Boolean, Date, List — Other prompt types