Skip to main content

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-state looks 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 a translating or peer-review state, rewire transitions.
  • Pass --config path/to/states.yaml to 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-state works with zero adaptation: run it straight against your vault folder. In Obsidian, the Dataview plugin can turn state into 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:: value line on the first block rather than a --- frontmatter block. Keep frontmatter and Logseq properties side by side (Logseq ignores the frontmatter block, note-state ignores the :: line), or adapt the parser in note_state/cli.py to read state:: drafting instead.

  • Notion (or any database-backed app without local files) — recreate states.yaml as a Status property with matching options, and use a Board view grouped by it. You lose note-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_revision is 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.
  • --force exists 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

note_state-0.2.0.tar.gz (17.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

note_state-0.2.0-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

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

Hashes for note_state-0.2.0.tar.gz
Algorithm Hash digest
SHA256 70b975b5e4c6328b648fbc46eb0ba9f606df256fbb35137bd38f75d3783b1c68
MD5 34f5103802de296441a4aaf58ff99424
BLAKE2b-256 bdc677d064e18076a7ed53451fa4ca4b8e7086f1e012ddbf1de93cafba9ed7cc

See more details on using hashes here.

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

Hashes for note_state-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 845e11d67761e5215d3ed5e6b98ab87a8230f158543beb69910e52f5dbd20596
MD5 57cecb141f0a47a072f2b91096251594
BLAKE2b-256 dc8571a0cd016c844734a429a27f8d743cf8eadf4d7bbc638807aba377f5428c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page