Skip to content

Building a Plugin

This page walks through writing your first Fern plugin from scratch. By the end you will have a working plugin that adds a keybinding and a command, and you will know where to look to go further.


Prerequisites

Fern plugins are written in Lua 5.4. If you have not used Lua before, the official Getting started with Lua page is a good starting point and introduction to Lua. You don't need to install the Lua runtime, as Fern embeds one, but if you do decide to, adhere to the above version.

You also need plugins enabled in your config:

[plugins]
enabled = true

Step 1 - Create the plugin directory

Every plugin lives in its own subdirectory inside your plugins folder.

~/.config/fern/plugins/
  daily-opener/
    main.lua

Create that structure now:

mkdir -p ~/.config/fern/plugins/daily-opener
touch ~/.config/fern/plugins/daily-opener/main.lua

Step 2 - Write a minimal plugin

Open main.lua in any editor and add:

fern.notify("daily-opener loaded")

Start Fern. Because this plugin was not installed through the plugin manager, Fern blocks it and shows a warning in the status bar. See Plugin approval for details and follow them to approve the new daily-opener plugin.

Once approved, Fern loads the plugin and you should see the notification briefly appear. That confirms the file is being executed correctly.


Step 3 - Open today's journal with a keybinding

Replace the contents of main.lua with:

fern.map("normal", "gd", function()
  local t = fern.now()
  local stem = string.format("%04d-%02d-%02d", t.year, t.month, t.day)
  fern.open(stem, "journal")
  fern.notify("opened " .. stem)
end)

Restart Fern (plugin code runs once at startup). In Normal mode press gd and Fern opens today's journal entry, creating it if it does not exist.

What each line does:

Line API used Purpose
fern.map(...) fern.map Registers the keybinding for Normal mode
fern.now() fern.now Returns a table with year, month, day, etc.
string.format(...) Lua stdlib Builds an ISO date stem like 2026-10-16
fern.open(stem, "journal") fern.open Opens (or creates) the journal entry
fern.notify(...) fern.notify Shows a message in the status bar

Step 4 - Add a command

Commands let users invoke behaviour from the command palette (ctrl+p) or the command bar (:). Add a second registration below the keybinding:

fern.map("normal", "gd", function()
  local t = fern.now()
  local stem = string.format("%04d-%02d-%02d", t.year, t.month, t.day)
  fern.open(stem, "journal")
  fern.notify("opened " .. stem)
end)

fern.add_command("today", function()
  local t = fern.now()
  local stem = string.format("%04d-%02d-%02d", t.year, t.month, t.day)
  fern.open(stem, "journal")
end)

After restarting you can invoke today from the command palette (ctrl+p) or run :today from the command bar.


Step 5 - Persist state across sessions

fern.store_set and fern.store_get give each plugin its own isolated key-value store backed by the vault database. Add a visit counter:

local function open_today()
  local t = fern.now()
  local stem = string.format("%04d-%02d-%02d", t.year, t.month, t.day)
  fern.open(stem, "journal")
  local count = tonumber(fern.store_get("visits") or "0") + 1
  fern.store_set("visits", tostring(count))
  fern.notify("opened " .. stem .. " (visit #" .. count .. ")")
end

fern.map("normal", "gd", open_today)
fern.add_command("today", open_today)

The count persists between restarts because it is written to the vault's fern.db file.


Where to go from here

  • Lua API Reference: full reference for every fern.* function: commands, keybindings, events, timers, block renderers, vault queries, and more.
  • Fern-Terranium: the official plugin registry. If you build something useful, consider publishing it there so other Fern users can install it with a single keypress.
  • Managing Plugins: how to install, update, remove, and approve plugins from inside Fern.