william-saxton38 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
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.
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