A portable state-tracking layer for markdown note vaults (Obsidian, Dendron, Foam, and plain markdown folders).
Project description
note-state
A portable "patch" for turning a pile of notes into a pipeline: every note
carries an explicit state telling you how far it's traveled from raw
capture to integrated, trustworthy knowledge — the same way a ticket in an
issue tracker carries a status, or a part on an assembly line carries a
stage.
Works with any note application that stores notes as markdown files with
YAML frontmatter on local disk — Obsidian, Dendron, Foam, Logseq, plain
markdown vaults, even a folder of .md files with no app at all.
Install
pip install note-state
The pipeline
drafting → review → formatting → referencing → integrating → published
↘ ↘ ↘ ↘
needs_revision (loops back to whichever
stage flagged the problem)
any state ──────────────────────────────→ archived
| State | What happens here | Exit condition |
|---|---|---|
| Drafting | Raw capture. Get the idea down with as little friction as possible. | The idea has enough substance to be judged. |
| Review | Read it critically: accurate, internally consistent, complete? | Content is verified, or it's sent back to Drafting. |
| Formatting | Give it shape: headings, atomic scope, a real title, clean prose, tags. | It reads well and is scoped correctly. |
| Referencing | Attach provenance: citations, source links, backlinks. | Every non-obvious claim is traceable to a source. |
| Integrating | Weave it into the graph: link from a Map of Content, cross-link related notes. | It's reachable from at least one other place in the vault. |
| Published | Stable and reusable — safe to cite, export, or share. | Terminal, but can be pulled back into Integrating. |
| Needs Revision (utility state) | Something's wrong. Flagged from any stage. | Always exits back to the stage that flagged it. |
| Archived (utility state) | Retired or superseded. | Terminal. |
This isn't sacred — it's just the bundled default. Drop your own
states.yaml next to your notes (or in any parent directory) to define a
different pipeline; note-state doesn't hardcode the states, it just
enforces whatever pipeline you describe.
The metadata schema
Every tracked note gets two frontmatter fields: the current state, and a timestamped log of how it got there.
---
title: "Maps of Content connect clusters of related notes"
state: referencing
state_history:
- state: drafting
date: 2026-07-01
- state: review
date: 2026-07-05
- state: formatting
date: 2026-07-08
- state: referencing
date: 2026-07-12
note: "added three sources"
tags: [pkm, moc]
---
Note body goes here as normal — nothing about the state system touches
the content itself.
state_history is what makes the system useful beyond a label: it's an
audit trail you can query later ("which notes have been stuck in Review for
three weeks?", "how long does formatting usually take?").
Usage
# Stamp every note in a folder with the initial state (only touches
# notes that don't already have one — safe to run repeatedly)
note-state init ./my-vault
# Move a note forward
note-state set my-vault/atomic-notes.md review
# Try to skip a stage — this is rejected unless you pass --force
note-state set my-vault/atomic-notes.md published
# Move many notes at once: multiple paths, a glob, or a list piped over stdin
note-state set a.md b.md c.md review
note-state set --glob "my-vault/**/*.md" review
find my-vault -name "*.md" -mtime -1 | note-state set review --stdin
# (each file is validated independently — one illegal transition doesn't
# block the rest, but the command still exits non-zero if any failed)
# See what's where
note-state list ./my-vault
note-state list ./my-vault --state review
# Find bottlenecks — counts per state + oldest untouched notes
note-state report ./my-vault
# CI / pre-commit friendly: exit non-zero if anything has sat too long
note-state check ./my-vault --max-age 14
note-state check ./my-vault --max-age 3 --state review --state formatting
# Print the pipeline as a Mermaid diagram (paste into anything that renders it)
note-state graph
list, report, and check cache their per-note results (in a
.note-state-cache.json file dropped next to the notes, keyed by each
file's mtime) so re-running them on a large vault only re-parses notes that
actually changed since the last run. Safe to .gitignore; it's rebuilt
automatically.
Customizing the pipeline
Two files define the state machine:
states.yaml— states + allowed transitions.note-statelooks for this file (or.note-states.yaml) in the current directory, then any parent directory, and falls back to the bundled default pipeline above if none is found. Copy the bundled default as a starting point and edit freely — rename stages, add atranslatingorpeer-reviewstate, rewire transitions.- Pass
--config path/to/states.yamlto point at a specific file instead of relying on directory discovery.
Shortcuts (trapdoor transitions without --force)
Add a top-level shortcuts: list to allow specific transitions from any
state (or from one named state) without requiring --force every time:
shortcuts:
- to: needs_revision # any state -> needs_revision, always allowed
- from: published
to: archived # only published -> archived is shortcut-allowed
The bundled default ships with any state -> needs_revision, since flagging
a problem shouldn't require fighting the tool.
Multiple pipelines (different note types, one vault)
If different kinds of notes should follow different pipelines — say, meeting
notes shouldn't go through the same stages as permanent notes — define a
pipelines: map instead of a single top-level pipeline:
pipelines:
default:
initial_state: drafting
states: { drafting: {}, review: {}, published: {} }
transitions:
drafting: [review]
review: [published, drafting]
published: []
meeting:
initial_state: captured
states: { captured: {}, actioned: {}, archived: {} }
transitions:
captured: [actioned]
actioned: [archived]
archived: []
A note picks its pipeline via a type: frontmatter field (type: meeting);
notes with no type use default. note-state init --type meeting ...
stamps that type onto notes that don't already declare one. note-state graph prints every pipeline unless you pass --pipeline NAME.
Wiring it into a specific app
-
Obsidian, Dendron, Foam, plain markdown folders — these already read and preserve YAML frontmatter, so
note-stateworks with zero adaptation: run it straight against your vault folder. In Obsidian, the Dataview plugin can turnstateinto a live board, e.g.:TABLE state, state_history[-1].date AS "last moved" FROM "Notes" SORT state -
Logseq — pages are markdown files too, but Logseq's own metadata lives as a
key:: valueline on the first block rather than a---frontmatter block. Keep frontmatter and Logseq properties side by side (Logseq ignores the frontmatter block,note-stateignores the::line), or adapt the parser innote_state/cli.pyto readstate:: draftinginstead. -
Notion (or any database-backed app without local files) — recreate
states.yamlas a Status property with matching options, and use a Board view grouped by it. You losenote-state's enforcement of transition rules unless you script it against the Notion API, but the state vocabulary and pipeline shape carry over directly.
Design notes
- Backward moves are allowed on purpose. Review can send a note back to Drafting; Formatting can send it back to Review. Knowledge work isn't strictly linear, and a state system that can't represent "this needs another pass" will just get ignored.
needs_revisionis a trapdoor, not a stage. It exists so a problem found at any point can be flagged without losing the note's place in the pipeline — it always returns to wherever it came from.--forceexists deliberately. Rules are useful defaults, not a cage; sometimes you really do want to fast-track or bulk-correct a note.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file note_state-0.2.0.tar.gz.
File metadata
- Download URL: note_state-0.2.0.tar.gz
- Upload date:
- Size: 17.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
70b975b5e4c6328b648fbc46eb0ba9f606df256fbb35137bd38f75d3783b1c68
|
|
| MD5 |
34f5103802de296441a4aaf58ff99424
|
|
| BLAKE2b-256 |
bdc677d064e18076a7ed53451fa4ca4b8e7086f1e012ddbf1de93cafba9ed7cc
|
File details
Details for the file note_state-0.2.0-py3-none-any.whl.
File metadata
- Download URL: note_state-0.2.0-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
845e11d67761e5215d3ed5e6b98ab87a8230f158543beb69910e52f5dbd20596
|
|
| MD5 |
57cecb141f0a47a072f2b91096251594
|
|
| BLAKE2b-256 |
dc8571a0cd016c844734a429a27f8d743cf8eadf4d7bbc638807aba377f5428c
|