Luis Mendez142 downloadsDisplay Bases results as a sortable product backlog tree of Epics, Features, PBIs and Tasks with drag-and-drop ranking, inspired by Azure DevOps Boards.
An Obsidian plugin that adds a Product Backlog view type to Bases. It turns a flat list of notes into a sortable work-item tree — Epics → Features → PBIs → Tasks — inspired by the backlog in Azure DevOps Boards.
▾ [Epic] Customer Portal (7)
▾ [Feature] Self-service login (3)
▸ [PBI] Password reset flow (2)
[Task] Design reset email
[Task] Add token endpoint
[PBI] SSO with Entra ID
▸ [Feature] Usage dashboard (2)
▸ [Epic] Billing revamp (4)
type matching one of the configured levels, or a parent. Meeting
notes, references and READMEs sitting in the same folder stay out of the tree (and out
of the backfill). The toolbar says how many notes it skipped.parent — a link to the parent item ("[[Customer Portal]]"). Items without a
parent are top-level.order — a number that ranks an item among its siblings.type — the ladder Epic → Feature → PBI → Task, the extra types Issue,
Bug, Idea and Deliverable that sit beside it rather than on it, or Milestone
— a marker on neither, which states a date rather than work.type, parent and order.parent and order, and leaves type alone —
always. A move is a move, never a re-classification: the type a note carries is the
one it keeps until you change it with Set type.type show a level implied from their parent's type (a child of a
Feature reads as a PBI, wherever that Feature sits).type, order and an empty value for each of
those properties on the notes that don't carry them. Nothing already set is
overwritten, no option you have set (or deliberately cleared) is changed, no type is
guessed for items whose parent is outside the view, and nothing moves: an empty
property is the "no state, not planned yet" the item was already in — it just becomes
visible and editable in Obsidian's own property editor, and pickable in the view
options.The fast way: run the Product Backlog: Create backlog command. It asks for a folder
(default docs), creates it together with a fully configured Product Backlog.base
inside, and opens the view — from empty vault to working backlog in one step.
Manually, the equivalent is:
docs/.Product Backlog.base) and add a filter such as
file.inFolder("docs").type/order your notes don't
have yet, and sets up the properties the board, the roadmap and the Deliverables
board need — each one's own empty state offers the same button when you get there
first. Notes with neither a supported type nor a parent aren't treated as backlog
items — to organize a folder of plain notes by dragging, turn Ignore notes outside
the hierarchy off in the view options first.Example .base file:
filters:
and:
- file.inFolder("docs")
views:
- type: product-backlog
name: Backlog
homeFolder: "docs"
order:
- note.status
- note.points
Keep the home folder and the filter pointing at the same place. New items are filed
under it, and the view can only show what the Base returns — a base filtering Backlog/
with the home folder left at docs creates items you will not see afterwards.
Backlogging into Roadmap/ means file.inFolder("Roadmap") and homeFolder: "Roadmap";
the type folders follow on their own. The Create backlog command writes it from the
one folder it asks you for, which is the whole reason it asks.
Any properties you enable under Properties in the Bases toolbar (the order list
above) get a column of their own on each row, in the order you put them — handy for
status, story points, assignee, etc. That menu is the only switch: a property it does
not show is not on the rows, and that includes the state, horizon, risk and tag chips.
| Action | How |
|---|---|
| Switch projection | Toolbar toggle — backlog tree, kanban board, roadmap, Deliverables board. See The board, The roadmap and The Deliverables board |
| Expand / collapse | Click the chevron, or use the toolbar buttons |
| Open an item | Click the row (Ctrl/Cmd-click for a new tab) |
| Re-order among siblings | Drag a row and drop it between two rows |
| Re-parent | Drag a row and drop it onto the middle of the new parent |
| Make an item top-level | Right-click → Outdent (Alt+Left), or drag it just above or below a row that is already top-level |
| Create a child item | Hover a row and click +, or use the context menu — where the row can hold more than one kind of item, the modal asks which |
| Create any type at the top | Toolbar New button, or the ▾ menu next to it for every other type |
| Focus one type | Toolbar focus button next to New → pick a level or an extra type (All types returns) |
| Move without dragging | Right-click → Move up / down / to top / to bottom / Indent / Outdent |
| Change an item's type | Right-click → Set type (every level, plus the extra types) |
| Change an item's state | Click the state chip on the row (there when the state property is a visible column), or right-click → Set state |
| Add a tag | Click the + in the row's tag column, or right-click → Edit tags |
| Remove a tag | Hover the row and click the ✕ on the tag |
| Undo the last change | Click the ↩ toolbar button, or press Ctrl/Cmd+Z in the tree |
| Hide finished work | Click the eye button in the toolbar (or toggle Show completed items in the view options) |
| Open in a new tab or split | Middle-click, Ctrl/Cmd-click, or right-click → Open in new tab / Open to the right |
| Find items | Use the Base's own search — the view is given the narrowed results and loads the ancestors they need, so the tree keeps its shape |
| See counts per type | Hover the item count in the toolbar |
A Base search narrows what the view is given rather than what it draws, so the rows that remain keep their place in the hierarchy — an ancestor the search excluded still loads as context (shown dimmed, and never written to) so a match is never stranded at the top level. The roadmap's unplaced shelf has a search of its own, scoped to the untriaged work beside it.
Like the separate Epics / Features / Stories backlogs in Azure DevOps, focus re-roots the
tree at any type: pick Feature from the button next to New in the toolbar and every
feature becomes a top-level row with its PBIs and tasks below it. Extra types are on that
menu too — focusing Bug gives you a list of every bug, which is the same kind of view.
Focusing the level an extra type ranks with (PBI, by default) shows both together. While
focused, that button shows the type, accented, with a ✕ beside it that returns to
everything in one click (so does picking All types). Items keep their real parents —
re-parenting by dropping into a row still works — but the top row of a focused view has
no shared ranking, so reordering, indent/outdent and the top-level drop strip are disabled
there.
Backlogs organized as folders work too. Enable Infer hierarchy from folder notes in the view options for structures like:
product-managements/
payments/ (a folder per product domain)
epics/
Checkout/
Checkout.md (folder note → top-level Epic)
One-click pay/
One-click pay.md (folder note → Feature under Checkout)
use-cases/
Pay with saved card.md (→ PBI under One-click pay)
Notes without an explicit parent link attach to the nearest ancestor folder note
(a note named like its folder, e.g. Checkout/Checkout.md). Container folders without
a folder note — epics/, use-cases/, the domain folders — simply pass through, and a
folder note itself looks for parents above its own folder. Untyped notes still imply
their level from the parent chain, so a note under a typed Feature reads as a PBI.
Rules to know:
parent link always overrides the folder structure, which is exactly what
drag and drop writes — so re-parenting works as usual, but files are not moved on
disk. The folder tree and the parent links can diverge; the links win. Right-click →
Use folder position removes the override and returns the item to its folder parent
(retyped for that level, together with its typed subtree, when auto-type is on).parent property as a "pinned to top
level" marker (deleting it would just re-infer the folder parent). Clear parent
link on an orphaned item removes the property entirely, so in folder mode the item
returns to its folder position.type — in folder mode the folder structure is the hierarchy. Notes in folders
without a folder note above them still need a supported type to appear.Domain, Epic, Feature, PBI, Task).Every property you make visible in the Base gets its own fixed-width column at the
end of the row, in the order the Base lists them, with the names in a header pinned to
the top of the tree. Values line up down the page instead of trailing each item's title,
so adding a property doesn't turn the rows into ragged text — a long Epic title and a
short Task title put their points in the same place. Property column width in the
view options sets how wide one column is; a value too long for its column is truncated,
with the full text (and the property name) in its tooltip.
The Bases properties menu decides the whole strip, chips included. The state, horizon, risk and tag properties are columns like any other: each draws its clickable chip where you put that property in the menu, and draws nothing at all while the menu is hiding it — configuring a property in the view options is what makes it editable, not what puts it on a row. Configure one and see no chip, and the properties menu is the place to look.
Columns never shrink — that is what keeps them aligned — so a long title truncates first, and a pane too narrow for the columns it is asked to show drops them instead of clipping them. They drop from the end of that same order, one at a time: the order is your statement of what matters, so nothing re-ranks it on your behalf, and the progress rollup outlasts every column because it is pinned past their end rather than being one of them. A dropped column is not rendered at all — there is nothing left of it for Tab or a screen reader to find — and widening the pane brings the columns back in the order they left. The view measures this against the width you configured and the depth on screen, so wide columns give way earlier than narrow ones, and expanding a deep branch can be what makes a column give way.
Rows carry no Property: labels of their own — that is what the header is for. To turn
a column off, hide its property in the Bases Properties menu; there is no second
switch in the view options.
When the property named by Tags property (tags by default) is one of the visible
properties, its column becomes editable:
Tags are written to frontmatter as a list, and typed input is normalized to a usable tag
(#Sprint 12! becomes Sprint-12); input Obsidian would not accept as a tag at all — a
number like 123 — is refused with a notice instead of being written (2026-07 is
fine: the hyphen is the non-numeric character Obsidian asks for). Removing the last
tag removes the key rather than leaving an empty list behind. Rows loaded as context from
outside the Base's filter show their tags but offer no editing, like every other write in
this view. Point Tags property at another key, or clear it, and that property goes
back to rendering as a plain, read-only value.
Set the State property (e.g. status) in the view options and parents show a
progress bar with a done count (e.g. 3/7), while done items dim out. Which values count
as done is configurable (Done, Closed, Completed, Removed by default, case-insensitive).
The progress rollup sits in a fixed column at the end of each row, after every property column, so it lines up vertically no matter how long an item's title is or how deep it sits in the tree.
Make the state property visible in the Bases Properties menu and it gets a column of its own there too — wherever you put it among the others (see Properties on a row) — and each row then carries a clickable state chip in it: pick a new state from its menu (also available via right-click → Set state, which stays offered whether or not the column is showing) and the note's frontmatter updates without opening it. The menu offers the Workflow states configured in the view options — or, when none are configured, the states already used in the backlog, with a done state appended so marking an item done is always one click away. An item whose state isn't in the list keeps it selectable in its own menu.
The toolbar's eye button (or the Show completed items view option) hides finished work: an item disappears once it and its entire subtree are done — a done parent with open children stays visible, so unfinished work can never hide. Progress bars keep counting hidden items, and moving or dropping rows around hidden siblings stays safe because ranking always runs over the real sibling lists.
While dragging, hovering the middle of a collapsed row expands it after a moment (the chevron lights up while the timer runs) so you can drop deeper into the tree. Dropping an item onto its own descendant is prevented. Which rows you left open is remembered per view, on this device — see Where the view remembers things. Indent guides connect each child group to its parent, and on touch devices the per-row + button and the tag add/remove controls are always visible, with larger touch targets. The tree is a real ARIA tree — screen readers announce level, position and expansion state — and the view honors reduced-motion and right-to-left settings.
Tab walks the view's toolbar — new item, the type picker, the focus level, backfill, undo, expand and collapse all and the completed-items toggle — and then reaches the tree as a single stop. Inside the tree the selected row moves with the arrow keys rather than with Tab, so a long backlog never becomes a long tab sequence; the row's own controls are reachable through the context menu.
Once in the tree (mirroring Azure DevOps backlog shortcuts where sensible):
| Keys | Action |
|---|---|
| ↑ / ↓ | Select the previous / next visible item |
| Home / End | Jump to the first / last visible item |
| ← | Collapse the item, or jump to its parent |
| → | Expand the item, or jump to its first child |
| Enter | Open the selected item (Ctrl/Cmd for a new tab) |
| Alt+↑ / Alt+↓ | Move the item up / down among its siblings |
| Alt+← | Outdent — make it a sibling of its parent |
| Alt+→ | Indent — nest it under the previous sibling |
| Ctrl/Cmd+Z | Undo the last backlog change (again to redo) |
| Escape | Clear the selection |
| Menu / Shift+F10 | Open the context menu for the selected item |
Every property change the view writes — a drop, a move, a state or tag change, the ✨ backfill — can be taken back right afterwards: click the ↩ toolbar button or press Ctrl/Cmd+Z in the tree. Undoing the undo redoes. One level is kept, per view and per session, and quick no-ops don't spend it — re-picking an item's current state won't cost you the undo of the drop before it. A batch that failed partway can still take back the part that landed.
Creating an item is the one exception: undo never deletes a note, so a new item stays — and the undo button still points at the last property change from before it. Delete the note itself to take a creation back.
Undo puts back exactly what was there before, and only where the note still holds what the view wrote: a property you edited by hand in the meantime is kept rather than overwritten, and a note deleted since is skipped — a notice says when either happened. It also works when the change itself moved an item out of the base's filter (marking a parent done in a base that hides done items): taking that change back is exactly what undo is for. Tags are undone as an add/remove of the same tags rather than as a snapshot, so tags you added yourself in between stay.
Epic → Feature → PBI → Task is a ladder: each level's children are the level below.
Some work does not fit a rung. A Bug breaks down into Tasks whether it was raised
against an Epic, a Feature or a PBI — its position says nothing about what it contains.
The same is true of an Idea: a thought about the portal and a thought about one screen of it are the same kind of thing, and neither is a Feature. A Deliverable is the other way round — a thing the project must produce rather than work to do — and it fits no rung for the same reason.
So Issue, Bug, Idea and Deliverable are extra types rather than a fifth
level, and two things follow:
All three are also creatable with no parent at all, from the toolbar's own "pick another type" menu — like every declared type.
Where a row can hold more than one kind of thing, the + button asks: the new-item
modal offers a type, defaulting to the ladder's own child. The context menu lists the
choices directly (New PBI, New Issue, New Bug, New Idea, New Deliverable),
and Set type offers every declared type. A row with only one option — a Task, or an
extra type, which holds only Tasks — asks nothing and creates it straight away.
Issue, Bug, Idea and Deliverable each get their own badge icon and colour — an
alert in pink, a bug in red, a lightbulb in yellow and a package in green. Nine badges
share the theme's eight colours, so one pair does overlap: an Idea and a Task read the
same yellow, told apart by the name on the badge. They rank with PBI, so focusing that level shows them
beside it rather than hiding them. Deliverable also has its own board with its own
workflow — see The Deliverables board below.
The type vocabulary is fixed. That is deliberate: a configurable vocabulary means every rule about levels has to hold for any list someone can type, and the reward is a rename. A note typed anything else keeps its own name on the badge and is carried through the ladder as before — nothing is rejected, it simply is not one of the shipped names.
None of this is enforced. The ladder has always guided what the view offers and what it writes without refusing a move you make deliberately, and extra types follow the same rule: drag a Bug wherever the work actually belongs.
Everything the view creates lives under one home folder (docs by default), and each
type gets its own folder picker — Folder for Epic items, Folder for Bug items, one
per type you have configured. A Bug is filed with the bugs wherever in the tree it hangs.
Each picker defaults to a subfolder of the home folder, so relocating a backlog is still
one setting: point the home folder at Roadmap and the defaults become
Roadmap/requirements, Roadmap/bugs, and so on. A folder you pick by hand stays picked;
only the untouched ones follow.
Types you rename or invent get no default: this plugin has no opinion about where a
Theme belongs, so it falls back to the home folder itself.
The new-item modal names the folder before you commit, and the line follows the type
picker — switch from PBI to Bug and it re-reads docs/bugs.
Keep these folders inside what your Base returns. The view creates a note and then
shows it only if the Base's filter matches, so a base filtered to Backlog/ with the
folders left at their docs/… defaults creates items you will not see afterwards. They
are not lost — they are notes with their parent links intact — but they are not where
you were looking. The Create backlog command writes every one of these folders under
the folder it scaffolds, so a backlog made that way is consistent from the start.
Full resolution order, first match wins:
A Base filtered to one level, one state or one tag returns matching items but not their
parents — and a backlog with no parents is just a list. So the view loads the missing
ancestors from the vault and renders them as context: filter to type == "PBI" and
each PBI still appears under its real Feature and Epic.
▾ [Epic] Customer Portal ↳ (context — not in the filter)
▾ [Feature] Self-service login ↳
[PBI] Password reset flow (the actual match)
Context rows are italic and dimmed, with a ↳ marker. They are not results, so:
parent link
still points at the right item);The last point generalizes into the one real caveat of working in a filtered base: any
parent whose children are partly filtered out has a partial sibling list, whether it is
a context row or a match. Dropping into such a parent appends after the last visible
child, so the new order is computed without knowing the excluded children's values and
can duplicate one of them. Nothing breaks — items with equal orders fall back to the
Base's own sort, and the group is renumbered by the next drop that needs the room — but
if you care about exact ranking, do the reordering in an unfiltered base.
Turn Show parents outside the filter off to go back to a flat list of matches, where items whose parent is missing show the unlink icon.
Expanding or collapsing a row re-renders only that row's children, selection and keyboard navigation use a path index instead of searching the tree, and the Base's property lookups happen once per render rather than once per row — so a backlog of several hundred items stays responsive to interaction. A write (dragging, a state change, anything that touches frontmatter) still re-renders every row, because the Base re-runs its query and any visible property may have changed; collapsing the levels you're not working on is the best lever there.
A batch — "Assign missing type and order properties" over a whole backlog, or a drop
that renumbers a large sibling group — writes one note at a time, and each of those writes
would otherwise come back as its own refresh. The view rebuilds once when the batch
finishes instead, so the tree doesn't churn through hundreds of half-applied states on the
way. Nothing is frozen while that happens: you can scroll, filter, expand and select
throughout. The toolbar shows how far along the batch is (Updating 12 of 340…), and the
commands that would be refused mid-batch grey out until it's done.
Undoing one is a batch in its own right, with the same progress indicator: a backfill over three hundred notes comes back in a single press.
Three different kinds of state, kept in three different places on purpose:
.base file. It describes the view itself, so
it is shared with anyone you share the base with, and it travels with the vault.A row nobody has ruled on yet opens collapsed, so a large backlog starts as a readable list of top-level items rather than a wall of every task. Once you open or close a row, that choice is what comes back. Notes you delete are forgotten on the next save.
If the view can't tell which base it belongs to, it quietly falls back to remembering your rows for the session only — sharing one bucket between bases would be worse than forgetting, because two backlogs would keep opening each other's rows.
Sibling order is a number (10, 20, 30…). Dropping between two items assigns the halfway
value; when the gap gets too small the view transparently renumbers that sibling group.
Items without an order sort after ranked siblings, alphabetically.
The same backlog read as a kanban board: one column per workflow state, and one card per item the view is showing. Switch with the toolbar's Show as kanban boards button.
Focus decides what a card is. With no focus set, every result gets a card. Focus a level — Feature, say — and the cards are the features, with their PBIs and tasks represented beneath them rather than scattered across the columns as cards of their own. That is the same re-rooting the tree does, and it is usually what you want from a board: one card per thing you are tracking, at the altitude you are tracking it.
The projection is working position, not configuration. Which of the four a view is
showing is remembered per saved view, per device, in the view-state store — it is never
written to the .base, so opening the same backlog on another machine does not move
anyone else's view.
The board needs a state property. Without one it shows guidance and a button that sets it up. The Workflow states (in order) list is optional: with it, those are the columns, in that order. Without it, the board draws the states your notes actually carry — plus a done column even if nothing is in it yet, when none of the states you carry already counts as done, so marking an item done is always one click away.
Only your results mint columns. A card the Base's filter excluded, shown as context, never adds a column for its own state — that state is not your board's vocabulary. If its value matches no column, it sits in the no-state column.
| Action | How |
|---|---|
| Move a card | Drag it to another column, press Alt+←/→, or right-click → Set state |
| Clear an item's state | Drop it on the column for items with no state — this removes the property rather than blanking it |
| Read a column's agreement | Hover the column header, or open the column menu |
| Create in a column | Toolbar New, then drag — creation from a column is not built yet |
started and finished ride the state write, so neither fires
without a state property. Each also needs its own list to name at least one value —
started in States that count as started (empty by default), finished in
States that count as done (populated by default) — or the property is only ever
created empty for you to fill by hand, never stamped. Once both are configured, the two
behave differently once work is reworked:started is written only while the property is empty, so the earliest start
survives. Entering a started state again does not move it.finished follows the done boundary. Completing an item stamps it; reopening
clears it, because an item back in progress must not claim a finish it no longer has;
completing again stamps the new date. Moving between two done states — Done becoming
Dropped — is a re-labelling and writes nothing.Deliverable items never appear here. They get a board of their own — see
The Deliverables board — though one acting purely as an
excluded ancestor can still render as an inert context card for a visible descendant,
the same as any other excluded parent.Every move — drag, keyboard or menu — is the same gated write, announced in the same words, and taken back by the same Ctrl/Cmd+Z.
A fourth projection, alongside tree/board/roadmap, reserved for items typed
Deliverable — concepts, designs and anything else the team must produce rather than
plan. It draws from a workflow: its own state property, ordered states and done
values when you configure one — in which case it is entirely independent of the board
above, and a Deliverable finished in one workflow does not read as finished in the
other — or, left unconfigured, the same workflow the board above already uses, so a
vault that never bothered to name a separate property still gets a working
Deliverables board rather than an inert one; in that case the two boards deliberately
share the one property and the one write.
A Deliverable never appears as a card on the board above — that board is scoped to everything else, whatever either workflow's state says — though it still counts on the tree and on both roadmap axes, and one acting purely as an excluded ancestor still shows there as a context card for a matching visible descendant, the same as any other excluded parent.
Columns and a workflow only — no WIP limits, no column policies, no started/finished date stamps, and "Show completed items" has no effect here: a Deliverable's completion state on either workflow never hides its card, so only the Base's own search narrows what is shown. The focus level set elsewhere in the toolbar has no effect on this board at all — a focus left on, say, Feature would otherwise make a Deliverable outside that subtree confusingly disappear, so the toolbar's Focus control always reads a plain, disabled "Deliverables" button here, whatever the inherited focus is: never a menu to pick a different focus (every card is already a Deliverable, so there is nothing to narrow by that way), and never a "Focused: …" label with a clear button, since no focus level narrows this board's own cards for one to clear. Moving a card (drag, Alt+←/→, or the card menu's Set state) writes the resolved Deliverable state property — its own key when you configured one, or the shared one when you did not.
The toolbar's New button on this board always creates a Deliverable; the picker for every other type, offered everywhere else, is absent here since nothing else could ever appear as a card.
Everything else about a Deliverable — its parent, its rank, its tags, its place on the roadmap — is the same property every other type already uses; nothing about this board changes how those work.
The same backlog on a time axis. Switch with the toolbar's Show as roadmap button. The mode persists exactly as the board's does.
The axis is declared, never guessed. The roadmap draws whichever axis the view options configure — it does not infer one from property names and never derives horizons from dates. There are two:
| Axis | Configured by | Writable |
|---|---|---|
| Horizons | Horizons (in order) plus a horizon property | Yes |
| Timeline | A start date property, a target date property, or either one alone | From the row menu, for any end the item can actually use — no drag gestures on the bars yet |
With both configured, an axis picker appears in the toolbar — Show horizons and Show timeline. With only one, there is no choice to make and the picker stays away.
| Action | How |
|---|---|
| Move between horizons | Drag the card, press Alt+←/→, or right-click → Set horizon |
| Un-place an item (horizons axis only) | Drag it to the shelf — this removes the horizon property |
| Create in a horizon | The + on the bucket, which files the new item with that horizon already set |
| Set dates | Right-click → Schedule / Unschedule |
Buckets are the values in Horizons (in order) — a Now / Next / Later axis, or whatever you name — plus one more for any result whose horizon value the list omits, the same carve-out the board's columns make. Every move is one gated write, undoable as one batch.
The shelf — labelled Unplaced on screen — holds the results the axis could not place, with a count. On the horizons axis it is also the drop target that un-places: dropping there removes the key rather than blanking it, and it stays reachable while empty, because a target that only exists when occupied is one nothing can reach. On the timeline it is display-only — nothing on the dated axis is draggable, so there is no un-place gesture there; an item lands on the shelf by having its dates cleared from the row menu instead.
Items your Base's filter excluded are not on the shelf and not in its count. On a focused roadmap, a focus-level item the filter excluded is shown as context so its children have somewhere to hang, not because it is work you have left unplanned. On the timeline every excluded item goes straight to a Context strip beside the shelf. On the horizons axis, one whose horizon value matches an existing bucket sits in that bucket instead — only one with no value, or a value no bucket names, reaches the Context strip. Unfocused, the roadmap draws results only and no context strip appears at all.
The timeline draws a bar from each item's dates. One date property is enough — a target-only roadmap of milestones and deadlines, or a start-only plan, are both supported. A parent with no dates of its own spans its dated descendants, endpoint to endpoint, drawn as the inference it is and written to no note — for an ordinary work item. A milestone is its target date alone: with none of its own, it goes to the shelf, Unplaced, whatever dates its children carry.
Dates are set from the row, not from the bar: right-click → Schedule or Unschedule, on any projection — the tree, the board, the roadmap and the Deliverables board all reach the same row menu, deliberately: a write reachable only from roadmap mode would be a projection disagreeing about what the backlog can do. What this release does not have is a gesture on the bar itself — dragging one to move it, dragging its edge to resize, or dragging an item off the shelf onto a date. Those are specified and not yet built.
Schedule appears only when the item has an end it can use. A milestone is its target date alone, so on a roadmap configured with a start property and no target, milestones offer no Schedule at all — there is nothing they could legally write. The entry is withheld rather than opened onto nothing.
Milestones are a type of their own: on no rung of the ladder, offered no child types,
and counted in no rollup — a milestone states a date rather than work, so a progress
bar must not count it. One is drawn at its target date, from the target property alone;
a start on a milestone is ignored, never rewritten and never removed.
Like every other type rule here, this guides rather than refuses: drag a milestone
under an Epic, or write a parent on one by hand, and the link is kept — the same
advisory-not-enforced rule the types section above states. What the type withholds is the
offer, not the possibility.
Planned dates are different properties from the board's transition stamps, so a plan can never overwrite a record of what actually happened.
Open the view options in the Bases toolbar to configure:
| Option | Default | Purpose |
|---|---|---|
| Parent property | parent |
Note property that links to the parent item |
| Order property | order |
Numeric sibling rank |
| Item type property | type |
Hierarchy level of the item |
| Ignore notes outside the hierarchy | on | Only treat notes with a supported type or a parent as backlog items |
| Show parents outside the filter | on | Load the ancestors the Base's filter excluded, so matches keep their place in the tree |
| Infer hierarchy from folder notes | off | Folder mode: a folder's own note is the parent of the notes beside it, so a child needs no explicit parent link |
| State property | (off) | Note property with the workflow state; enables progress bars and done styling |
| Workflow states (in order) | (off) | The board's columns, in that order. Left unset, the board draws the states your notes actually carry, plus a done column even if nothing is in it yet, so marking an item done is always one click away |
| States that count as done | Done, Closed, Completed, Removed |
Which state values complete an item |
| States that count as started | (off) | Which state values start the clock — entering one stamps the started date |
| WIP limit for <state> | (off) | One per configured state that is not a done state — a finished column is a record, not a queue, so it is never offered a limit. The most items that stage should hold. Reads the full column, not the filtered count, and refuses nothing |
| Policy for <state> | (off) | One per configured state, done ones included. The working agreement for that column, readable from its header and menu |
| Home folder | docs |
The folder the backlog lives under; every type folder below defaults to a subfolder of it |
| Horizon property | (off) | Note property holding the roadmap's horizon; with Horizons (in order) it makes the bucket axis |
| Horizons (in order) | Now, Next, Later |
The buckets the horizon axis draws, in order. Naming a Horizon property is enough to turn the axis on — the values ship populated, so you only need to edit this list to rename or add buckets |
| Start date property / Target date property | (off) | The dates the timeline draws bars from. Either one alone is enough — a target-only roadmap or a start-only plan both work |
| Started date / Finished date property | (off) | Where the board stamps transition dates as a card moves. Never the same properties as the planned dates above — a plan must not overwrite a record |
| Show completed items | on | Off hides fully-done subtrees from the tree, the board and the roadmap (only while a state property is set); the Deliverables board ignores it — see The Deliverables board — and nothing about ranking or rollups changes anywhere |
| Folder for <type> items | <home>/requirements, <home>/tasks, <home>/issues, <home>/bugs, <home>/ideas, <home>/deliverables, <home>/milestones |
One folder picker per configured type. Untouched, each follows the home folder |
| Deliverable state property | (off) | Note property with the Deliverable workflow's own state. Left off, the Deliverables board falls back to the board above's own state property rather than going inert — and to its states and done values only where you have left the two rows below empty, since a list you fill in is this workflow's own either way |
| Deliverable workflow states (in order) | (off) | The Deliverables board's columns, in that order. Whatever you set here wins, whether the workflow has a property of its own or shares the one above. Left empty it falls back to Workflow states (in order) while Deliverable state property is also unset; with your own property set it draws the states your Deliverables actually carry |
| Deliverable states that count as done | Done, Closed, Completed, Removed |
Which Deliverable state values complete a Deliverable. Whatever you set here wins, whether the workflow has a property of its own or shares the one above. Left empty it falls back to States that count as done while Deliverable state property is also unset; with your own property set it stays the default shown here rather than borrowing that customization |
| Property column width | 132 px |
Width of one property column. Which properties are columns is the Bases Properties menu's, not a view option — see Properties on a row |
| Tags property | tags |
Property whose column supports adding and removing tags inline |
| Show descendant counts | on | Show the number of items below each parent (replaced by the progress rollup when a state property is set) |
Notes:
order property always wins for ranked siblings. Items without an order
sort last — in the order the Base's sort setting produces, so sorting by e.g.
priority or modified date arranges your unranked items until you rank them.type is one of the configured Levels, or when it has a parent —
an explicit link (even a broken one, so stale links stay fixable), the empty "pinned to
top level" marker, or a folder note in folder mode. The test runs per subtree, so an
untyped child of a typed item stays, and so does an untyped or custom-typed note that
holds typed ones. Everything else — the meeting notes, the folder's README, a
type: meeting-note page — is skipped, and the toolbar shows an N notes ignored
advisory. Turn the option off to show every note the base returns (useful for
organizing a folder of plain notes by dragging them into a hierarchy).parent links to a note that does not exist at all are shown at the top
level with an unlink icon. Dropping such an item at the top level clears the stale link.
A parent that exists but sits outside the filter is loaded as a context row instead.In Obsidian: Settings → Community plugins → Browse, search for Product Backlog, then install and enable it. The directory listing is at community.obsidian.md/plugins/product-backlog-view.
Manually, from a release:
main.js, manifest.json and styles.css from the latest release
(or build them yourself, see below).<your vault>/.obsidian/plugins/product-backlog-view/.To track unreleased builds, install via BRAT with the repository URL.
npm install
npm run dev # watch mode
npm run build # typecheck + production build
npm run test-build # build into .obsidian/plugins/ here, so the repo is a test vault
npm test # unit + DOM interaction tests (vitest, jsdom)
npm run test:coverage # tests with enforced coverage thresholds
npm run lint # eslint with the official eslint-plugin-obsidianmd rules
npm run analyze # fallow: dead code, duplication, complexity, dependencies
npm run check # everything in one shot — the pre-commit gate
npm run test-build bundles the plugin into .obsidian/plugins/product-backlog-view/
inside this repository, so the repository root can be opened as an Obsidian vault
with the plugin already installed and listed as enabled — no second checkout, no
symlink, no copying three files by hand after every edit. The bundle is unminified with
an inline sourcemap, so a stack trace in the developer console points back at the
TypeScript. The vault folder is gitignored.
A vault opened for the first time is in Restricted Mode, which loads no community plugin whatever the enabled list says: turn it off once under Settings → Community plugins. The script deliberately doesn't do that for you — it's a security decision that belongs to whoever opens the vault.
There is a backlog waiting in it. docs/ is this plugin's own register, written in the
plugin's own schema and laid out the way the view files things by default —
requirements/ (Epic → Feature → PBI), tasks/, issues/, bugs/. Open
docs/Product Backlog.base and the plugin is displaying the backlog that produced it.
Bases is a core plugin and must be enabled for the view to appear at all.
This matters more than a convenience script usually would: no test in this repository can check what the plugin looks like, and several Bases behaviours are assumed rather than exercised, because Obsidian cannot run in the jsdom harness. This is the shortest path to checking those by hand.
src/ is organised in four layers, each of which may reach anything below it and
nothing above:
domain/ |
What a backlog is: tree building, ranking, drop-target math, the view-options schema. Reads the vault, never writes it, never touches the DOM. |
storage/ |
The only place anything is persisted: frontmatter and its inverses, new notes, the .base file, view state. |
view/ |
The Bases view itself — rendering, drag & drop, keyboard, menus, undo. |
commands/, ui/ |
The "Create backlog" command, and the shared prompts. |
The direction is enforced, not just documented: eslint.config.mjs fails the build if
domain/ imports from view/, and bans processFrontMatter, vault.create and
load/saveLocalStorage anywhere outside storage/ — so a new write path can't appear
by accident. Several of the subtler invariants are checks rather than prose for the same
reason: ranking may not run over the rendered (focus-mode) roots, a menu opened from a
button must anchor to that button, and a hierarchy level may never be derived from the
depth a row happens to be drawn at.
The pure logic — tree building, drop planning, ranking, property backfill, note
creation, undo capture and restore — is covered by node unit tests, and the interaction
layer (rendering, drag & drop, keyboard, menus, creation prompts) by jsdom tests that dispatch real DOM events
against the actual view, all running against a small mock of the obsidian module
(test/helpers/obsidian-mock.ts). Coverage (v8) is threshold-enforced. Linting uses Obsidian's
official eslint-plugin-obsidianmd
ruleset plus size/complexity budgets, and fallow
gates dead code, duplication, complexity hotspots (CRAP, fed by the coverage report) and
dependency hygiene. CI runs the full gate on every push and pull request. CLAUDE.md
documents the architecture, invariants and test harness for AI-assisted development.
MIT