<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sub Engel</title>
    <description>The latest articles on DEV Community by Sub Engel (@productivityforge).</description>
    <link>https://dev.to/productivityforge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4146089%2F2fb3c554-58cc-479b-92cd-b9527d47f4b0.png</url>
      <title>DEV Community: Sub Engel</title>
      <link>https://dev.to/productivityforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/productivityforge"/>
    <language>en</language>
    <item>
      <title>Writing docs that survive framework churn</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:15:18 +0000</pubDate>
      <link>https://dev.to/productivityforge/writing-docs-that-survive-framework-churn-1j70</link>
      <guid>https://dev.to/productivityforge/writing-docs-that-survive-framework-churn-1j70</guid>
      <description>&lt;p&gt;Most of us have opened a project's docs, followed the setup steps, and hit a wall by step three. The CLI flag was renamed, the config file moved, the recommended package is deprecated. Usually the explanation of &lt;em&gt;how the system works&lt;/em&gt; in that same document is still mostly right. But because it sits next to steps that are visibly broken, you stop trusting any of it.&lt;/p&gt;

&lt;p&gt;That's the core problem: docs contain information with very different lifespans, and we tend to mix it all together. Separate it, and most of the rot becomes contained and easy to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort information by how fast it goes stale
&lt;/h2&gt;

&lt;p&gt;Roughly four layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why (slow to change).&lt;/strong&gt; Principles, architecture, trade-offs, the reasoning behind big decisions. "We keep business logic out of route handlers so it can be tested without HTTP." This stays true across framework rewrites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns (changes occasionally).&lt;/strong&gt; How we usually do things. "Validate input at the edge, pass typed objects inward." "Every async UI state is one of idle, loading, success, error." The details shift; the idea holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How-to (changes with major versions).&lt;/strong&gt; Setup, deployment, "how to add a new endpoint". Tied to specific tools and versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference (changes every release).&lt;/strong&gt; CLI flags, config schemas, API endpoints. Ideally generated from the code, not written by hand.&lt;/p&gt;

&lt;p&gt;The mistake is putting all four in one README section. A single framework upgrade then makes the whole thing look abandoned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep "why" and "how" in separate documents
&lt;/h2&gt;

&lt;p&gt;A typical setup doc mixes both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Getting started&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`npm install`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Run &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt;
&lt;span class="p"&gt;3.&lt;/span&gt; Open http://localhost:3000

The app uses the Next.js App Router, so pages live in &lt;span class="sb"&gt;`app/`&lt;/span&gt; and data fetching happens in server components...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the project moves to a different framework, or even changes its dev port, every line here is suspect.&lt;/p&gt;

&lt;p&gt;Split it instead. The architecture doc explains the idea and links to the current implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Architecture: rendering and data loading&lt;/span&gt;

Pages render on the server by default and fetch their own data, so the client bundle
stays small and there's no separate API layer for page data. Interactive pieces are
isolated client components.

Current implementation: Next.js App Router. See &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Setup (Next.js)&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./how-to/setup-next.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
Previous approach: see &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;archive/pages-router.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./archive/pages-router.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the setup doc is openly version-specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Setup (Next.js)&lt;/span&gt;

Last verified: 2026-09-01 with Node 24, Next.js 16.
&lt;span class="p"&gt;
1.&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the framework changes, you rewrite the setup doc and change one line in the architecture doc. The explanation, which is the expensive part to write, survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe contracts, not syntax
&lt;/h2&gt;

&lt;p&gt;The same idea works at the level of individual examples. Instead of documenting "here's how we use &lt;code&gt;useState&lt;/code&gt; for errors", document the shape of the thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AsyncState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then explain the rules in plain language: a retry moves from &lt;code&gt;error&lt;/code&gt; back to &lt;code&gt;loading&lt;/code&gt;, so stale errors never show next to fresh data. That holds whether the implementation is React hooks, a state machine library, or something that doesn't exist yet. Link to the current implementation for the framework-specific part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a date and versions on anything that can go stale
&lt;/h2&gt;

&lt;p&gt;Every how-to page gets a line at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Last verified: 2026-09-01 · Node 24 · pnpm 10 · Next.js 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two things. Readers can judge at a glance whether to trust the page. And you get an obvious list of what to re-check when you bump a major dependency: search for the old version number.&lt;/p&gt;

&lt;p&gt;If your docs have frontmatter, make it a field (&lt;code&gt;last_verified: 2026-09-01&lt;/code&gt;) so a script can list pages that haven't been verified in, say, six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deprecate instead of deleting
&lt;/h2&gt;

&lt;p&gt;When a doc is outdated but some code still depends on it, don't delete it. Mark it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt; **Deprecated (2026-03).** This describes the Create React App setup.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; New projects: see [Setup (Vite)](./setup-vite.md).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Kept for the legacy admin app, which still uses CRA.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New readers get sent to the right place immediately, people maintaining old code still have what they need, and you have an explicit list of docs to remove once the legacy code is gone. Moving them into an &lt;code&gt;archive/&lt;/code&gt; folder with the banner intact works too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record decisions where the docs can link to them
&lt;/h2&gt;

&lt;p&gt;Big "why" questions (why this database, why a monorepo, why this auth model) deserve short decision records: context, options considered, what was chosen, consequences. Keep them in the repo, link to them from the architecture docs, and mark them superseded rather than deleting them when things change. The architecture doc then stays short, and the detailed reasoning is one click away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review on a schedule, triggered by events
&lt;/h2&gt;

&lt;p&gt;Different layers need different attention. A starting point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;When to review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture / why&lt;/td&gt;
&lt;td&gt;Once a year, or when the architecture actually changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patterns&lt;/td&gt;
&lt;td&gt;A few times a year, or when the team's habits shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How-to / setup&lt;/td&gt;
&lt;td&gt;On every major dependency or tooling upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;td&gt;Every release (automate it if you can)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated docs&lt;/td&gt;
&lt;td&gt;When the legacy code they support is removed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The event-based triggers matter more than the calendar. Add "update the setup doc" to the checklist for dependency upgrades, and "does this PR change how someone uses the system?" to your review habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let tooling catch the mechanical rot
&lt;/h2&gt;

&lt;p&gt;Some rot is easy to detect automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broken links.&lt;/strong&gt; Run a link checker like &lt;a href="https://github.com/lycheeverse/lychee" rel="noopener noreferrer"&gt;lychee&lt;/a&gt; or &lt;a href="https://github.com/tcort/markdown-link-check" rel="noopener noreferrer"&gt;markdown-link-check&lt;/a&gt; in CI. Internal links break every time someone renames a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code examples drifting from code.&lt;/strong&gt; Where possible, embed examples from real source or test files instead of pasting them, or keep examples as small tests that run in CI. An example that's compiled is an example that's still true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale pages.&lt;/strong&gt; A small scheduled script that reads &lt;code&gt;last_verified&lt;/code&gt; from frontmatter and opens an issue for anything older than six months turns review into a to-do list instead of a good intention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned doc sites.&lt;/strong&gt; If users run old versions of your software, publish docs per version. Docusaurus has built-in &lt;a href="https://docusaurus.io/docs/versioning" rel="noopener noreferrer"&gt;versioning&lt;/a&gt;; for MkDocs, &lt;a href="https://github.com/jimporter/mike" rel="noopener noreferrer"&gt;mike&lt;/a&gt; is the common choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Give each layer an owner
&lt;/h2&gt;

&lt;p&gt;"Everyone owns the docs" means nobody does. Even on a small team, assign names: someone reviews the architecture docs yearly, whoever does a major upgrade updates the setup docs, reference docs are generated as part of the release. On a solo project the owner is you, but writing down &lt;em&gt;when&lt;/em&gt; you'll review each layer still helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;You don't need to restructure everything. Start with the next doc you touch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the "why" paragraphs out of setup instructions into their own page.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;Last verified&lt;/code&gt; line with versions to every how-to you edit.&lt;/li&gt;
&lt;li&gt;Add a link checker to CI; it takes ten minutes and catches real problems immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over time the pages that explain your system stop being dragged down by the pages that describe this month's commands, and the parts that do go stale are small, dated, and obvious to fix.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Linking decision records to git commits</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:09:21 +0000</pubDate>
      <link>https://dev.to/productivityforge/linking-decision-records-to-git-commits-1f3a</link>
      <guid>https://dev.to/productivityforge/linking-decision-records-to-git-commits-1f3a</guid>
      <description>&lt;p&gt;Six months after a change, &lt;code&gt;git log&lt;/code&gt; tells you &lt;em&gt;what&lt;/em&gt; happened and &lt;code&gt;git blame&lt;/code&gt; tells you &lt;em&gt;who&lt;/em&gt;. Neither tells you why you picked this approach over the two you rejected. That reasoning usually lived in a meeting, a chat thread, or your head.&lt;/p&gt;

&lt;p&gt;Architecture decision records (ADRs) fix half of this by writing the reasoning down. The other half is linking each decision to the commits that carried it out, so you can go from code to reasoning and back. This post shows a setup that uses nothing more exotic than a notes folder, a naming convention, and git's own features.&lt;/p&gt;

&lt;p&gt;I'll use Obsidian for the notes, but everything here works with any folder of Markdown files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a decision
&lt;/h2&gt;

&lt;p&gt;Not every commit needs one. I'd write a decision note when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there were real alternatives and you rejected some of them,&lt;/li&gt;
&lt;li&gt;the choice is expensive to reverse (a database, a framework, an auth model, a public API shape), or&lt;/li&gt;
&lt;li&gt;you can imagine someone (including you) asking "why did we do it this way?" later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Renaming a variable doesn't qualify. Switching your session store does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Give every decision a stable ID
&lt;/h2&gt;

&lt;p&gt;The ID is the glue, so it has to be boring and predictable. A date plus a short slug works well and sorts nicely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;decisions/adr-20260927-session-store.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filename &lt;em&gt;is&lt;/em&gt; the ID. Here's a Templater template that creates the frontmatter and structure (save it as &lt;code&gt;templates/decision.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.file.title %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proposed&lt;/span&gt;   &lt;span class="c1"&gt;# proposed | accepted | superseded | rejected&lt;/span&gt;
&lt;span class="na"&gt;decided&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.date.now("YYYY-MM-DD") %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.date.now("YYYY-MM-DD") %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;superseded_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# &amp;lt;% tp.file.title %&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
What problem, what constraints, what forced a decision now.

&lt;span class="gu"&gt;## Options considered&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Option A**&lt;/span&gt;: what it is. Pros / cons.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Option B**&lt;/span&gt;: what it is. Pros / cons.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;
We chose ... because ...

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;
What gets easier, what gets harder, what we're now committed to.

&lt;span class="gu"&gt;## Commits&lt;/span&gt;
Filled in from git, see below.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the note with a filename like &lt;code&gt;adr-20260927-session-store&lt;/code&gt;, apply the template, fill it in. It takes ten minutes when the decision is fresh and an hour of archaeology if you wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Reference the ID in commits with a trailer
&lt;/h2&gt;

&lt;p&gt;Git has a built-in convention for structured lines at the end of a commit message, called trailers (&lt;code&gt;Signed-off-by:&lt;/code&gt; is the best-known one). Use one for decisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Replace JWT refresh flow with server-side sessions

Refresh-token rotation was racing across browser tabs.
Sessions now live in Redis with a 30-day sliding expiry.

Decision: adr-20260927-session-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add the trailer from the command line too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Move session reads to Redis"&lt;/span&gt; &lt;span class="nt"&gt;--trailer&lt;/span&gt; &lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;--trailer&lt;/code&gt; needs git 2.32 or newer.)&lt;/p&gt;

&lt;p&gt;If you want a reminder, add a commit template to the repo and point git at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitmessage in the repo root&lt;/span&gt;
&lt;span class="c"&gt;# Subject line (~50 chars)&lt;/span&gt;

&lt;span class="c"&gt;# Why this change?&lt;/span&gt;

&lt;span class="c"&gt;# Decision: adr-YYYYMMDD-slug   (delete if not applicable)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config commit.template .gitmessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lines starting with &lt;code&gt;#&lt;/code&gt; are stripped from the final message, so the hints cost nothing.&lt;/p&gt;

&lt;p&gt;Forgot the trailer on a commit that's already pushed? You don't have to rewrite history. &lt;code&gt;git notes&lt;/code&gt; can attach the ID after the fact (thanks to shieldx in the comments for the nudge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git notes add &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
git log &lt;span class="nt"&gt;--notes&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two catches: &lt;code&gt;--grep&lt;/code&gt; only searches notes when you pass &lt;code&gt;--notes&lt;/code&gt;, so add it to any &lt;code&gt;git log --grep&lt;/code&gt; in this post if you use notes. And notes don't travel with a normal push or clone, so share them with &lt;code&gt;git push origin refs/notes/commits&lt;/code&gt; and pull them with &lt;code&gt;git fetch origin refs/notes/commits:refs/notes/commits&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Querying the link from the git side
&lt;/h2&gt;

&lt;p&gt;This is where the trailer pays off. No script needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every commit that implemented a given decision&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;

&lt;span class="c"&gt;# All commits that reference any decision, with the ID shown&lt;/span&gt;
git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%h %s  [%(trailers:key=Decision,valueonly,separator=%x2C)]'&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"^Decision:"&lt;/span&gt;

&lt;span class="c"&gt;# Which decision explains this line? blame first, then read the commit&lt;/span&gt;
git blame &lt;span class="nt"&gt;-L&lt;/span&gt; 40,60 src/auth/session.ts
git show &amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="c"&gt;# the trailer is at the bottom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command prints each commit with its decision ID in brackets, which makes a decent audit log on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Catch typos with a commit-msg hook
&lt;/h2&gt;

&lt;p&gt;Links are only useful if they point at something real. A small &lt;code&gt;commit-msg&lt;/code&gt; hook rejects IDs that don't match a note. (It must be &lt;code&gt;commit-msg&lt;/code&gt;, not &lt;code&gt;pre-commit&lt;/code&gt;: only &lt;code&gt;commit-msg&lt;/code&gt; receives the message file as &lt;code&gt;$1&lt;/code&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/commit-msg  (or .husky/commit-msg)&lt;/span&gt;
&lt;span class="nv"&gt;DECISIONS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docs/decisions"&lt;/span&gt;   &lt;span class="c"&gt;# adjust to where your notes live&lt;/span&gt;

&lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'^Decision: adr-[0-9]{8}-[a-z0-9-]+'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^Decision: //'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$ids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"commit-msg: no decision note found for '&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
done
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable (&lt;code&gt;chmod +x&lt;/code&gt;). It uses &lt;code&gt;grep -E&lt;/code&gt; rather than &lt;code&gt;grep -P&lt;/code&gt; so it also works with the BSD grep on macOS.&lt;/p&gt;

&lt;p&gt;If your vault is a separate repo, point &lt;code&gt;DECISIONS_DIR&lt;/code&gt; at its checkout path, or drop the hook and live with the occasional typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 (optional): Show commits inside the note
&lt;/h2&gt;

&lt;p&gt;Going from commit to decision is covered by git. Going from decision to commits is covered by &lt;code&gt;git log --grep&lt;/code&gt; too, but it's nice to see the list inside the note itself. A small script can rebuild a &lt;code&gt;## Commits&lt;/code&gt; section from git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# scripts/decision-commits.sh adr-20260927-session-store&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docs/decisions/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
git log &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'- `%h` %ad %s'&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/commits.md
&lt;span class="c"&gt;# Replace everything after the "## Commits" heading with the fresh list&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^## Commits/{print; while((getline l &amp;lt; "/tmp/commits.md")&amp;gt;0) print l; skip=1; next} !skip'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It assumes &lt;code&gt;## Commits&lt;/code&gt; is the last section of the note, which the template above guarantees. Run it by hand when you finish a decision; I wouldn't wire it into a post-commit hook, because a hook that edits files after every commit leaves your working tree permanently dirty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing decisions in Obsidian
&lt;/h2&gt;

&lt;p&gt;With Dataview, a small dashboard shows what's in flight and what's gone stale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Proposed, not yet decided&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;LIST
FROM "decisions"
WHERE status = "proposed"
SORT decided ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gu"&gt;## Accepted, not reviewed in a year&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE decided, reviewed
FROM "decisions"
WHERE status = "accepted" AND reviewed &amp;lt; date(today) - dur(1 year)
SORT reviewed ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decisions that span many commits
&lt;/h2&gt;

&lt;p&gt;A migration might take a dozen commits over several weeks. Nothing changes: every commit carries the same trailer, and &lt;code&gt;git log --grep&lt;/code&gt; returns them in order. If phases matter, say so in the subject line ("Phase 2: dual-write sessions to Redis") rather than inventing more metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the rejected ones
&lt;/h2&gt;

&lt;p&gt;When you decide &lt;em&gt;not&lt;/em&gt; to do something, write that down too and set &lt;code&gt;status: rejected&lt;/code&gt;. The next time someone proposes the same idea, there's a note with the context and the reasons, and you can judge whether those reasons still hold instead of having the whole discussion again. Same for superseded decisions: link the old note to the new one with &lt;code&gt;superseded_by&lt;/code&gt; instead of deleting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example, start to finish
&lt;/h2&gt;

&lt;p&gt;To make it concrete, here's what a small one might look like (illustrative, not a real project):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;adr-20260310-drop-axios.md&lt;/code&gt;. Context: we only use axios for simple JSON requests, and Node 18+ ships a global &lt;code&gt;fetch&lt;/code&gt;. Options: keep axios, switch to &lt;code&gt;fetch&lt;/code&gt; with a small wrapper. Decision: switch, with a wrapper in &lt;code&gt;lib/http.ts&lt;/code&gt; that handles retries and JSON errors. Consequence: we lose interceptors and write our own retry helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 1:&lt;/strong&gt; "Add fetch wrapper with retry" + &lt;code&gt;Decision: adr-20260310-drop-axios&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 2:&lt;/strong&gt; "Migrate API handlers to lib/http" + same trailer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 3:&lt;/strong&gt; "Remove axios dependency" + same trailer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A year later, someone runs &lt;code&gt;git blame&lt;/code&gt; on &lt;code&gt;lib/http.ts&lt;/code&gt;, opens the commit, sees the trailer, and reads the note. Total overhead at the time: one note and one extra line per commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Name decision notes with a stable ID, reference it in a commit trailer, and let git do the searching. Add the hook if typos bother you and the Dataview dashboard if you use Obsidian. Everything else is optional.&lt;/p&gt;




&lt;p&gt;If you want a ready-made starting point, &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;, my Obsidian vault for developers, includes an ADR template along with Dataview dashboards.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>architecture</category>
      <category>obsidian</category>
      <category>productivity</category>
    </item>
    <item>
      <title>n8n vs GitHub Actions for side-project automation</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:13:37 +0000</pubDate>
      <link>https://dev.to/productivityforge/n8n-vs-github-actions-for-side-project-automation-4gcp</link>
      <guid>https://dev.to/productivityforge/n8n-vs-github-actions-for-side-project-automation-4gcp</guid>
      <description>&lt;p&gt;Once a side project has users, the chores start: deploy on merge, sync payments into your database, send a welcome email, post a note somewhere when something breaks. Two tools come up constantly for this: GitHub Actions and n8n.&lt;/p&gt;

&lt;p&gt;They overlap, and you can force either one to do almost anything. But they're built around different centers of gravity, and picking the right one per job saves a lot of friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; is built around your repository. It shines when the trigger is a git event (push, pull request, tag, release) or the job needs your code checked out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; is built around moving data between services. It shines when the job is "when X happens in service A, do Y in services B and C", especially with SaaS APIs you don't want to write auth and pagination code for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time the choice is obvious once you ask: &lt;em&gt;does this job start from my code, or from someone else's service?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions: what it's good at
&lt;/h2&gt;

&lt;p&gt;Workflows are YAML files in &lt;code&gt;.github/workflows/&lt;/code&gt;. They run on GitHub-hosted runners (or your own) in response to events.&lt;/p&gt;

&lt;p&gt;Good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tests and linting on every PR,&lt;/li&gt;
&lt;li&gt;building and deploying on push to &lt;code&gt;main&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;publishing a package or release notes when you push a tag,&lt;/li&gt;
&lt;li&gt;scheduled jobs that need the repo, like regenerating a sitemap or updating a dependency report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal deploy-on-push workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy.sh&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get for free: no server to run, secrets management, logs for every run, and direct access to commit and branch context. Actions also supports scheduled workflows with cron syntax (&lt;code&gt;on: schedule: - cron: '0 6 * * *'&lt;/code&gt;), with a few caveats covered below.&lt;/p&gt;

&lt;p&gt;Where it gets awkward: anything that's mostly calling third-party APIs. You can do it with a script step, but you're writing and maintaining the API client, retries and error handling yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;At the time of writing, public repositories get GitHub-hosted runner minutes on standard runners for free. Private repositories draw from a monthly allowance (2,000 minutes on the Free plan, more on paid plans), then bill per minute. Check &lt;a href="https://docs.github.com/billing/reference/actions-runner-pricing" rel="noopener noreferrer"&gt;GitHub's current pricing&lt;/a&gt; before relying on this; it has changed before.&lt;/p&gt;

&lt;p&gt;The detail that bites people: &lt;strong&gt;GitHub rounds each job up to the nearest whole minute.&lt;/strong&gt; A job that runs for 5 seconds costs a full minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n: what it's good at
&lt;/h2&gt;

&lt;p&gt;n8n is a workflow automation tool with a visual node editor. You chain triggers (webhook, schedule, an app event) with nodes for specific services (Stripe, Postgres, Slack, Notion, Gmail, GitHub and many more), and drop into JavaScript or Python when you need custom logic. You can self-host it (the source is available under n8n's Sustainable Use License) or use their paid cloud.&lt;/p&gt;

&lt;p&gt;Good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Stripe event creates or updates a row in your database,&lt;/li&gt;
&lt;li&gt;a new signup triggers a welcome email and a note in your CRM or Notion,&lt;/li&gt;
&lt;li&gt;a form submission lands in a spreadsheet and pings you,&lt;/li&gt;
&lt;li&gt;a daily digest pulls numbers from a few APIs and emails them to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a "new paying customer" workflow might be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Trigger&lt;/strong&gt; on &lt;code&gt;checkout.session.completed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; node: upsert the customer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Request&lt;/strong&gt; node: call your own API to provision their account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send Email&lt;/strong&gt; (or Gmail) node: welcome message&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;error workflow&lt;/strong&gt; that notifies you if any step fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of those would be a chunk of code in an Actions script. In n8n the nodes handle credentials and API details, and you mostly write the data mapping.&lt;/p&gt;

&lt;p&gt;n8n can also react to repo events: its GitHub Trigger node registers a webhook for things like pushes or new issues. So "n8n can't do git events" isn't true; it's just not where it's strongest, because it doesn't have your code checked out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;Self-hosting is free in license terms, but it's a server you now own: updates, backups of its database (which holds your workflows and credentials), HTTPS, and monitoring. For many indie developers that's a small VPS they already have. n8n Cloud removes the ops work for a monthly fee; plans and execution limits change, so check their pricing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: frequent schedules on Actions
&lt;/h2&gt;

&lt;p&gt;A pattern that looks cheap and isn't: using Actions as an uptime monitor with a cron every five minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every 5 minutes is 288 runs a day, roughly 8,640 a month.&lt;/li&gt;
&lt;li&gt;Each run is billed as at least one minute.&lt;/li&gt;
&lt;li&gt;On a private repo, that's around 8,640 billed minutes a month, over four times the Free plan's allowance, for a job that runs &lt;code&gt;curl&lt;/code&gt; for two seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's also not a great monitor. GitHub's docs note that scheduled workflows can be delayed during periods of high load, five minutes is the shortest interval allowed, and in public repositories scheduled workflows are automatically disabled after 60 days without repository activity.&lt;/p&gt;

&lt;p&gt;For uptime checks, use a dedicated uptime monitoring service or a schedule trigger in n8n (which runs on your server, so frequency doesn't cost minutes). Keep Actions schedules for jobs that run a few times a day at most and genuinely need the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use both
&lt;/h2&gt;

&lt;p&gt;A split that works well: Actions owns everything up to "the new version is live", and n8n owns what happens after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="c1"&gt;# last step of the deploy job&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tell n8n about the deploy&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsS -X POST "$N8N_WEBHOOK_URL" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
            &lt;span class="s"&gt;-d "{\"sha\": \"${GITHUB_SHA}\", \"ref\": \"${GITHUB_REF_NAME}\"}"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.N8N_DEPLOY_WEBHOOK }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The n8n workflow behind that webhook can post to Discord, update a status page, add a changelog row, and email beta users, without any of that logic living in your repo's CI config. If the n8n side fails, your deploy still succeeded, which is usually what you want.&lt;/p&gt;

&lt;p&gt;If you add a webhook like this, protect it: at minimum a hard-to-guess URL kept in secrets, ideally a shared token checked in the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick way to decide
&lt;/h2&gt;

&lt;p&gt;List the chores you currently do by hand and note two things for each: what triggers it, and what it touches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chore&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Touches&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run tests on PRs&lt;/td&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy on merge&lt;/td&gt;
&lt;td&gt;Push to &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Your code, host&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Stripe customer -&amp;gt; DB + welcome email&lt;/td&gt;
&lt;td&gt;Stripe event&lt;/td&gt;
&lt;td&gt;Stripe, DB, email&lt;/td&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly metrics email&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;Several APIs&lt;/td&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regenerate docs site&lt;/td&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime check every few minutes&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;Your site&lt;/td&gt;
&lt;td&gt;Uptime monitor or n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announce a release&lt;/td&gt;
&lt;td&gt;Tag push&lt;/td&gt;
&lt;td&gt;Discord, email, status page&lt;/td&gt;
&lt;td&gt;Actions -&amp;gt; n8n webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a chore takes you five minutes a month, automating it with either tool may not be worth the maintenance. Automate the ones that are frequent, error-prone, or that you tend to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Start with GitHub Actions, because you probably already have it and it covers everything code-shaped. Add n8n when you catch yourself writing API glue in YAML, or when a job has nothing to do with your repository. And be careful with frequent cron schedules on private repos; that's where Actions gets surprisingly expensive.&lt;/p&gt;




&lt;p&gt;If you're running a few side projects and want one place to track them (ideas, launch checklist, revenue), I made a &lt;a href="https://subengel.gumroad.com/l/tsatqt" rel="noopener noreferrer"&gt;Side Project Tracker&lt;/a&gt; Notion template.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>nocode</category>
      <category>automation</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>A weekly review for engineers that isn't about tasks</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 02 Oct 2026 17:09:30 +0000</pubDate>
      <link>https://dev.to/productivityforge/a-weekly-review-for-engineers-that-isnt-about-tasks-4ib4</link>
      <guid>https://dev.to/productivityforge/a-weekly-review-for-engineers-that-isnt-about-tasks-4ib4</guid>
      <description>&lt;p&gt;The classic weekly review (from Getting Things Done) is about tasks and commitments: empty your inboxes, update your lists, look at your calendar. That's useful, and your issue tracker probably covers most of it already.&lt;/p&gt;

&lt;p&gt;What it doesn't cover is the stuff developers lose all the time: why you picked that library, what the fix was for that weird timezone bug, which config flag finally made the build work. You solve a problem, move on, and meet it again six months later with no memory of the answer.&lt;/p&gt;

&lt;p&gt;This is a review built around that problem. It has two parts: a tiny daily habit and a fixed weekly session, plus an optional monthly skim.&lt;/p&gt;

&lt;h2&gt;
  
  
  During the week: one line per thing worth remembering
&lt;/h2&gt;

&lt;p&gt;Keep a section in your daily note (or a single running file, if you don't do daily notes) and add one line whenever something happens that you'd be annoyed to rediscover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bug that took more than half an hour to understand,&lt;/li&gt;
&lt;li&gt;an API or library that didn't behave the way you assumed,&lt;/li&gt;
&lt;li&gt;a choice between two approaches,&lt;/li&gt;
&lt;li&gt;a command, flag or config you had to look up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the format dumb so you actually do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Log&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres: ILIKE treats &lt;span class="sb"&gt;`_`&lt;/span&gt; and &lt;span class="sb"&gt;`%`&lt;/span&gt; in user input as wildcards. Escape them before building the pattern. PR #412
&lt;span class="p"&gt;-&lt;/span&gt; Chose server-side sessions over JWT refresh; token rotation raced across tabs. -&amp;gt; needs decision note
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`git log -S "term"`&lt;/span&gt; finds when a string first appeared. Keep forgetting this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problem, answer, link if there is one. No tags required, no templates to fill in. If you're adding more than a couple of minutes a day, you're overdoing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friday: 45 minutes, same questions every week
&lt;/h2&gt;

&lt;p&gt;Put it on your calendar. Friday afternoon works for most people because the week is still fresh and it's rarely prime focus time anyway. Open the week's log lines and go through four questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What happened more than once?
&lt;/h3&gt;

&lt;p&gt;Scan for repeats. Same kind of bug twice, same lookup twice, same review comment on two PRs. A repeat is a signal that a small artifact would pay off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a snippet you can paste next time,&lt;/li&gt;
&lt;li&gt;a checklist (e.g. "things to check when a query gets slow"),&lt;/li&gt;
&lt;li&gt;a lint rule or test that stops it happening again,&lt;/li&gt;
&lt;li&gt;a line in the project README.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make the artifact now, while you remember the details. Don't make one for things that only happened once; most of those never come back.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What did I decide?
&lt;/h3&gt;

&lt;p&gt;Any line that's really a decision (you picked X over Y and there were trade-offs) gets promoted to its own note with context, options and reasons. It takes ten minutes on Friday and saves the "why on earth did we do this" conversation later. If you link decisions to commits, add the ID to the relevant commits now.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What did I have to look up, and should I actually learn it?
&lt;/h3&gt;

&lt;p&gt;Things you looked up once are fine. Things you looked up three times are a gap. Keep a short "learn next" list, and be honest about priority: only put something at the top if it keeps costing you time. A line like this is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Learn next&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres window functions (hit twice this month writing reports)
&lt;span class="p"&gt;-&lt;/span&gt; TypeScript conditional types (fought inference on the API client again)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then actually block time for the top item. An hour of reading the docs properly beats another three rounds of copying from Stack Overflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. What's still open?
&lt;/h3&gt;

&lt;p&gt;Last: anything from the log that's unresolved. A workaround you shipped that needs a real fix, a question you never answered, a follow-up from an incident. Turn each into a task in whatever tracker you use, or delete it if you honestly won't get to it. The point is that nothing unresolved lives only in the log.&lt;/p&gt;

&lt;h3&gt;
  
  
  The weekly note
&lt;/h3&gt;

&lt;p&gt;The output of all this is one short note per week. A template like this keeps it consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Week 2026-W39&lt;/span&gt;

&lt;span class="gu"&gt;## Repeats -&amp;gt; artifacts&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ILIKE escaping: added &lt;span class="sb"&gt;`escapeLike()`&lt;/span&gt; helper + test

&lt;span class="gu"&gt;## Decisions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[adr-20260924-session-store]]

&lt;span class="gu"&gt;## Learn next&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres window functions

&lt;span class="gu"&gt;## Open -&amp;gt; tasks&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Replace retry workaround in webhook handler (#431)

&lt;span class="gu"&gt;## One sentence on the week&lt;/span&gt;
Mostly auth work; lost a day to the tab race before understanding it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a section is empty, leave it empty. Some weeks nothing repeats and you decide nothing interesting. That's fine; the review takes fifteen minutes those weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once a month: skim, don't audit
&lt;/h2&gt;

&lt;p&gt;On the first Friday of the month, spend an extra twenty minutes reading the last four weekly notes in a row. You're looking for things that only show up at that distance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same area of the codebase appearing in "repeats" every week (a refactor candidate),&lt;/li&gt;
&lt;li&gt;a "learn next" item that's been sitting at the top for a month (block the time or drop it),&lt;/li&gt;
&lt;li&gt;decisions that contradict each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write two or three bullets at the top of the month's first weekly note and move on. No spreadsheets, no scoring yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's built this way
&lt;/h2&gt;

&lt;p&gt;The usual failure mode of review systems is that they're more work than the problems they solve, so they get dropped after a few weeks. This one tries to avoid that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture costs almost nothing.&lt;/strong&gt; One line, no structure, in a note you already have open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weekly session has fixed questions.&lt;/strong&gt; You're never staring at a blank page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every question produces something concrete&lt;/strong&gt; (a snippet, a decision note, a task) or produces nothing and you move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no metric to maintain.&lt;/strong&gt; Tracking "hours saved" by your notes is itself a job, and the numbers are guesses anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;## Log&lt;/code&gt; section to today's daily note (or create &lt;code&gt;log.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Add one line every time something annoys you or surprises you.&lt;/li&gt;
&lt;li&gt;Put a 45-minute block on Friday and run the four questions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip the monthly skim until you've done four weekly reviews. If you only ever do the Friday part, you're already ahead of where most of us are.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;, my Obsidian vault for developers, includes a weekly review template, if you'd rather start from one than build it yourself.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>obsidian</category>
      <category>developers</category>
    </item>
    <item>
      <title>Dataview queries I'd put in any developer vault (and the gotchas that break them)</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Thu, 01 Oct 2026 17:30:15 +0000</pubDate>
      <link>https://dev.to/productivityforge/dataview-queries-id-put-in-any-developer-vault-and-the-gotchas-that-break-them-1jga</link>
      <guid>https://dev.to/productivityforge/dataview-queries-id-put-in-any-developer-vault-and-the-gotchas-that-break-them-1jga</guid>
      <description>&lt;p&gt;A few days ago I posted &lt;a href="https://dev.to/productivityforge/dataview-queries-worth-having-in-a-developer-vault-59e"&gt;Dataview queries worth having in a developer vault&lt;/a&gt;. The comments were better than the post. They pointed out three things that quietly break these queries.&lt;/p&gt;

&lt;p&gt;This is the follow-up. Different queries, and the fixes baked in.&lt;/p&gt;

&lt;p&gt;Same disclaimer as last time: Dataview only reads the Markdown notes in your vault. It doesn't read your code. It's for the notes &lt;em&gt;about&lt;/em&gt; your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three gotchas first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cancelled tasks count as open.&lt;/strong&gt; Dataview only treats &lt;code&gt;[x]&lt;/code&gt; as completed. So &lt;code&gt;!completed&lt;/code&gt; also matches &lt;code&gt;[-]&lt;/code&gt;, which is how the Tasks plugin (and many themes) mark cancelled tasks. Add &lt;code&gt;status != "-"&lt;/code&gt;. &lt;code&gt;status&lt;/code&gt; is the character between the brackets.&lt;/p&gt;

&lt;p&gt;While you're there, add &lt;code&gt;text != ""&lt;/code&gt;. Templates leave empty checkboxes behind, and they show up as open tasks too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync bumps &lt;code&gt;file.mtime&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;file.mtime&lt;/code&gt; is the time on disk. Obsidian Sync, iCloud and Syncthing can change it when they write a note to another device. So "recently modified" fills up with notes you didn't touch. The fix is a &lt;code&gt;modified:&lt;/code&gt; field in frontmatter, with &lt;code&gt;file.mtime&lt;/code&gt; as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan lists fill up with templates.&lt;/strong&gt; Templates, daily notes and dashboards are unlinked on purpose. If you leave them in, the real orphans get buried. Excluding folders by name works until you rename a folder. A frontmatter flag doesn't have that problem.&lt;/p&gt;

&lt;p&gt;Now the queries. Folder names are mine (&lt;code&gt;Projects&lt;/code&gt;, &lt;code&gt;Templates&lt;/code&gt;). Change the text after &lt;code&gt;FROM&lt;/code&gt; to match yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Open tasks across the whole vault
&lt;/h2&gt;

&lt;p&gt;Every unchecked task outside the templates folder, grouped by note. No fields needed, just checkboxes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TASK
FROM -"Templates"
WHERE !completed AND status != "-" AND text != ""
GROUP BY file.link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all three task filters in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Recently modified notes, sync-safe
&lt;/h2&gt;

&lt;p&gt;Notes edited in the last 7 days, newest first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TABLE file.folder AS "Folder", default(modified, file.mtime) AS "Modified"
FROM -"Templates"
WHERE default(modified, file.mtime) &amp;gt;= date(today) - dur(7 days) AND file.path != this.file.path
SORT min(default(modified, file.mtime), date(now) - dur(5 minutes)) DESC, file.name ASC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things going on here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;default(modified, file.mtime)&lt;/code&gt; uses your &lt;code&gt;modified:&lt;/code&gt; field when a note has one. Older notes fall back to the file time.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;min(...)&lt;/code&gt; in the sort is a grace window. Anything edited in the last 5 minutes sorts as "5 minutes ago". So when a sync lands mid-read, the list doesn't reshuffle under you. (That idea came from a commenter on the last post.)&lt;/p&gt;

&lt;p&gt;To fill &lt;code&gt;modified:&lt;/code&gt; automatically, the Linter plugin's "YAML timestamp" rule works. Set the key to &lt;code&gt;modified&lt;/code&gt;, the format to &lt;code&gt;YYYY-MM-DDTHH:mm:ss&lt;/code&gt;, and "Date modified source of truth" to "user or Linter edits". The default source copies the file time, which is the thing sync bumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Orphan notes, with an opt-out flag
&lt;/h2&gt;

&lt;p&gt;Notes nothing links to, newest first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIST
FROM -"Templates"
WHERE length(file.inlinks) = 0 AND exclude-from-orphans != true
SORT file.ctime DESC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put &lt;code&gt;exclude-from-orphans: true&lt;/code&gt; in the frontmatter of anything that's unlinked on purpose. Daily notes, dashboards, the note holding this query. Put it in your daily note template once and you're done.&lt;/p&gt;

&lt;p&gt;Note this only checks inlinks. A note that links out but nothing links to is still hard to find again. That's the one I want to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Stale projects
&lt;/h2&gt;

&lt;p&gt;Projects still marked active that nobody has touched in 30 days, oldest first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TABLE default(modified, file.mtime) AS "Last edited", length(filter(file.tasks, (t) =&amp;gt; !t.completed AND t.status != "-" AND t.text != "")) AS "Open tasks"
FROM "Projects"
WHERE type = "project" AND status = "active" AND default(modified, file.mtime) &amp;lt; date(today) - dur(30 days)
SORT default(modified, file.mtime) ASC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expects &lt;code&gt;type: project&lt;/code&gt; and &lt;code&gt;status: active&lt;/code&gt; in frontmatter. Same &lt;code&gt;modified:&lt;/code&gt; fallback as above, otherwise a sync makes every project look fresh.&lt;/p&gt;

&lt;p&gt;The open task count comes from the checkboxes in the note. No counter field to keep in sync by hand.&lt;/p&gt;

&lt;p&gt;When something shows up here, either pick it back up or change its status. "Active" should mean active.&lt;/p&gt;

&lt;h2&gt;
  
  
  If a query shows nothing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dates in frontmatter need to be plain ISO (&lt;code&gt;2026-09-27&lt;/code&gt;). &lt;code&gt;Sept 27&lt;/code&gt; is just text, and date comparisons silently return nothing.&lt;/li&gt;
&lt;li&gt;Field names get normalized. &lt;code&gt;Due Date&lt;/code&gt; becomes &lt;code&gt;due-date&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Strip the query down to &lt;code&gt;LIST FROM "Folder"&lt;/code&gt; and add clauses back one at a time. Usually it's the folder name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rest
&lt;/h2&gt;

&lt;p&gt;I put all 10 in one note: these four, plus unfinished tasks from recent daily notes, an ADR log, ADRs due for review, active projects with open task counts, incident follow-ups and a tech debt register. Each one says what it shows and which fields it expects. It's free (pay what you want): &lt;a href="https://subengel.gumroad.com/l/dzanl" rel="noopener noreferrer"&gt;https://subengel.gumroad.com/l/dzanl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather start from a whole vault with the templates and dashboards already set up, that's &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>obsidian</category>
      <category>productivity</category>
      <category>notes</category>
    </item>
    <item>
      <title>An Obsidian + Git workflow for solo developers</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Tue, 29 Sep 2026 17:16:47 +0000</pubDate>
      <link>https://dev.to/productivityforge/an-obsidian-git-workflow-for-solo-developers-4mbk</link>
      <guid>https://dev.to/productivityforge/an-obsidian-git-workflow-for-solo-developers-4mbk</guid>
      <description>&lt;p&gt;If you write code alone, your project knowledge is scattered in a predictable way: the code and its history live in git, and the &lt;em&gt;reasons&lt;/em&gt; live in your head, a notes app, or nowhere. Putting your Obsidian vault under git doesn't solve that by itself, but it gets your notes into the same kind of history as your code: dated, diffable, and searchable with tools you already use.&lt;/p&gt;

&lt;p&gt;This is the setup I'd recommend if you're starting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why git and not just a sync service
&lt;/h2&gt;

&lt;p&gt;Obsidian Sync, iCloud, Dropbox and friends are good at one thing: making the same files show up on every device. Some keep version history too. What they don't give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Messages.&lt;/strong&gt; A sync service knows a file changed at 14:02. It doesn't know you changed it because the retry logic was rewritten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History you can query.&lt;/strong&gt; &lt;code&gt;git log -- notes/payments.md&lt;/code&gt;, &lt;code&gt;git log -S "rate limit"&lt;/code&gt;, &lt;code&gt;git blame&lt;/code&gt;. Your notes get the same archaeology tools as your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One place for code and docs&lt;/strong&gt;, if you choose to keep them in the same repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use both. A common pattern is Obsidian Sync for moving files between devices and git on one desktop as backup and history. The plugin docs cover this: you can disable the plugin on other devices, and set the pull merge strategy to "Other sync service" so git doesn't fight Sync over your files. Test any combination on a copy of the vault first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Put the vault in a repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/vaults/dev-notes
git init
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the one decision that matters: what to do with &lt;code&gt;.obsidian/&lt;/code&gt;. That folder holds your settings, installed plugins and hotkeys, plus some files that change constantly (open panes, recent files). Ignoring the whole folder is common advice, but it means a fresh clone opens with no plugins and default settings. I'd ignore only the noisy parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Obsidian: per-device UI state
.obsidian/workspace.json
.obsidian/workspace-mobile.json

# Trash and OS junk
.trash/
.DS_Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two exceptions worth checking. Some plugins store API keys or tokens in their &lt;code&gt;data.json&lt;/code&gt; under &lt;code&gt;.obsidian/plugins/&lt;/code&gt;; ignore those plugin folders if the repo could ever be seen by anyone else. And if a file you didn't expect shows up in every commit, it probably belongs in &lt;code&gt;.gitignore&lt;/code&gt; (remember to &lt;code&gt;git rm --cached&lt;/code&gt; it, since ignoring an already-committed file does nothing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add a remote
&lt;/h2&gt;

&lt;p&gt;A private repo on GitHub, GitLab, Codeberg, or a self-hosted Gitea all work. Private repos are free on GitHub's free plan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin git@github.com:you/dev-notes.git
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"init: vault baseline"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even solo, a remote is worth it: it's your off-machine backup, and it's how a second computer gets the vault.&lt;/p&gt;

&lt;p&gt;One warning: &lt;strong&gt;don't put secrets in notes.&lt;/strong&gt; It's easy to paste an API key into a debugging note. Once it's committed and pushed, it's in the history for good (rotating the key is the fix, not deleting the note).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Install Obsidian Git
&lt;/h2&gt;

&lt;p&gt;The community plugin &lt;a href="https://github.com/Vinzent03/obsidian-git" rel="noopener noreferrer"&gt;Obsidian Git&lt;/a&gt; handles commits, pulls and pushes from inside Obsidian. After installing and enabling it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto commit-and-sync interval:&lt;/strong&gt; 10-15 minutes is a reasonable start. This commits any changes, pulls, and pushes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull on startup:&lt;/strong&gt; on. This is what saves you when you switch machines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit message:&lt;/strong&gt; the default is &lt;code&gt;vault backup: {{date}}&lt;/code&gt;. Fine for automatic commits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin also has a source control view (stage, diff, commit) and a history view, so you rarely need the terminal. Its mobile support is marked experimental in the README, so don't make your phone a critical part of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Two kinds of commits
&lt;/h2&gt;

&lt;p&gt;Auto-commits are your safety net. They're noisy by design, and that's fine: nobody reads &lt;code&gt;vault backup: 2026-09-27 14:10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The commits worth writing by hand are the ones that capture &lt;em&gt;why&lt;/em&gt;. When you change a note because something real happened (a decision, an incident, a surprising bug), commit it yourself with a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs(auth): record why sessions moved from JWT to Redis

Refresh-token rotation kept breaking on multiple tabs.
See decisions/2026-09-auth-sessions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A loose convention like &lt;code&gt;docs(area): ...&lt;/code&gt; for notes and your usual style for code is enough. The payoff comes months later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Everything that ever touched the auth notes, with messages&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; notes/auth/

&lt;span class="c"&gt;# When did "idempotency key" first appear anywhere in the vault?&lt;/span&gt;
git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"idempotency key"&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't bother rewriting history to squash auto-commits. Force-pushing &lt;code&gt;main&lt;/code&gt; to make the log look tidy is risky once a second machine is pulling from it, and &lt;code&gt;git log --grep&lt;/code&gt;/&lt;code&gt;-S&lt;/code&gt;/&lt;code&gt;-- path&lt;/code&gt; already let you ignore the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same repo as the code, or separate?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Separate vault repo&lt;/strong&gt; works best when your notes span several projects, which for most solo developers they do. It also keeps notes out of any repo you might later open-source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;docs/&lt;/code&gt; or &lt;code&gt;notes/&lt;/code&gt; folder inside the project repo&lt;/strong&gt; works when the notes are strictly about that one codebase. The upside is that a code change and its explanation can land in the same commit or PR, and &lt;code&gt;git log&lt;/code&gt; shows them interleaved. You can still open that folder as its own Obsidian vault.&lt;/p&gt;

&lt;p&gt;If you go with the same repo, keep the commits separate even when they're close in time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refactor(api): split router into modules"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; src/
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs(api): note why routers are split by domain"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; notes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps &lt;code&gt;git log -- src/&lt;/code&gt; clean for code review and &lt;code&gt;git log -- notes/&lt;/code&gt; readable as a record of decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge conflicts.&lt;/strong&gt; If you edit the same note on two machines between syncs, you'll get a normal git conflict with markers in the file. Resolve it like code. Pulling on startup and a short auto-sync interval make this rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Huge repo.&lt;/strong&gt; Images and PDFs add up. Put attachments in one folder and consider Git LFS or keeping large files out of the vault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin stops syncing.&lt;/strong&gt; Check the plugin's notices, then run &lt;code&gt;git status&lt;/code&gt; in a terminal. Nine times out of ten it's an auth problem with the remote or a conflict that needs resolving.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you end up with
&lt;/h2&gt;

&lt;p&gt;A vault that's backed up off-machine without thinking about it, readable on any computer with a clone, and a history where the important changes have a sentence explaining them. It's not fancy. The value shows up the first time you need to know when and why you changed your mind about something, and &lt;code&gt;git log&lt;/code&gt; just tells you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; If your vault also runs on Dataview, I put 10 ready-to-paste queries for a developer vault in one free note (pay what you want): &lt;a href="https://subengel.gumroad.com/l/dzanl" rel="noopener noreferrer"&gt;https://subengel.gumroad.com/l/dzanl&lt;/a&gt;&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>git</category>
      <category>productivity</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Dataview queries worth having in a developer vault</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Mon, 28 Sep 2026 00:43:59 +0000</pubDate>
      <link>https://dev.to/productivityforge/dataview-queries-worth-having-in-a-developer-vault-59e</link>
      <guid>https://dev.to/productivityforge/dataview-queries-worth-having-in-a-developer-vault-59e</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;This post assumes you know the basics (&lt;code&gt;TABLE&lt;/code&gt;, &lt;code&gt;LIST&lt;/code&gt;, &lt;code&gt;TASK&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;). If not, the &lt;a href="https://blacksmithgu.github.io/obsidian-dataview/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; are short and good.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tech debt register, sorted by priority
&lt;/h2&gt;

&lt;p&gt;Give each debt item its own note in a &lt;code&gt;debt/&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debt&lt;/span&gt;
&lt;span class="na"&gt;area&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;        &lt;span class="c1"&gt;# 1 = fix now ... 4 = someday&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;       &lt;span class="c1"&gt;# open | in-progress | resolved&lt;/span&gt;
&lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-09-14&lt;/span&gt;
&lt;span class="na"&gt;estimate_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE area, priority, estimate_hours AS "Est. h", created
FROM "debt"
WHERE status != "resolved"
SORT priority ASC, created ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;To see what you've actually paid down, add a &lt;code&gt;resolved: 2026-09-20&lt;/code&gt; field when you close an item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE area, resolved, resolved - created AS "Took"
FROM "debt"
WHERE resolved
SORT resolved DESC
LIMIT 10&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 (&lt;code&gt;created: 2026-09-14&lt;/code&gt;). A quoted date in an inline field (&lt;code&gt;created:: "2026-09-14"&lt;/code&gt;) stays a string, and so does anything not in ISO form; &lt;code&gt;resolved - created&lt;/code&gt; then silently renders nothing. If a field comes through as text, wrap it in the query: &lt;code&gt;date(resolved) - date(created)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ADRs that are due for a second look
&lt;/h2&gt;

&lt;p&gt;Architecture decision records go stale quietly. The decision was right at the time; the constraints moved. A &lt;code&gt;reviewed&lt;/code&gt; date in the frontmatter lets you surface old ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adr&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accepted&lt;/span&gt;   &lt;span class="c1"&gt;# proposed | accepted | superseded | rejected&lt;/span&gt;
&lt;span class="na"&gt;decided&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2025-06-02&lt;/span&gt;
&lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2025-06-02&lt;/span&gt;
&lt;span class="na"&gt;superseded_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE decided, reviewed, choice(reviewed &amp;lt; date(today) - dur(1 year), "revisit", "ok") AS "Freshness"
FROM "decisions"
WHERE status = "accepted"
SORT reviewed ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;choice(condition, ifTrue, ifFalse)&lt;/code&gt; takes exactly three arguments. If you want three buckets, nest it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choice(reviewed &amp;lt; date(today) - dur(1 year), "revisit",
  choice(reviewed &amp;lt; date(today) - dur(6 months), "soon", "ok"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Revisit" doesn't mean "wrong". It means "read this again and bump &lt;code&gt;reviewed&lt;/code&gt; if it still holds."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Incidents with open follow-ups
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TASK
FROM "incidents"
WHERE !completed AND status != "-"
GROUP BY file.link&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a table view for the incidents themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE severity, service, occurred, length(filter(file.tasks, (t) =&amp;gt; !t.completed AND t.status != "-")) AS "Open items"
FROM "incidents"
WHERE length(filter(file.tasks, (t) =&amp;gt; !t.completed AND t.status != "-")) &amp;gt; 0
SORT occurred DESC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual &lt;code&gt;action_items_pending: 3&lt;/code&gt; field to keep in sync. The count comes from the checkboxes.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  4. PR reviews still waiting on something
&lt;/h2&gt;

&lt;p&gt;If you keep a short note per PR you're reviewing (or authored and are waiting on), a status field is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;pr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1234&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;waiting&lt;/span&gt;    &lt;span class="c1"&gt;# waiting | changes-requested | approved | merged&lt;/span&gt;
&lt;span class="na"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-09-18&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE repo, pr, status, opened
FROM "reviews"
WHERE status = "waiting" OR status = "changes-requested"
SORT opened ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to flag the old ones? Filter on age instead of storing a &lt;code&gt;days_in_review&lt;/code&gt; number you'd have to update by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"waiting"&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;opened&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Unfinished tasks from recent daily notes
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TASK
FROM "daily"
WHERE !completed AND status != "-" AND file.day &amp;gt;= date(today) - dur(14 days)
GROUP BY file.link
SORT file.day DESC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Orphan notes
&lt;/h2&gt;

&lt;p&gt;Notes with no links in or out are usually either finished thoughts that never got connected, or junk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;LIST
FROM "" AND -"90 Templates"
WHERE length(file.inlinks) = 0 AND length(file.outlinks) = 0 AND exclude-from-orphans != true
SORT file.mtime ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template files and daily notes are usually unlinked on purpose. Prefer an &lt;code&gt;exclude-from-orphans: true&lt;/code&gt; 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).&lt;/p&gt;

&lt;p&gt;I'd run this during a weekly review, not keep it on a dashboard. Either link them to something or delete them.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What changed this week
&lt;/h2&gt;

&lt;p&gt;Useful for writing a weekly summary or standup notes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE file.folder AS "Folder", file.mtime AS "Modified"
FROM ""
WHERE file.mtime &amp;gt;= date(today) - dur(7 days)
SORT file.mtime DESC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One catch: &lt;code&gt;file.mtime&lt;/code&gt; 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 &lt;code&gt;modified:&lt;/code&gt; field in the frontmatter and query that instead, falling back to &lt;code&gt;file.mtime&lt;/code&gt; for notes that don't have it yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE file.folder AS "Folder", default(modified, file.mtime) AS "Modified"
FROM ""
WHERE default(modified, file.mtime) &amp;gt;= date(today) - dur(7 days)
SORT default(modified, file.mtime) DESC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;modified&lt;/code&gt;, the format to &lt;code&gt;YYYY-MM-DDTHH:mm:ss&lt;/code&gt; (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).&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE file.folder AS "Folder", default(modified, file.mtime) AS "Modified"
FROM ""
WHERE default(modified, file.mtime) &amp;gt;= date(today) - dur(7 days)
SORT (date(now) - default(modified, file.mtime) &amp;lt; dur(5 minutes) ? dur(5 minutes) : (date(now) - default(modified, file.mtime))) ASC, file.name ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Things that trip people up
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a class="mentioned-user" href="https://dev.to/contentclips_st"&gt;@contentclips_st&lt;/a&gt; on dev.to for the sync, date, cancelled-task, orphan-flag and grace-window fixes.&lt;/p&gt;




&lt;p&gt;If you'd rather start from a vault that already has these conventions and dashboards set up, I packaged mine as &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt; (Obsidian, needs Templater and Dataview).&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>productivity</category>
      <category>markdown</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
