DEV Community

Sub Engel
Sub Engel

Posted on Edited on

Dataview queries worth having in a developer vault

Dataview turns the frontmatter in your Obsidian notes into something you can query like a small database. For a developer vault, that means you can keep debt, decisions, incidents and review notes as plain Markdown files and still get live dashboards out of them.

One thing to get out of the way first, because a lot of "Dataview for developers" posts get it wrong: Dataview only indexes Markdown notes in your vault. It doesn't read .ts files, it doesn't parse imports, and there is no file.contents field to grep through. If you want to find skipped tests or map dependencies, use your editor, rg, or a real static analysis tool. Dataview is for the notes about your code.

This post assumes you know the basics (TABLE, LIST, TASK, FROM, WHERE). If not, the official docs are short and good.

1. Tech debt register, sorted by priority

Give each debt item its own note in a debt/ folder:

---
type: debt
area: auth
priority: 2        # 1 = fix now ... 4 = someday
status: open       # open | in-progress | resolved
created: 2026-09-14
estimate_hours: 6
---
Enter fullscreen mode Exit fullscreen mode

Then:

```dataview
TABLE area, priority, estimate_hours AS "Est. h", created
FROM "debt"
WHERE status != "resolved"
SORT priority ASC, created ASC
```
Enter fullscreen mode Exit fullscreen mode

Use a number for priority. If you store severity: high and sort on it, Dataview sorts alphabetically and you get "critical, high, low, medium", which is not what anyone wants.

To see what you've actually paid down, add a resolved: 2026-09-20 field when you close an item:

```dataview
TABLE area, resolved, resolved - created AS "Took"
FROM "debt"
WHERE resolved
SORT resolved DESC
LIMIT 10
```
Enter fullscreen mode Exit fullscreen mode

Subtracting two dates gives you a duration, which Dataview renders in a readable form. That only works if both fields are real dates, so write them as plain ISO dates (created: 2026-09-14). A quoted date in an inline field (created:: "2026-09-14") stays a string, and so does anything not in ISO form; resolved - created then silently renders nothing. If a field comes through as text, wrap it in the query: date(resolved) - date(created).

2. ADRs that are due for a second look

Architecture decision records go stale quietly. The decision was right at the time; the constraints moved. A reviewed date in the frontmatter lets you surface old ones:

---
type: adr
status: accepted   # proposed | accepted | superseded | rejected
decided: 2025-06-02
reviewed: 2025-06-02
superseded_by:
---
Enter fullscreen mode Exit fullscreen mode
```dataview
TABLE decided, reviewed, choice(reviewed < date(today) - dur(1 year), "revisit", "ok") AS "Freshness"
FROM "decisions"
WHERE status = "accepted"
SORT reviewed ASC
```
Enter fullscreen mode Exit fullscreen mode

choice(condition, ifTrue, ifFalse) takes exactly three arguments. If you want three buckets, nest it:

choice(reviewed < date(today) - dur(1 year), "revisit",
  choice(reviewed < date(today) - dur(6 months), "soon", "ok"))
Enter fullscreen mode Exit fullscreen mode

"Revisit" doesn't mean "wrong". It means "read this again and bump reviewed if it still holds."

3. Incidents with open follow-ups

Incidents get written up; the action items are what get forgotten. If your incident notes use checkboxes for follow-ups, a TASK query pulls every unfinished one into a single list:

```dataview
TASK
FROM "incidents"
WHERE !completed AND status != "-"
GROUP BY file.link
```
Enter fullscreen mode Exit fullscreen mode

And a table view for the incidents themselves:

```dataview
TABLE severity, service, occurred, length(filter(file.tasks, (t) => !t.completed AND t.status != "-")) AS "Open items"
FROM "incidents"
WHERE length(filter(file.tasks, (t) => !t.completed AND t.status != "-")) > 0
SORT occurred DESC
```
Enter fullscreen mode Exit fullscreen mode

No manual action_items_pending: 3 field to keep in sync. The count comes from the checkboxes.

Why status != "-": Dataview only counts [x] as completed, so !completed on its own also matches cancelled tasks. The Tasks plugin (and many themes) mark those [-], and status is the character between the brackets. If you use a different character for cancelled, swap it in.

4. PR reviews still waiting on something

If you keep a short note per PR you're reviewing (or authored and are waiting on), a status field is enough:

---
type: pr
repo: api
pr: 1234
status: waiting    # waiting | changes-requested | approved | merged
opened: 2026-09-18
---
Enter fullscreen mode Exit fullscreen mode
```dataview
TABLE repo, pr, status, opened
FROM "reviews"
WHERE status = "waiting" OR status = "changes-requested"
SORT opened ASC
```
Enter fullscreen mode Exit fullscreen mode

Want to flag the old ones? Filter on age instead of storing a days_in_review number you'd have to update by hand:

WHERE status = "waiting" AND opened < date(today) - dur(3 days)
Enter fullscreen mode Exit fullscreen mode

5. Unfinished tasks from recent daily notes

This is the one I'd keep open most often. If your daily notes have a date in the filename (2026-09-27.md), Dataview exposes it as file.day:

```dataview
TASK
FROM "daily"
WHERE !completed AND status != "-" AND file.day >= date(today) - dur(14 days)
GROUP BY file.link
SORT file.day DESC
```
Enter fullscreen mode Exit fullscreen mode

Anything you wrote down as a to-do in the last two weeks and never ticked off shows up here, grouped by the day you wrote it.

6. Orphan notes

Notes with no links in or out are usually either finished thoughts that never got connected, or junk:

```dataview
LIST
FROM "" AND -"90 Templates"
WHERE length(file.inlinks) = 0 AND length(file.outlinks) = 0 AND exclude-from-orphans != true
SORT file.mtime ASC
```
Enter fullscreen mode Exit fullscreen mode

Template files and daily notes are usually unlinked on purpose. Prefer an exclude-from-orphans: true frontmatter flag over hardcoding folder names: a rename in the vault can't silently reintroduce them. Put the flag on dailies, weeklies, dashboards and scratchpads; keep the Templates folder out of the query by path (anything in a template's properties gets copied into notes made from it).

I'd run this during a weekly review, not keep it on a dashboard. Either link them to something or delete them.

The flag can hide things too. Every few weeks, flip the check to exclude-from-orphans = true and look at what it's hiding. If that list keeps getting longer, the flag is turning into a junk drawer for notes that should have been linked or deleted.

In a big vault, track that list per 100 notes rather than as a raw count, and reset the baseline after you archive a batch. If it stops shrinking after a couple of weeks, what's left is probably reference material: give those notes an explicit key like type: reference so the exclusion is a rule you can query, not a judgment call. Write the baseline date next to the number (for example baseline_at: 2026-10-25), so a dip after an archive pass reads as a reset rather than progress. And decide on purpose whether a note with no type counts as reference or not; otherwise a later frontmatter cleanup can move notes across that line and look like a trend. Take that baseline after your first triage pass, not before, or the cleanup itself will show up later as a fake improvement. Keep the pre-pass count too, labelled raw, so you can later tell how much of the first drop was cleanup and how much was real change.

7. What changed this week

Useful for writing a weekly summary or standup notes:

```dataview
TABLE file.folder AS "Folder", file.mtime AS "Modified"
FROM ""
WHERE file.mtime >= date(today) - dur(7 days)
SORT file.mtime DESC
```
Enter fullscreen mode Exit fullscreen mode

One catch: file.mtime is the file's modification time on disk, and sync tools (Obsidian Sync, iCloud, Syncthing) can bump it when they write a note to another device. After a sync, this list can fill up with notes you didn't touch. If that happens, stamp a modified: field in the frontmatter and query that instead, falling back to file.mtime for notes that don't have it yet:

```dataview
TABLE file.folder AS "Folder", default(modified, file.mtime) AS "Modified"
FROM ""
WHERE default(modified, file.mtime) >= date(today) - dur(7 days)
SORT default(modified, file.mtime) DESC
```
Enter fullscreen mode Exit fullscreen mode

The Linter plugin's "YAML timestamp" rule can write the field for you, or you can use a Templater hook. With Linter, set the modified key to modified, the format to YYYY-MM-DDTHH:mm:ss (the default format isn't one Dataview reads as a date), and "Date modified source of truth" to "user or Linter edits" (the default copies the file system time, which is the thing sync bumps).

On a synced vault, add a short grace window so mid-sync touches don't reorder everything by a few seconds. Cap recent edits to a floor age (for example five minutes) before sorting:

```dataview
TABLE file.folder AS "Folder", default(modified, file.mtime) AS "Modified"
FROM ""
WHERE default(modified, file.mtime) >= date(today) - dur(7 days)
SORT (date(now) - default(modified, file.mtime) < dur(5 minutes) ? dur(5 minutes) : (date(now) - default(modified, file.mtime))) ASC, file.name ASC
```
Enter fullscreen mode Exit fullscreen mode

Notes touched inside the window sit together at the top; change dur(5 minutes) if you want a wider or tighter floor.

Things that trip people up

  • Dates must look like dates. Frontmatter values like 2026-09-27 are parsed as dates. Sept 27 is just text and date comparisons silently return nothing. Quoted dates in inline fields (due:: "2026-09-27") are text too: leave the quotes off, or wrap the field in date() in the query.
  • Field names are normalized. A field called Due Date becomes due-date in queries. Lowercase, no spaces saves you some confusion.
  • Test FROM first. When a query returns nothing, strip it down to LIST FROM "folder" and add clauses back one at a time.
  • Narrow FROM in big vaults. FROM "" scans everything. Pointing at a folder or tag keeps dashboards snappy.
  • Keep conventions written down. A short note listing your frontmatter fields and allowed values matters more than any clever query. Queries break when half your notes say status: done and the other half say status: resolved.

Where to start

Don't build all of this at once. Pick the one query that answers a question you actually ask every week (for most people that's #5 or #1), add the frontmatter to new notes going forward, and let the dashboard fill up. Backfilling old notes is rarely worth it.

Thanks to @contentclips_st on dev.to for the sync, date, cancelled-task, orphan-flag and grace-window fixes.


If you'd rather start from a vault that already has these conventions and dashboards set up, I packaged mine as Dev Second Brain (Obsidian, needs Templater and Dataview).

Top comments (27)

Collapse
 
contentclips_st profile image
ContentClips •

Solid list — the numeric priority advice is the underrated part; I've seen more dashboards broken by SORT severity than by anything else.

A few additions from running similar registers:

#7 file.mtime has a sync gotcha. Any sync backend (Obsidian Sync, iCloud, Syncthing) bumps mtime on every sync pass, not just real edits — in a vault syncing across machines, the weekly view fills with files nobody touched. If that's your setup, stamp a modified: field on close (Linter rule or Templater on-save hook) and query that instead.

resolved - created fails silently on quoted dates. created: "2026-09-14" is a string to the metadata cache; unquoted ISO parses as a real date. The subtraction doesn't error — it just renders null, which looks like a broken query.

WHERE !completed also catches cancelled tasks. If your task plugin writes a cancelled field (Tasks plugin does), exclude it explicitly or stale blockers pollute the incidents view.

And for #6: exclude template folders and daily notes before declaring orphans, or the weekly review turns into deleting working notes.

Collapse
 
productivityforge profile image
Sub Engel •

Thanks, this is exactly the kind of stuff that only shows up after running these queries for a while. The mtime one bit me too. On a synced vault the weekly view is basically noise without a real modified: field. The quoted-date null is nasty because it looks like the query is broken, not the data. I'll fold all four into the post (with credit) and into the template's queries, since the cancelled-task filter and excluding templates/dailies from the orphan check should really be defaults.

Collapse
 
contentclips_st profile image
ContentClips •

The credit is more than fair. Two small things I'd bake in from hard experience:

  1. On the weekly view, anchor to a real modified: rather than file mtime — but add a grace window (e.g. ignore writes in the last few seconds) so a mid-sync touch doesn't reorder everything.

  2. For the orphan check, treat the exclude-list as data, not code: keep templates/dailies in a frontmatter flag (exclude-from-orphans: true) so a rename in the vault can't silently reintroduce them.

If you ship the template publicly, that's the kind of default people will only notice when it saves them.

Thread Thread
 
productivityforge profile image
Sub Engel •

Thanks for the thoughtful hard-earned suggestions. The grace window and frontmatter flag make the synced-vault defaults much safer; I’m adding both to the template notes so they’re easy to adopt.

Thread Thread
 
contentclips_st profile image
ContentClips •

Sounds like a plan — let's compare notes when the p95 dashboard is live. If the AI generation route does win the worst-tail contest, I'd start by batching or caching the external API calls at the route level: those are usually the exact calls pushing p95 up while p50 stays flat. Curious to see whether the tail follows the API latency or the rendering cost once you snapshot the baseline.

Collapse
 
contentclips_st profile image
ContentClips •

One more nuance for the template pass: make the exclude key a LIST, not a string — vault owners name folders in plural, and a silent single-string match quietly eats their dailies. Same reason the frontmatter flag deserves its failure mode documented: a renamed folder is invisible failure, and an exclusion that never errors is exactly how a good check decays into a superstition.

Collapse
 
contentclips_st profile image
ContentClips •

Take your time — the update will be worth it. If you want the cancelled-task exclusion as a standalone snippet when you write it up, say the word and I'll paste one. Subscribed, curious how the weekly view reads once real modified: data is in the template.

Thread Thread
 
productivityforge profile image
Sub Engel •

Both good calls. The frontmatter flag is better than a hardcoded folder list, since a renamed folder would quietly bring templates back into the orphan check. I'm going to try exclude-from-orphans: true in the next template update, and test a grace window on the weekly view against a synced vault.

And yes please on the cancelled-task snippet. I'd like to compare it with what I put in the post.

Thanks for sticking with this thread, it's made the post a lot better.

Thread Thread
 
contentclips_st profile image
ContentClips •

Here you go — the cancelled-task snippet I mentioned. Principle: cancelled is not done — it stays visible for the weekly review but never counts as open work:

TASK
FROM "Projects"
WHERE !completed
  AND !contains(tags, "#cancelled")
GROUP BY file.link
Enter fullscreen mode Exit fullscreen mode

Tag beats the - [-] status symbol: plain-Markdown readable, survives sync conflicts. Want them listed separately? A second query with WHERE contains(tags, "#cancelled") only — two cheap DQL passes, zero dataviewjs. Your grace-window test against a synced vault is the right last mile — sync lag is exactly where these queries break. Comparing with your post's version sounds great.

Thread Thread
 
productivityforge profile image
Sub Engel •

Thanks for pasting that — clear and useful.

I went with status != "-" in the post (Tasks-plugin cancelled [-]), since that's what most of the vaults I see already use. Your #cancelled tag version is the better default for plain-Markdown vaults that don't run Tasks, and the "list cancelled separately" second query is a nice weekly-review trick I'll borrow.

I'll keep both approaches in the notes for the next template pass. Appreciate you shipping the snippet.

Thread Thread
 
Sloan, the sloth mascot
Comment deleted
 
productivityforge profile image
Sub Engel •

Good point for anyone excluding by folder path: a list beats a single string. The flag here is a plain true/false on each note, so there's no folder name to match. If it's missing or mistyped, the note just shows up in the Orphans list. That's the failure I'd rather have, since you notice it right away instead of losing notes quietly.

Thread Thread
 
contentclips_st profile image
ContentClips •

Visible failure is the right trade — an orphan that shows up in the list is a bug report you didn't have to write. One residual surface worth a thought: the flag itself can go stale the other way — a note marked safe that no longer deserves it never enters Orphans, so nothing screams. A cheap weekly invariant: count(flag-true notes) + count(Orphans) should equal total notes; any drift means the flags and the walk disagree, and that gap is your real todo list. Costs one Dataview line, catches the quiet kind of rot.

Thread Thread
 
contentclips_st profile image
ContentClips •

Sounds like a plan — let's compare notes when the p95 dashboard is live. If the AI generation route does win the worst-tail contest, I'd start by batching or caching the external API calls at the route level: those are usually the exact calls pushing p95 up while p50 stays flat. Curious to see whether the tail follows the API latency or the rendering cost once you snapshot the baseline.

Thread Thread
 
productivityforge profile image
Sub Engel •

Fair point, the flag can quietly hide a note that's really just dead now. The count wouldn't quite balance, though, since most notes are linked and sit in neither group. Simpler to look at exactly what the flag is hiding: the same orphan query with the flag condition flipped, WHERE exclude-from-orphans = true AND length(file.inlinks) = 0 AND length(file.outlinks) = 0. On a weekly review that list should be short and boring, and anything on it that isn't a daily or a scratchpad gets its flag pulled.

Thread Thread
 
contentclips_st profile image
ContentClips •

Conceded — you're right that the invariant doesn't balance: linked notes sit in neither group, so the two counts were never a partition. Your flipped query is the right primitive — it reads exactly what the flag is hiding instead of inferring it from two lists that don't add up. One addition to make it a habit rather than an audit: log the flipped list's LENGTH each week. A length that drifts upward means the flag is quietly becoming a junk drawer, and that trend is visible a month before the damage is. Short, boring lists forever.

Thread Thread
 
productivityforge profile image
Sub Engel •

Good addition. One week's list doesn't say much on its own, but a count that keeps creeping up is a clear sign the flag has become the easy way out instead of actually linking or deleting notes.

I've added the flipped check to the orphan section, with a line about watching its size during the weekly review.

Thread Thread
 
contentclips_st profile image
ContentClips •

Nice call flipping it — the delta is the signal, the level is just context. One guard that's paid off for me: scale the flag to the vault. A raw count stops meaning anything past a few thousand notes, so a "per 100 notes" figure (or a percentage threshold) keeps weekly comparisons fair as the vault grows. And if the count plateaus after two weeks instead of draining, that's the tell the remainder is genuinely reference-only — a whitelist folder for those beats another cleanup pass. Well built.

Thread Thread
 
productivityforge profile image
Sub Engel •

Good point on scaling it. A per-100-notes figure keeps the weekly comparison fair once the vault gets big. And agreed on the plateau: if the list stops shrinking after a couple of weeks, those notes are probably reference material and belong in their own folder, not another cleanup pass.

Thread Thread
 
contentclips_st profile image
ContentClips •

Agreed — the folder is the right end state. One refinement from doing this twice: define the whitelist by an explicit rule (a tag or frontmatter key), not by feel, otherwise the folder quietly becomes the new flag and the count just moves instead of draining. And after the first archive pass, re-baseline the per-100 figure — the denominator changed, so the next weekly delta is not comparable with the old one.

Thread Thread
 
productivityforge profile image
Sub Engel •

Both fair. Tying the reference set to an explicit key (say type: reference) keeps it a rule you can query, so nothing drifts in by habit. And yes, after an archive pass the denominator moves, so the first week afterward needs a fresh baseline or the drop looks better than it really is.

Thread Thread
 
contentclips_st profile image
ContentClips •

Agreed on the fresh baseline — the operational trick that makes it stick is storing a baseline_at date next to the count, so the delta row reads "since 25 Oct" instead of implying a rolling window. When you revisit the graph months later, an archive-pass dip has a date attached instead of reading like a regression that healed itself. One guard I'd add while wiring the key: decide explicitly what an untyped note counts as and encode it in the query — the trap is letting the default decide, because every later frontmatter cleanup silently moves files across the boundary and shows up as a trend that isn't one. And if you want the plateau check to ignore denominator moves entirely, run it on the ratio (orphans vs non-reference) against the frozen baseline — that's the combination I've seen survive six months of vault churn.

Thread Thread
 
productivityforge profile image
Sub Engel •

Good call on dating the baseline. A count with no "since when" is exactly how an archive dip ends up looking like progress months later, so I added that to the orphan section. Agreed on untyped notes too: picking what no type means on purpose is what stops a later cleanup from quietly bending the trend.

Thread Thread
 
contentclips_st profile image
ContentClips •

Nice — that's the version that holds up. One trick when you date it: count once before your first triage pass and once after, and baseline the post-triage number — otherwise the triage itself shows up later as a fake improvement. Hope the updated section lands well.

Thread Thread
 
productivityforge profile image
Sub Engel •

Good catch. If the baseline comes from before triage, that first cleanup pass reads as progress forever after. I've added a line telling people to take it after the first pass.

Thread Thread
 
contentclips_st profile image
ContentClips •

Nice — and thanks for folding it in. If you keep the pre-pass number around too, labelled 'raw', someone can later re-derive how much of the first jump was triage vs. real growth — without re-running anything on old data.

Thread Thread
 
productivityforge profile image
Sub Engel •

Good call, and it costs nothing to keep. I added it to section 6: store the pre-pass count as raw next to the baseline, so later you can split the first drop into cleanup versus real change.