Skip to content

Plugin Lifecycle

Recall owns plugin installation, loading, and activation. A plugin participates in this lifecycle through the entry declared in its manifest. JavaScript plugins export lifecycle functions, while theme plugins provide validated JSON.

Set recall.entry in the plugin’s package.json:

{
"recall": {
"manifestVersion": "0.1",
"permissions": ["registry.command", "api.toast"],
"entry": {
"runtime": "js",
"file": "src/index.ts"
}
}
}

entry.file is relative to the plugin root. The Plugin Creator builds this source entry and writes the resulting path into the packaged manifest.json. Recall then loads the declared built file when the plugin is activated.

See Plugin Schema for all manifest fields and entry-path rules, and Plugin Creator for the build command.

A JavaScript entry exports init(ctx) and can optionally export unload(ctx). Recall runs every enabled JavaScript plugin in a separate headless runtime, so context methods are asynchronous RPC calls and lifecycle functions should be async.

import type { RuntimeCtx } from "@jrtilak-recall/runtime";
const cleanups: Array<() => Promise<void>> = [];
export async function init(ctx: RuntimeCtx) {
if (!ctx.commands || !ctx.toast) return;
const command = await ctx.commands.register({
id: "focus-tools.start-session",
name: "Start focus session",
async run() {
await ctx.toast?.success({ title: "Focus session started" });
},
});
cleanups.push(() => command.dispose());
}
export async function unload(_ctx: RuntimeCtx) {
for (const cleanup of cleanups.splice(0).reverse()) {
await cleanup();
}
}

The example requires registry.command and api.toast in recall.permissions.

Recall calls init after an enabled plugin and its entry are ready to run. The host currently prevents a successfully initialized plugin id from being initialized again until it has been unloaded. Plugins should still keep their own setup and cleanup straightforward instead of relying on repeated init calls.

Recall calls unload only for a JavaScript plugin that completed init and is currently loaded. The function is optional, but any plugin that owns resources with a lifetime beyond init should provide it. The host also force-disposes tracked registrations when a plugin unloads or its sandbox fails.

Plugins do not render React Native components. Menus, toasts, dialogs, and other supported contributions are data or actions rendered by the host app.

Release everything created by the plugin when unload runs. Common examples include:

  • Registry handles returned by commands, menus, sorting methods, or highlight colors.
  • Runtime subscriptions and external event listeners.
  • Timers, background tasks, and connections started by the plugin.
  • Module-level references that should not survive the plugin lifecycle.

If init throws, Recall does not mark the plugin as loaded. The host releases tracked resources and destroys the failed sandbox. Plugins may still use try/finally when setup owns resources outside the runtime context.

Disabled plugins are not initialized. When Recall processes a disabled plugin that was previously loaded, it unloads that plugin instead of running it. Activation changes may be applied by the host during its next plugin initialization cycle, so do not depend on plugin code running while the plugin is disabled.

Theme entries use runtime: "theme", point to a theme JSON file, and request the registry.theme permission:

{
"recall": {
"manifestVersion": "0.1",
"permissions": ["registry.theme"],
"entry": {
"runtime": "theme",
"file": "src/theme.json"
}
}
}

Theme plugins do not export init or unload. Recall reads the entry, validates it against the theme schema, and contributes its themes to the host. See the theme plugin contract for the file format and schema URL.

Recall rejects plugin metadata and entry files that fail the applicable validation. A malformed manifest or theme, a missing or unsafe entry path, or manifest name and version values that do not match the installed plugin can prevent activation.

Errors thrown while loading the JavaScript module or running init also fail that plugin’s initialization. Other plugins can continue initializing, so each plugin should throw actionable errors rather than hiding a failed setup.