Types and Inheritance
Bowerbird uses strict type inheritance to reduce duplication and ensure consistency.
Type Hierarchy
Section titled “Type Hierarchy”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 └── placeDefining Types
Section titled “Defining Types”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:
statusandcreatedfrommetadeadlinefromobjectivepriorityfrom itself
Inheritance Rules
Section titled “Inheritance Rules”- Single inheritance — Each type has exactly one parent
- Unique names — Type names must be unique across the entire schema
- No cycles — A type cannot extend its own descendant
- 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, andsource. 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.
Precedence
Section titled “Precedence”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.
Field Types
Section titled “Field Types”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
Next Steps
Section titled “Next Steps”- Schema Reference — Complete property reference
- Migrations — Evolving your type system