github ZenNotes/zennotes v2.20.0
ZenNotes v2.20.0

latest release: v2.20.1
5 hours ago

Warning

2.20.0 does not launch. Please install 2.20.1 instead.
On every platform this build stops with "A JavaScript error occurred in the main process" before opening a window. Nothing is wrong with your vault. 2.20.1 contains everything below, with the launch fixed.

ZenNotes 2.20.0

Installers for macOS, Windows, and Linux (x64 + arm64) are attached below.

✨ Features

  • Workflows: repeatable pipelines over your notes, drawn as a graph you can read. Anyone who keeps a vault long enough ends up with a ritual: find the notes tagged #book, keep the good ones, sort them, paste a table into the reading log, tag the rest for later. Workflows turn that ritual into a file. A workflow is a plain .md under .zennotes/workflows/, a few lines of frontmatter and a pipeline per line: books = tag #book, then good = books | where rating >= 4 | sort finished desc, then good | render table title, rating, finished | write-section "Reading Log.md" "Finished". Naming a wire is the whole grammar; reading the same wire from two lines is a branch, no extra syntax. The canvas renders that text as a graph, and the text renders back from the canvas, losslessly in both directions, because the layout is computed rather than stored: no coordinates in your file, clean diffs in a git-backed vault, and dragging a node saves only that node's position as a single frontmatter key, which you can delete to snap back to auto-layout.

    The part that makes it trustworthy is that wires carry note sets. Every wire on the canvas shows the live count of notes flowing through it, and the whole graph plans without writing: the engine structurally cannot touch disk, it only proposes changes. Press Run and you get the dry-run confirmation first, every change grouped and counted ("write-section Reading Log.md · Finished", "add-tag #someday ×2"), with the paths in the tooltips. Apply, and every file's pre-run bytes are journalled before the first write: Undo restores every file the run wrote, byte for byte, and a run that fails midway rolls the whole thing back on its own, including the steps that had already landed. Half-applied is not a state this feature is allowed to leave you in, and that holds even if the app is killed mid-apply: each file's pre-run bytes reach the disk before the write they cover, so a run the process never finished is found on the next launch and offered back to you as an undo rather than left as a silence. Undo is also honest about what it costs. It always restores the run's pre-run bytes, so if you edited one of those notes yourself after the run, it names the files it overwrote instead of quietly reverting your afternoon. Sinks that cannot be undone (a notification, the clipboard) are declared in the confirmation before you agree, not discovered after. And the sources mean what they say: all, tag and search cover your working notes only, with the Trash and the Archive reachable solely by naming them (folder trash), so a bulk retag can never edit files that vanish on the next empty-trash.

    You do not have to start from a blank line. New workflow opens a gallery of eight working recipes, each a readable file that copies into your vault and opens in the same editor as anything you write yourself: run it as is, or change one filter and it is yours. The gallery is also yours to prune: press x on a recipe you will never use and it leaves the gallery entirely, with a one-line breadcrumb in the footer saying where it went. Settings → Workflows hides or restores all eight in one click, and the hidden list travels in config.toml like any other preference. The editor completes as you type (verbs, your actual tags, fields, folders and note paths), a Syntax panel documents every step with a real example, and diagnostics mark broken lines in place while the rest of the graph keeps working. New workflows start as drafts, which are saved but inert; activating one shows exactly what it writes before it may run. Importing a workflow someone sent you is a review, never an install: it has to parse, what it writes is spelled out up front, any trigger it asked for is stripped back to manual, and it lands as a draft. Sharing out is just the file, over the clipboard or as an export. The whole feature is keyboard-first like everything else here: Space a opens the view, j/k walk the list, Enter opens, R runs, t flips draft/active, e drops to the text, ? opens the syntax reference, and every binding is rebindable in Settings or config.toml.

    Scope worth stating: workflows are off by default. They can rewrite notes in bulk and the graph editor asks more of a new user than any other view, so the feature is a deliberate opt-in: flip the one toggle under Settings → Workflows and the view, its sidebar row, its command and its Space a binding all appear together. Workflows are desktop-only in this release and run when you run them. Event and schedule triggers parse in the format but do not fire yet, and the web client shows workflows read-only. No code steps, no shell, no network: a workflow can only do what the app can do, which is precisely why it is safe to run one you did not write.

  • Run a workflow from the command palette, from anywhere. Running a pipeline should not require visiting the view that edits it. Every active workflow is now a palette command (Run: Reading log), and Run Workflow… opens a browsable picker in the same shape as Switch Vault: your workflows listed with their descriptions, a writes tag on the ones that change notes, filter and Enter. Both paths walk the exact same trust ladder as the view's Run button: fresh plan from what is on disk, the unsaved-edits question if the run would write a note you are mid-edit in, the dry-run confirmation, then apply. The receipt arrives as a toast that carries the Undo button with it, because you never left whatever you were doing, and it waits twelve seconds instead of four so the offer is real. Runs made from the view are even harder to lose: the receipt card sits in the view's footer, Undo included, and survives you leaving to glance at a note, so the offer outlasts the toast. Drafts deliberately get no palette entry: a row whose only possible answer is an error is worse than no row.

  • A guided tutorial that teaches workflows on real material, then cleans up after itself. A pipeline language is learned by running one, not by reading about one. Settings → Workflows → Start tutorial (or "Start Workflows Tutorial" in the palette, findable before you have opted in, because starting it IS the opt-in) seeds a small practice folder, three rated book notes and a reading list, plus one draft workflow that reads and writes only inside that folder, then walks nine short chapters on the real view: select it, read the canvas, flip to the text and back, edit a step and watch a wire count change, activate, run it against the dry-run confirmation, undo it byte for byte, and find it in the palette. Every chapter is a todo list that checks itself off as you actually do each thing, in order, and a chapter only turns its own page when you performed its action while it was showing. The chapter card is draggable, because whatever it happens to cover is exactly what some chapter will ask you to press. Finish (or leaving early) removes the practice folder, the tutorial workflow, and its run history: your vault ends exactly as it started, which is a promise the tutorial also keeps if it is abandoned halfway and started again later.

  • Bring your own language: import a TextMate grammar and your fenced code blocks highlight. ZenNotes ships highlighting for the usual suspects, and until now anything outside that list rendered as flat grey text. If you write in a niche language, or you are the person who invented it, that was the end of the road. Now it is a file you drop in: grab that language's .tmLanguage.json, the same format VS Code extensions use so nearly every language already has one, then Settings → Editor → Languages → Add language. Pick which fence tag it owns plus any aliases, check the live preview against a sample snippet, install. From that point ```gleam highlights while you type, in split, and in the reading view, using your current theme's colours rather than a palette of its own. Grammars live as ordinary files under languages/<id>/ in your config directory and are watched, so editing one repaints open notes immediately, and your notes are never modified: this is display only.

    The guardrails are the interesting part, because a grammar is someone else's file. Built-in language and diagram tags are reserved, so nothing can quietly take over python or mermaid. Grammars that depend on external scopes (the ones that embed another language, like Vue templates) are rejected up front with a message saying so, rather than half-working. There are caps on grammar size and code-block size, and a grammar that cannot keep up is refused rather than tolerated: before an import counts as installed, the grammar is run off the UI thread against a corpus of the repetitive lines that make backtracking patterns explode, with a hard timeout, and one that fails to finish is declined with a message saying so. A grammar that slips through and turns out slow on your actual notes is timed while it works and dropped to plain text for the session, so a pathological pattern costs you one slow moment rather than one per keystroke. Installs are atomic with a rollback, and built-in languages always win even if a conflicting alias is added by hand. Highlighting only: no completion, no formatting, no language server. Desktop only this release. Costs nothing at all if you never install one, since none of the engine is loaded until you do. (#406, contributed by @rossirpaulo, who writes their own language and wanted to stop losing highlighting by moving to ZenNotes)

  • Renaming a note carries its title heading along. A new note is created as # <title> with an empty body, so the heading and the filename start out identical. Then the first rename separates them, leaving # Untitled sitting above a note called something else. Every rename was really two edits: the one you asked for, and a manual fix to line one. Now the heading follows the name, from the breadcrumb, the sidebar, the note list, or the palette, and whether or not the note is open. The new toggle is Settings → Editor → Writing → Sync title heading on rename, on by default, and it travels in config.toml like every other preference.

    The rule is deliberately narrow, because rewriting the text of a note you did not ask to edit is the kind of help that has to stay predictable: an existing top-level # heading is retitled, and one is never invented. A note that opens with prose, a list, or a ## heading is left byte-identical, which makes deleting the # line a permanent per-note opt-out rather than something the next rename quietly undoes. The heading is found after any frontmatter, so a --- block does not hide it; CRLF endings and indentation survive untouched; and Obsidian drawings stored as .md are skipped entirely, since their # Excalidraw Data is structure rather than a title. Renames from MCP, the CLI, and the HTTP API stay purely mechanical: this is a thing the app does when a person renames a note, not a rule imposed on the vault. (#455, requested by @Denatle)

  • Your folders can BE the system folders: remap Inbox, Quick Notes, Archive, and Trash to any directory in your vault. Until now the four system folders were hardcoded directory names, and pointing ZenNotes at an existing vault meant reorganizing it: an Obsidian vault with 01 - Entry for capture had to rename it inbox/ or lose the Inbox view. Settings → Vault → Folder Paths now maps each system folder to a directory of your choosing, stored per-vault in vault.json like the primary-notes mode, so the mapping travels with the vault to every device and to a self-hosted server. Classification follows everywhere: sidebar counts, task scanning, search, the web app, the MCP server, and workflows all treat the remapped directory as the system folder it stands for, and creating, archiving, or trashing a note files it into your directory rather than inventing a fresh inbox/ or trash/. Validation keeps it sane (single directory names, no collisions between folders, reserved names refused), and the server enforces the same rules, so a hand-edited vault.json cannot point folder operations outside the vault. Workflows keep their trust promise on remapped vaults too: all still cannot touch the Trash or the Archive whatever they are called, and a workflow's trash step files into the directory the app actually watches. (#115, requested by @danscheer, @TomSchimana, and @hotondo; built by @wholetthekfcgo in #398)

  • Notes grew real exits: Word export, copy-for-email HTML, and every export finally states its title. Sharing a note with someone outside your vault used to mean choosing between three degraded copies of it. The PDF opened mid-thought, because a title kept in frontmatter (where ZenNotes puts it) was treated as metadata and dropped. There was no editable format at all for the collaborator who wants to comment and restyle rather than read. And pasting rendered HTML into an email arrived as bare unstyled text, since mail clients strip the classes and stylesheets the reading view depends on. All three exits are now real, and they came from one user report (thanks Jan!):

    Export Note as Word Document… writes a .docx built from Word's own vocabulary, not a frozen picture of the app: your frontmatter title lands as the document's Heading 1, headings map to Word's real Heading styles, links use the Hyperlink style, lists are actual Word numbering, tables are tables, task lists keep their checked and unchecked glyphs, ==highlights== highlight, code keeps its mono block, and vault images are embedded (resized to fit the page, re-encoded when Word would not understand the format). Because the structure is Word's own, the person you send it to restyles the whole document by editing Heading 2 once, exactly as they would a document born in Word. Copy Note as HTML puts a fully inline-styled rendering on the clipboard, styles attached to every element rather than a stylesheet, because that is the only styling mail clients reliably keep; paste into an email and the headings, tables, code and task glyphs survive. And the PDF export states the title: the frontmatter title: (or the note's name) is written as an opening H1, in the document body and in the PDF's metadata title, so the export no longer starts mid-thought. A note that already opens with its own # heading is left alone; nothing is ever double-titled. All three respect the existing export styling, and the two new commands sit in the palette next to Export as PDF.

  • MCP answers are now clickable: every note result carries a zennotes:// link. Point an AI client at the ZenNotes MCP server and ask about your vault, and until now the answer was a list of bare paths; the best a model could do was invent a file:// link that routes through the OS "pick an app" dialog. Now every note-shaped result (listings, searches, text hits, tasks, and the receipts from create/rename/move/duplicate) carries a link field, a zennotes://open?path=... URL, and the server's instructions teach models to render note titles as markdown links with it, unprompted. Ctrl+click one in your chat and ZenNotes comes to focus with that note open; it works with the app closed too, since the OS launches it. The URL scheme has been registered on all three platforms since the links feature shipped, and the parser refuses absolute paths and traversal, so a link can only ever name a note inside your vault. Whether a given chat client makes custom-scheme links clickable is the client's call, but the standard ones prompt and open. (#509, requested by @MrClyfar)

  • Ctrl+O opens a file on Windows and Linux. 2.16.0's "Open File…" bound ⌘O through the macOS File menu, and the other platforms deliberately have no application menu, so the chord did nothing there and the only route was the command palette. It is now a real keymap (global.openFile), which also makes it rebindable everywhere, something a menu accelerator never was. Vim keeps every claim it already has on Ctrl+O: normal and visual mode keep the jump-back, insert mode keeps i_CTRL-O, so in Vim mode the chord opens the picker when focus is outside the editor, and with Vim off it is simply Ctrl+O. Reported on Discord by Olivier, catching up from 2.9.0 on arch/hyprland, whose report also produced the two CLI fixes below.

🐛 Fixes

  • zn open against a running app no longer piles ZenNotes icons into the Dock. Opening a file while the app was already running did the right thing on screen: the file appeared in the running window. But the helper process that delivered it lingered for a minute and a half before quitting, because on macOS Electron never promptly fires ready in a process that lost the single-instance race, and our quit was parked behind ready. Anyone feeding files in steadily (say, an agent handing you generated markdown one zn open at a time) stacked up Dock icons that looked exactly like ZenNotes multiplying. The loser now exits the instant the lock is denied, which is safe because its arguments are handed to the running app before the denial even returns: delivery is unchanged, and the helper is gone in about a quarter of a second instead of ninety. (#511, reported by @rrpolanco)

  • A table-menu action can no longer vanish because you had just typed in a cell. Third and final act of #485. Type into a table cell, then open the table menu and pick an action, and the action silently did nothing; picking it again worked. The chain: opening the menu moves focus out of the widget, leaving a just-edited cell commits it, that commit rebuilds the table widget, and the menu you are looking at still belongs to the old, detached widget, whose commit then died in a silent early-return. It hit fresh tables hardest because their first commit also writes the column-widths marker, so even trivial edits forced the rebuild. The menu now records where its table lives when it opens and, if its widget has been rebuilt out from under it, commits against the current document at that anchor instead; and a commit that cannot find its table logs a warning rather than discarding your action without a word, which is how this bug survived two earlier fixes. Diagnosed frame by frame from the reporter's screen recording, which is what finally made it reproducible: the missing ingredient was typing in a cell right before opening the menu. (#485, reported by @uNyanda, whose recording cracked it)

  • Typing [[ works again on German-family Mac keyboards. On a German, Austrian, or Swiss Mac layout, [ is Option+5, and 2.19.0 quietly claimed that exact chord: the new hop-across-markers shortcut shipped bound to Alt+[ and Alt+], so the keystroke that should have typed your second bracket moved the cursor instead, right when you were mid-wikilink. It felt haunted because the first bracket always worked: with nothing on the line to hop across, the shortcut declined and let the key through, so the failure only began once the auto-closed pair existed. The hops now default to Ctrl+, and Ctrl+. on macOS (the keys carry < and >, which is the direction they hop; Windows/Linux keep Alt+[ and Alt+], where AltGr layouts never collided), and both remain rebindable under Settings → Keymaps. While in there, a papercut for every layout: typing [[ now opens the note picker immediately, where before it waited for the first letter of your query, because the auto-closing bracket's edit was invisible to autocomplete's activate-on-typing. (#514, reported by @ryanthara)

  • Databases work on remote workspaces. Connecting the desktop app to a ZenNotes server showed your databases in the sidebar and then refused to do anything with them: clicking one died silently in the developer console, and creating one did nothing at all. Every database operation was guarded local-only, with a comment claiming the server had no database endpoints yet. The premise was stale: the web app has been composing full database support out of the server's ordinary file reads and writes all along, no database endpoints involved. That composition now lives in one shared module bound to each transport (the web bridge binds it to its HTTP functions, the desktop binds it to the remote-workspace client), so opening, editing, creating and renaming databases now works against any server version, including adopting a bare data.csv someone dropped into the vault, and the on-disk format stays byte-identical everywhere by construction. Local vaults keep their direct-filesystem path untouched. Two honest notes: remote databases refresh when reopened (like the web app) rather than watching the filesystem live, and database failures are no longer silent anywhere: a load or create that fails now says so in a toast instead of dying in the console, which is how this sat unreported-looking for so long. (#499, reported by @lawtj)

  • Remote workspaces fail loud and recover, instead of failing strange. Two rough edges surfaced while fixing #499. First: if the server was unreachable, anything that read a file over the connection treated the failure as "this file does not exist", and for databases that meant a dropped connection could silently close your database tab and forget it. The client now distinguishes a server that answered "not found" from a server that never answered: absence is only ever concluded from an answer, and a connection failure surfaces as a visible error instead (the server also reports missing files as a proper 404 now, though the desktop tolerates older servers that said 500). Second: launching the desktop app while its configured server was down dumped you on the first-boot "Welcome to ZenNotes" screen, as if nothing had ever been set up, one click away from switching modes. A configured-but-unreachable workspace now gets its own screen: it names the server, says plainly that your notes live there and nothing has changed, and offers Try again (which re-runs the normal boot, saved credentials included), Edit server connection, and an explicit local-vault escape hatch.

  • Your pre-2.10 zen CLI heals into zn on launch. The CLI was renamed in 2.10.0 because zen collides with Zen Browser, but the rename only ever migrated inside an explicit Install click. Anyone who installed on 2.9.0 or earlier and never re-ran it kept a working zen and no zn at all, while Settings told exactly those users the CLI was "not installed", because the status check only ever looked for the new name. The app now migrates the managed symlink at launch: zn appears beside your old zen, the legacy name is removed, and every documented command finally works as written. A foreign zen (Zen Browser's) is never touched, and the status read recognises a legacy install instead of denying it.

  • zn open <folder> stops failing silently. Opening a folder as a focused session shipped in 2.17.0, but four quiet failure paths could make it look absent, worst on Wayland. The app dropped folders whose notes sat behind a bounded scan's blind spots (4,000 entries, no dot-directories) while the CLI had already printed success; the CLI now watches its detached child's first half-second and reports a launch that dies instead of claiming victory; on Hyprland, focusing an already-open folder did visibly nothing because a Wayland client cannot raise itself, so Linux now pulses always-on-top as the portable nudge; and a zn open racing the app's own startup queued its paths into a queue nothing drained. All four are fixed, "Open with ZenNotes" from a Linux file manager works (the .desktop's file:// URLs were being discarded), the AUR package now pulls desktop-file-utils so zennotes:// links actually register, and the how-to finally documents the folder form. (Discord, Olivier)

  • Walking a Kanban card quickly no longer loses it, in either sense. Move a task across the board with quick Shift+H presses and the second hop broke: the focus ring vanished, and, invisibly, the move itself reverted on disk, so the task never arrived where you sent it. Two races compounded when the next press landed before the previous write settled: the task mutation handed its change to an editor autosave that does not exist for a note nobody has open (the previous write's watcher reload then clobbered the pending edit), and the optimistic overlay shielding the moved card dropped too early, letting a stale refresh snap the card back under the cursor. Clean notes now write straight to disk, and the overlay is retired only by its own guarded timer, so the ring rides the card through every hop and every hop lands in the file. The slow-motion half of this was #492 in 2.19.0; the fast version needed a custom-status board to pin down. (#503, reported by @dwarjie)

  • Visiting a daily note no longer switches the Calendar panel on everywhere. The panel is deliberately sticky: whatever you set stays while you browse. But the auto-open for periodic notes wrote into that same flag with nothing marking who opened it, so one daily-note visit turned into "the panel is now open on every note", quietly undoing a close you had made elsewhere. The open now carries its provenance: a panel the app opened for a periodic note closes itself on the way out, a panel you opened yourself keeps the sticky behaviour, closing an auto-opened panel by hand sticks, and re-opening it by hand makes it yours. The reporter's step-by-step repro is pinned as a test. (#502, reported by @uNyanda)

  • Middle-clicking a tab closed no longer pastes into the note (Linux). On Linux the middle button is two gestures at once: close this tab, and paste the PRIMARY selection. The tab already refused the click the textbook way (cancelling its own mousedown and auxclick), but under Wayland, Chromium can deliver the paste to the focused editor rather than to the element under the pointer, so whatever you last selected landed in the note that took over after the close. The close now also cancels the mouseup, and arms a short-lived guard that catches the paste event itself wherever it is delivered: one paste, inside a 150ms window, then it disarms, so a paste you actually ask for is never touched. Verified at the event level in the real build; if you are on Wayland and it still bites on 2.20.0, reopening the issue is very welcome. (#498, reported by @ribal-gz)

  • Buttons stay one line tall in narrow windows. Shrink the window enough and the workflows header broke "New workflow" onto two lines while its neighbours stayed short, which read as a squished row. The shared button primitive now refuses to wrap or shrink its label anywhere in the app; in a tight row the truncatable things beside it (a keyboard hint, a file path) give way instead, which is what they were for. Where a row genuinely cannot fit its buttons, it wraps them onto their own line rather than clipping actions off the edge of the pane, which the workflow header now does. Found in-house while testing 2.20.0.

  • The app no longer loads 2.5 MB of diagram code every time it starts. Mermaid is meant to arrive only when a note actually contains a mermaid block, and in the source it is written that way: the reading view is lazy-loaded, and mermaid behind a second lazy import inside it. The bundler was undoing that. A rule that grouped mermaid, cytoscape and dagre into one named chunk pulled the whole thing back into the app's startup path, so every launch fetched and parsed all of mermaid, plus the markdown renderer it depends on, before you had opened a single note. Dropping that rule lets the bundler keep its own boundaries, and the result is better than the original intent: drawing a flowchart now pulls three small pieces (the mermaid core, the flowchart parser, and the layout engine) instead of one bundle containing every diagram type you might have asked for. Same download size overall, just not at startup. Diagrams render exactly as before, verified in the real app. The same sweep caught the workflows canvas library about to make the same mistake from two directions (a chunking rule and a static import), so the graph view's code now loads when you open Workflows, not when the app boots: the feature costs nothing if you never opt in, and that claim is now enforced by the bundle layout rather than intended by it.

  • Two check-then-use windows are closed. CodeQL flagged both on this release's own new code, and both got the atomic version rather than a re-check: importing a workflow file used to stat the path and then read the same path, so the size that was checked was not necessarily the size that was read; one file handle now serves both, and the byte cap is re-checked on what actually arrived, so a file swapped or grown inside the window is refused rather than imported. And the custom-languages setup wrote its README behind an exists-check; it now uses exclusive create, so an existing file is never overwritten and nothing fits in the gap. Small windows, but the whole point of the workflows import path is that it reviews before it trusts.

  • Building from source: turbo run typecheck test:run no longer trips over itself. Both tasks funnel through the step that copies the web build into the Go server's embed directory, and turbo runs them concurrently, so one process could delete the directory while the other was still copying into it. The loser failed with an error that pointed nowhere near the cause, on a machine where each task passes alone. The copy now stages into a private directory and swaps into place with a single atomic rename; whichever racer loses keeps the winner's identical bytes and cleans up after itself. Only affects contributors and self-builders, but it was the worst kind of red.


Keyboard-first and local-first, as always.

Don't miss a new zennotes release

NewReleases is sending notifications on new releases.