william-saxton390 downloadsLong-form writing with user-defined structure (novel, chapter, scene, or anything you like) and a type-aware compile pipeline.
Long-form writing in Obsidian with a structure you define and a compile pipeline that understands it.
Novelr is inspired by Longform and novelWriter. Where Longform gives you a flat list of scenes, Novelr lets you decide the shape of your project: a novel made of chapters made of scenes, a novel with parts, a screenplay with acts and sequences, or anything else. Containers are real folders and content nodes are real notes, so your vault stays plain Markdown that any other tool can read.
Requires Obsidian 1.13 or newer.
main.js, manifest.json and styles.css from the latest release into <vault>/.obsidian/plugins/novelr/, then enable the plugin.william-saxton/novlr in the BRAT plugin to follow releases before the directory listing.novelr.md) whose novelr property holds the title, schema, ordered tree, and compile settings. Order lives in the index, so reordering never renames files.Notes that already exist in the project folder but are not in the index appear under Needs attention; add them with one click or ignore them with a glob pattern.
---
novelr:
version: 1
title: The Hollow Road
workflow: Manuscript (default)
ignore:
- _notes/**
schema:
rootType: novel
types:
- id: novel
name: Novel
kind: container
allowedChildren: [chapter]
- id: chapter
name: Chapter
kind: container
allowedChildren: [scene]
- id: scene
name: Scene
kind: content
tree:
- chapter: Chapter One
children:
- scene: Opening
- scene: The Call
- chapter: Chapter Two
children:
- scene: Aftermath
---
Each tree entry is <typeId>: <name>, optionally with children. Paths are derived from the tree: Chapter One/Opening.md is a scene inside the Chapter One folder. You can edit this by hand; Novelr tolerates mistakes and reports them in the pane.
Content notes get a novelr-type property when Novelr creates them, so Dataview and friends can query by type. Add novelr-skip: true to a note to leave it out of every compile.
Every node can carry a status such as New, In progress or Done. Statuses are defined per project in the Project tab and each one has:
Pushing works all the way up. With the defaults, a chapter marked Done that gains a New scene shows In progress (hollow dot, tooltip says why), and so does the novel above it. When the scene is finished, the chapter shows Done again. If several children push different statuses, the one listed first wins, so order the list from "most attention needed" down.
Colors are the eight theme accent colors or any hex value: the plus swatch opens a color picker, and custom colors are kept in a vault-wide palette (right-click a custom swatch to edit or remove it).
Click a node's dot, right-click and choose Set status…, or run Novelr: Set status of current node. Content notes also get a novelr-status property when "Write node type and status to files" is on.
novelr:
statuses:
- { id: new, name: New, color: blue, parent: in-progress, default: true }
- { id: in-progress, name: In progress, color: yellow, parent: in-progress }
- { id: done, name: Done, color: green }
tree:
- chapter: Chapter One
status: done
children:
- scene: Opening
status: new
Proof readers and editors who share the vault can leave comments on your notes, and you can action them from a sidebar. Comments are built to travel over Obsidian Sync (or git, or any file sync):
_comments, changeable in the Project tab and saved in the index note so everyone uses the same folder). Markdown always syncs, and separate files mean two people commenting at the same time never overwrite each other. Nothing lives in the plugin's data.json.Select text in a note that belongs to a project, then right-click › Add comment to selection or run Novelr: Add comment to selection (or note). With nothing selected the comment applies to the whole note. Commented passages are highlighted in the editor (turn this off in settings); click a highlight to show its comment.
A comment on a selection can also carry a suggested change: switch on Suggest a change in the comment dialog and type the replacement. The card then shows the old and new text with Accept, Reject and Resolve buttons. Accept rewrites exactly that passage in the note and records the comment as accepted; Reject resolves it as rejected without touching the note; Resolve closes it with no verdict. If the passage has been edited since the suggestion was made, Accept is disabled and the change has to be applied by hand. Suggestions live in the comment file as suggestion: "…" and the outcome as resolution: "accepted" | "rejected".
The Novelr comments pane (right sidebar, Novelr: Open comments pane) lists comments for the active note or the whole project, filtered by open or resolved, searchable and sortable by position or age. Each comment can be jumped to, replied to, edited, resolved or deleted. Resolving keeps the file with status: resolved so the author of the comment sees what happened; deleting moves it to the trash. The structure pane shows a count of open comments next to each note.
A comment file looks like this:
---
novelr-comment: 1
id: "20260926-134012-k3x9"
note: "Chapter One/Opening.md"
author: "Jane"
created: "2026-09-26T13:40:12.000Z"
status: "open"
quote: "The rain fell in torrents"
before: "It was a dark and stormy night. "
after: ", except at occasional intervals"
offset: 32
---
Too many adverbs here. Consider cutting the second clause.
note is relative to the project root and follows the note when it is renamed or moved. Replies carry reply-to: "<id>". You can write or fix these files by hand; anything the plugin cannot read is simply not shown.
A workflow is a sequence of steps of four kinds:
| Kind | What it sees | Built-in steps |
|---|---|---|
| Node | every node whose type matches the step's Apply to list (empty = all) | Strip frontmatter, Remove links, Remove comments, Remove strikethroughs, Remove headings, Insert before, Insert after, Find and replace, Trim whitespace |
| Structure | the whole tree, before it is built | Filter by status |
| Build | the whole tree, once | Build manuscript |
| Manuscript | the flattened text | Normalize blank lines, Find and replace, Add frontmatter, Save as note |
Node and structure steps come first, then one build step, then manuscript steps. Filter by status leaves out nodes with chosen statuses (a container's effective status counts, so a whole In progress chapter can be dropped) or keeps only content with chosen statuses, and renumbers what remains. Insert before and Insert after attach text to nodes of a given type, which is how you get chapter headings, part pages or scene separators:
chapter with # Chapter {number}: {title}scene with * * * and Skip the first onpart with {PB}# Part {number:Roman}{BR}{BR}## {title}| Placeholder | Meaning |
|---|---|
{title} {type} {status} {status.id} |
the node's name, type id, effective status name and id |
{number} |
position among siblings of the same type (1-based) |
{count} {index} {absolute} {depth} |
siblings of this type, 0-based position among all siblings, position of this type across the whole project, nesting depth |
{number:word} {number:Word} {number:WORD} {number:roman} {number:Roman} {number:pad2} |
number formatting (works on any numeric placeholder) |
{parent.title} {parent.number} |
the containing node |
{chapter.title} {chapter.number} |
the nearest ancestor of that type (any type id works) |
{project.title} {date} |
project title, today's date |
{BR} {PB} |
line break, page break (configurable in settings) |
---- |
as the whole value, a horizontal rule |
Novelr does not load or evaluate script files. Other plugins (including a small personal one) can register compile steps through the public API instead:
// In another plugin, after Novelr has loaded:
const novelr = this.app.plugins.plugins["novelr"];
novelr?.api.registerStep({
description: {
canonicalID: "my-plugin:shout", // prefix with your plugin id
name: "Shout",
description: "Uppercases every targeted node.",
kind: "node", // "node" | "tree" | "join" | "manuscript"
external: true,
options: [{ id: "suffix", name: "Suffix", description: "", type: "text", default: "!" }],
},
compile(node, ctx) {
if (node.kind === "content") node.text = node.text.toUpperCase() + String(ctx.options.suffix);
},
});
// and in onunload: novelr?.api.unregisterStep("my-plugin:shout");
Registered steps appear under "From other plugins" in the workflow editor. Node steps receive (node, ctx) and mutate node.text, node.before or node.after. Tree steps receive (root, ctx) and may prune or reorder children. Build steps receive (root, ctx) and return a string. Manuscript steps receive (text, ctx) and return a string. ctx.format(fmt, node) expands placeholders and ctx.app is the Obsidian app.
Turn on Enable grammar checking in settings and Novelr can send a note to a Claude model and record what it finds as regular Novelr comments, anchored to the passage concerned and authored as "Grammar check":
Grammar check on "She walk to the door" Subject-verb agreement: third-person singular needs "walks".
Suggested: She walks to the door.
When the checker proposes a concrete replacement it is attached as a suggestion, so one click on Accept fixes the text. They sit in the comments pane beside everyone else's, are highlighted in the editor, and can be rejected, resolved, replied to or deleted like any other comment. Run a check from the command palette (Check grammar in current note, Check grammar in current container) or right-click a node in the structure pane; containers are checked one note at a time. Remove grammar comments deletes the checker's comments on a note and leaves human ones alone.
The check runs through the Claude Code CLI (claude -p) using whatever login and plan the CLI already has, so Novelr never stores an API key. It is desktop only and needs the note to be part of a project. Settings let you pick the model (--model, default sonnet), add author guidance such as the dialect you write in, cap findings per note, and choose whether the checker's earlier comments on a note are replaced. Nothing is sent anywhere unless you run a check.
novelr property). It does not read file contents to do this.npm install
npm run dev # rebuilds on change and copies into test-vault/.obsidian/plugins/novelr when that folder exists
npm test
npm run lint
npm run build