Plugins
Anatomy of a plugin
The files of a plugin package, what each one does, and the naming rules for ids, classes and the entry point.
A vault plugin is a small Swift package. This page explains each file that loam plugin new creates, the fields of plugin.json, and the names the build expects. The examples use the word-count plugin from the quickstart.
The package
.loam/plugins/word-count/
plugin.json id, name, version, library, permissions
Package.swift the package; links LoamRuntime only
Sources/WordCount/WordCountPlugin.swift the plugin class and the entry point
Tests/WordCountTests/WordCountPluginTests.swift one test
README.md a short README to fill in
.gitignore ignores .build/ and .swiftpm/Loam compiles the package into .loam/cache/plugins/word-count/, outside the plugin folder. A logic plugin uses Swift tools 5.10 and targets macOS 13.
Add --ui to plugin new for a plugin with a window part. Its Package.swift targets Swift tools 6.2 and macOS 26, and it also links LoamUIRuntime. See Interface plugins.
Link only the runtime libraries. Do not add other products of the Loam SDK: loam plugin build refuses a library that uses other parts of Loam.
plugin.json
The manifest tells Loam what the plugin is before it loads any code. The generated file looks like this:
plugin.json
{
"id": "word-count",
"name": "Word Count",
"version": "0.1.0",
"description": "",
"icon": "puzzlepiece.extension",
"library": "WordCountPlugin",
"permissions": ["vault.read"]
}id: kebab-case, starting with a letter. It must equal the folder name.nameandversion: the display name and the version.library: the name of the library product. The generated value is<Name>Plugin.description: one line, shown under the name in Settings > Plugins.icon: an SF Symbol name.iconFilecan name an image in the plugin folder instead.permissions: what the plugin does, such asvault.read. You see them before you enable the plugin. See Permissions.
The plugin class
The class conforms to Plugin. It declares its manifest, has an empty init(), and registers its features in activate(context:).
Sources/WordCount/WordCountPlugin.swift
import PluginKit
public final class WordCountPlugin: Plugin {
public static var manifest: PluginManifest {
PluginManifest(id: "word-count", name: "Word Count", version: "0.1.0")
}
public required init() {}
public func activate(context: PluginContext) throws {
// Register commands, services and listeners here.
}
}Name the class <PascalCase id>Plugin. The id word-count gives the module WordCount and the class WordCountPlugin.
Everything you register through context is removed when the plugin turns off. Keep no global state, and do no cleanup by hand.
The entry point
The last function of the file is the entry point. Loam calls it to get the plugin classes, and it records the SDK version the plugin was built against. The generated function is:
@_cdecl("loam_plugin_entry")
public func loamPluginEntry() -> UnsafeMutableRawPointer {
Unmanaged.passRetained(PluginEntry(plugins: [WordCountPlugin.self])).toOpaque()
}Keep it. If the package has more plugin classes, add each one to its list.
Ids, imports and versions
- Every plugin class in the entry must use the package id, or the id followed by a dot:
word-countorword-count.extra. - Name commands, views and panels
<id>.<verb>. Ids are shared by every plugin in the app. - Import only
PluginKit,PluginKitUI,SwiftUIandFoundation. - On macOS, a plugin built once keeps working after app updates that only add to the SDK. Rebuild it when the SDK major version changes.
- Switch over an SDK enum with
@unknown default, because a later release may add cases.
loam guide plugins describes the same package, and the checks that loam plugin build runs.