Product Backlog

by Luis Mendez
5
4
3
2
1
Score: 60/100

Description

An Obsidian bases view plugin to convert a list of items into a tree which is sortable.

Reviews

No reviews yet.

Stats

0
stars
126
downloads
0
forks
17
days
0
days
1
days
168
total PRs
3
open PRs
1
closed PRs
164
merged PRs
0
total issues
0
open issues
0
closed issues
2,114
commits

Latest Version

a day ago

Changelog

Changed

  • Milestones now share one row at the top of the dated roadmap. Instead of a row each among the bars, every milestone draws as a diamond in a single Milestones row ahead of the first bar — the same row the resources axis already had — so the dates the plan is measured against read across the work beneath them and the work starts at the top of the grid. Each diamond names itself (title, exact date and workflow state) and opens its note on a click; the full-height line and its label are unchanged, and so is everything about where a milestone lands, what a drag writes and what the shelf does with it. Two consequences worth knowing: no fold can take a milestone off the grid any more, not even folding its parent — that is the point of the row — and a milestone is no longer a stop for the roadmap's arrow keys, so reach one from the tree or the board when there is no pointer.

Added

  • A MoSCoW priority on every row. Name a priority property under the new Prioritization view options and each row draws a priority chip: press it, or use Set priority in the row menu, to pick a rung. The ladder ships holding 1 - Must, 2 - Should, 3 - Could, 4 - Won't and is yours to rewrite; clearing it takes the chip and the menu away and leaves an ordinary property. The ✨ button binds and backfills it like every other optional property, clearing removes the key rather than blanking it, and every write is one undoable batch.

  • The Deliverables board moved into the scope picker. Its toolbar toggle position is gone: every board is the Board button now, and the picker beside it says which — Product and Deliverables lead the menu, each under its own icon, with the iterations below. The pick is remembered like an iteration scope, so leaving Board and returning reopens the board you were on.

  • The scope picker is the board's own control. It draws after the New button, on the board and nowhere else, and it draws even in a vault with no iterations yet — because it carries the only way to make the first one. Iterations no longer appear in the tree or in any New menu: an iteration is the container a board is scoped to, not work the backlog holds. A new one is named 1 - Iteration by default, numbered so a folder of them sorts in the order they run.

  • Make and edit an iteration from the board. The scope picker carries New iteration… and, on a chosen sprint, Edit iteration…. A new one is dated for you — the day after the last sprint ends, running for the length you configure — and every field is a prefill you can change before it is written. The note is not opened: making a sprint is a planning act, and the board you are planning on stays in front of you. Editing writes to the iteration note alone: it never re-stamps the work already in it.

  • A board scoped to one iteration. Pick a sprint from the scope picker beside the projection switcher and the Board position draws that iteration alone, in three columns over your own workflow: Open, In progress and Resolved. Which of your states fall in the two outer columns is configured; everything else is in between. The iteration's goal draws above the columns, a card moves between buckets by drag, Alt+arrow or its menu, and the choice of scope is remembered per view, per device — through a rename of the note or of a folder above it.

  • An iteration to put work in. A note typed Iteration is a time box: name its property in the view options (a goal property too, for later), then, from any row's or card's menu, put an item in it with Set iteration (or take it back out with None). Joining one takes its start and target dates in the same action, so scheduling a sprint is one pick rather than three.

  • Fold a type group in the shelf. Each type group in the expanded shelf now has a disclosure beside its name.

  • Search the shelf, and pick its types without the menu shutting. The expanded shelf's header carries a search box: type and it narrows to the unplaced cards whose title matches, leaving the placed half of the roadmap alone, and its count keeps reporting the true total. Escape clears it, and the card menu offers Search unplaced... for a keyboard. The type filter now stays open across a pick and leads with Show all types and Hide all types, so "only the type I want" is two picks in one menu.

  • A toggle for the bucket grid on the horizon roadmap. A wide bucket reflows its cards into several columns, which is right for a backlog slice and wrong for a short list you are reading down. The toolbar now carries a toggle for it while the horizon axis is showing: press it and every bucket lists its cards one per row.

  • Every milestone in one row above the roster. On the resources axis a milestone is no longer filed under whoever is named on it — where folding that person's band took the date off screen — and one naming nobody no longer waits on the shelf.

  • One row per resource, whatever they have. An absence used to draw a blocked line of its own beneath its resource's header; it draws inside the header itself now, and two that share a day pack into their own sub-lanes rather than either one hiding the other.

  • Fold a board column or a horizon bucket — press the chevron in its header and the column narrows to a strip, keeping its name, its count and its ability to take a drop. The choice is remembered per saved view and per device, beside the rows you have collapsed, and never written to the .base. On a board the column's own context menu offers the same fold, which is the keyboard path to it.

  • A done column of finished work opens folded — the first time a board draws a done column holding finished work and nothing else, it starts shut, the same once-only default the tree applies to a parent nobody has ruled on. One still carrying open work opens normally, an empty one is left alone, and once you open a folded column it stays open. Horizon buckets have no such default: an axis has no notion of finished, so a bucket is open until you shut it.

    A running quick filter opens every fold, so a search can still find what is inside one.

  • How far along a roadmap bar is — a bar on the dated axis now carries a band showing the share of the work beneath it that is done, and every row with descendants carries the count the tree's rollup column shows.

  • The roadmap says what your search found underneath — filter the roadmap and any bucket card, bar, shelf card or context row that is only on screen because something beneath it matched now names those matches, each one opening its note.

  • Resize the tree's property columns — drag the grip at a column header's trailing edge, double click it to put it back, or focus it and use the arrow keys. Each column keeps its own width, per saved view and per device, beside the folded rows — so a title column and a risk chip no longer have to be the same size, and nothing about your working position is written to the .base file.

Changed

  • The board and the shelf only draw the cards you can see — a column, a horizon bucket and the shelf now let the browser skip the layout and paint of cards scrolled out of view, which the tree's rows have done since 0.7. Measured over ~800 notes in the development harness, switching to the board went from 330ms to 126ms and to the roadmap from 557ms to 203ms.

  • Recording an absence now asks for the dates alone.

  • State colours no longer offer a done state.

Fixed

  • Two rough edges on the shelf's own controls. The type filter's menu opened under the mouse the first time and then reopened at the button's edge after every pick, so the menu moved the moment it was used; it now opens in the same place both times. And the search box drew a border and a background inside the theme's own, giving the field a double outline; it is now a plain search field wearing whatever the theme gives one.

  • The pause after a write on a large backlog is gone. Every change used to redraw the whole tree, so on a vault of around eight hundred notes with the tree open, each move, each state change and each undo was followed by roughly half a second of nothing. The view now redraws only the rows that would look different — measured at about a third of the old cost per row, on the same expanded tree at every size tried. Three things switch it off, and on a vault that shows one of them nothing changes: a column that is not a note property (a file's modified time, a formula), a row whose cell draws a link or an embed rather than plain text, and a row whose file Obsidian has not finished indexing — each redraws as it always did, so nothing can go stale on screen.

  • One view saving can no longer forget every other view's working position. Saving tidies away entries for .base files the vault no longer has — and if the vault could not answer at that moment, every entry looked gone and every one was dropped. It now checks that the vault can see the base of the view doing the saving before it believes any of the answers.

  • Progress bars line up again in a big backlog. The bar and its count share a lane anchored at the right, and the count had a fixed reservation that only held two digits over two — so a row counting hundreds pushed its own bar left, and rows counting units, tens and hundreds each drew their bar in a different place.

  • Folding every resource row no longer reports the plan as empty. The roadmap counted the rows it had drawn, and a folded band draws none — so shutting the last open band answered with "all your items are done and hidden" beside the headers, counts and rails still on screen.

  • A bar drag or resize no longer loses its release. If the vault changed while a bar was in the air — which happens most often in the first minutes after a view is opened, while the query is still settling — the release could write nothing at all: the ghost showed the dates it meant, the bar snapped back, and nothing was said about it. The view now waits for the gesture to finish before it rebuilds.

  • Drawing a dependency no longer loses its release either. The wait above covered a bar's own moves and resizes but not the dependency connector: a vault change mid-draw took the preview line with it and the release wrote no dependency, silently. A link gesture now holds the rebuild back the same way.

  • A one-day absence crossing reads "1 day lost", not "1 days lost". The cost sentence in the crossing's tooltip and its screen-reader text now pluralizes like every other count on the roadmap.

  • The Property column width option is gone. The width is a per-column pick you drag now, kept on the device rather than in the shared .base

Removed

  • The card's right-click menu no longer offers Expand/Collapse unplaced.

  • The card's right-click menu no longer lists the children one by one — the Show/Hide children toggle stays, and so does the list on the card itself. A card with many children no longer pushes the rest of its menu off the screen.

What's Changed

Full Changelog: https://github.com/Luis85/backlog-view/compare/0.8.0...0.9.0

README file from

Github

Product Backlog — an Obsidian Bases view

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)

How it works

  • All items live flat in one folder — the hierarchy comes from properties, not subfolders.
  • A folder holds more than work items, so the view only shows the notes that belong to the hierarchy: a 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.
  • Each item is an ordinary markdown note. The view reads three frontmatter properties:
    • 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.
  • You never have to maintain these properties by hand. The view assigns them:
    • Creating an item via the view writes type, parent and order.
    • Dragging an item writes its new 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.
    • Items without a type show a level implied from their parent's type (a child of a Feature reads as a PBI, wherever that Feature sits).
    • The toolbar's ✨ Assign missing properties button sets the whole view up in one press: it picks this view's suggested property for every optional feature you have not configured yet — the workflow state, the date stamps, and the roadmap's horizon and dates — and then backfills 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.
  • Any of those writes can be taken back — Ctrl/Cmd+Z or the ↩ toolbar button, however many notes the change touched (see Undo).

Requirements

  • Obsidian 1.12.0 or newer (the Bases custom-view API, with the view options a base configures).
  • The Bases core plugin enabled.

Setup

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:

  1. Create a folder for your backlog, e.g. docs/.
  2. Create a Base (e.g. Product Backlog.base) and add a filter such as file.inFolder("docs").
  3. In the view switcher of the Base, add a new view and pick Product Backlog.
  4. Drop existing notes into the folder, or use + New Epic in the view to create items.
  5. Click the ✨ toolbar button once: it fills in the 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.

Using the view

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.

Focus on one type

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.

Folder-based backlogs

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:

  • An explicit 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).
  • Moving an item to the top level writes an empty 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.
  • A folder note is a parent, so every note below it counts as a backlog item even without a 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.
  • New child items are created in their parent note's folder.
  • If your domain folders also contain folder notes inside the filter, they become the top level — add a level name for them (e.g. Domain, Epic, Feature, PBI, Task).

Properties on a row

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.

Tags

When the property named by Tags property (tags by default) is one of the visible properties, its column becomes editable:

  • each tag renders as a pill; hover the row and click the on a pill to remove it,
  • the + at the end of the column opens the tags already used in this base, checked where the item carries them, plus New tag... for a free-text one (with autocomplete),
  • right-click → Edit tags offers exactly the same list, for the keyboard path.

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.

States and progress

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.

Keyboard

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

Undo

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.

Extra types sit beside the ladder

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:

  • They hang from any level above the lowest. Add one under an Epic, a Feature or a PBI. Their own children are always Tasks, so nothing is offered under one but a Task. They can also hang from nothing: the toolbar's type picker creates one at the top level, which is where an idea usually starts.
  • A move never re-types them. Dropping a Bug under an Epic leaves a Bug — where dropping a PBI there would make it a Feature. Their Tasks stay Tasks too, because the subtree follows the extra type rather than the rung it landed on.

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.

Where new items are filed

Everything the view creates lives under one home folder (docs by default), and each type gets its own folder pickerFolder 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:

  1. In folder mode, beside the parent's folder note — that mode makes folders the hierarchy, and a filing default should not quietly overrule it.
  2. The folder configured for the type being created.
  3. The home folder.
  4. The folder most existing items live in — only reachable by clearing the two above, since both are configured out of the box.
  5. Otherwise the modal asks, and remembers the answer as the home folder.

Filtered bases keep their tree

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:

  • they can't be dragged, moved, indented or outdented — the Base never returned their real siblings, so there is no sibling order to rank them within;
  • nothing ever writes into them. Their state chip is display-only, and the context menu drops Set type, Set state and the parent-link commands — a note the filter excluded is not yours to edit from a view that doesn't contain it. Re-ranking a sibling group also renumbers all of it when the gaps run out, so a group that contains a context row offers no reordering at all: no before/after drop, no Move up/down/to top/to bottom, no Outdent — even for an ordinary result row that happens to sit next to one. Dropping into a parent, dropping on the tree background and Indent keep working, because those append;
  • they don't influence where new notes go: the folder for new items is inferred from the Base's own results, never from ancestors that live somewhere else in the vault — and New <child> on a context row creates the note in that results folder rather than beside the excluded parent, so it doesn't vanish on the next refresh (its parent link still points at the right item);
  • they don't contribute workflow states: the state menu offers the values your results use, not one an excluded ancestor happens to carry;
  • they don't count. The item count, the per-level breakdown and the "N hidden" figure all describe what the Base returned; and a context row disappears as soon as nothing below it is visible, so hiding completed work never leaves empty scaffolding behind;
  • they stop the auto-type cascade. A filter can leave a context row between two results (the Epic and its PBI returned, the Feature between them not), and moving the item above it retypes only down to that row — its branch keeps the types it has, rather than being half-rewritten around a note that can't be touched;
  • they are valid drop targets, so you can drag a match onto its parent as usual, and New <child> works on them;
  • the ✨ backfill never writes properties into them;
  • they don't count anywhere: descendant counts and progress bars report the results the Base returned, so a context row in the middle of a chain is passed through rather than tallied, and its own state can't skew a rollup or keep a finished subtree on screen. (Children the filter excluded are still not counted — a rollup describes the visible subtree, not the whole backlog.)

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.

Large backlogs

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.

Where the view remembers things

Three different kinds of state, kept in three different places on purpose:

  • Everything in the view options — the properties, the levels, the focus level, the folder for new items — lives in the .base file. It describes the view itself, so it is shared with anyone you share the base with, and it travels with the vault.
  • Which rows you left open lives in this device's local storage, keyed per base and per view name. It is your working position rather than a property of the backlog: it would be noise in a shared file, and a path per collapsed row is growth that file should not take. So it survives restarts and stays out of everyone else's way.
  • What undo would put back lives only in memory, for as long as the view is open. It describes a change you just made, not the backlog, and the notes it refers to may be edited by anything in the vault meanwhile — so an undo offered after a restart would be a promise the plugin can't keep. Close the tab and the slot goes with it.

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.

Ranking details

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 board

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
  • Columns are the no-state column first, then Workflow states (in order) if you set it — or, left unconfigured, the states your notes actually carry plus a done column even if nothing is in it yet, when none of those already counts as done — and finally one more column per observed result value neither names, so a stray status still gets a column of its own rather than losing its card.
  • WIP limits are set per state in the view options — for every state except the done ones, since a finished column is a record rather than a queue and capping it would mean nothing. A limit reads the column's full population, not the filtered count, so narrowing the view cannot make an overcommitted stage look calm. It signals in colour, in shape and in words — and it refuses nothing. Going over a limit is information, not a locked door.
  • Policies are a sentence per configured workflow column, done ones included — the working agreement for that stage. Set one in the view options and it is readable from the column header and the column menu. A column minted from an observed value the workflow list doesn't name has no policy option and no menu entry for one.
  • Date stamps. Both 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.
  • Cards outside the base's filter appear only on a focused board: a focus-level item the filter excluded still gets an inert card, so its results have somewhere to sit. Unfocused, the board is results only — an excluded item never gets a card without a focus level pointing at it. Either way, a context card carries no control that would write to it.
  • 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.

The Deliverables board

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 roadmap

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.

View options

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:

  • The 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.
  • Ignore notes outside the hierarchy decides what counts as a backlog item. A note qualifies when its 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).
  • Group by is ignored — the hierarchy is the grouping. The toolbar says so when a group-by is configured.
  • A Base limit truncates the result set, which can drop parents while keeping their children. Their ancestors are loaded back in as context rows (see above); the counts and rollups on those rows still describe only what the Base returned, so prefer filters over limits for backlogs.
  • Creating an item from a focused view's toolbar makes it top-level (parentless) at that level; assign a parent afterwards by dragging it into place.
  • Items whose 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.
  • When the view is empty and no folder is configured, creating the first item asks for the target folder (with autocomplete) and saves the choice to the view options.

Installation

In Obsidian: SettingsCommunity pluginsBrowse, 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:

  1. Download main.js, manifest.json and styles.css from the latest release (or build them yourself, see below).
  2. Copy them to <your vault>/.obsidian/plugins/product-backlog-view/.
  3. Reload Obsidian and enable Product Backlog under Community plugins.

To track unreleased builds, install via BRAT with the repository URL.

Development

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

Trying a build

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.

License

MIT