Skip to content

JavaScript Plugin

A JavaScript plugin runs in its own headless JavaScript sandbox. Recall passes it a permission-scoped context for registering behavior and calling host APIs. Plugins cannot render React Native components or access Recall’s private app internals.

Use the js runtime and request only the capabilities the plugin uses. This example registers a command and shows a toast:

package.json
{
"$schema": "https://recall.jrtilak.dev/schemas/plugin-config/v0.1/schema.json",
"name": "@example/focus-tools",
"displayName": "Focus Tools",
"version": "1.0.0",
"description": "Adds focused study actions to Recall.",
"author": "Example Developer <https://example.com>",
"recall": {
"manifestVersion": "0.1",
"category": "productivity",
"tags": ["focus", "study"],
"permissions": ["registry.command", "api.toast"],
"entry": {
"runtime": "js",
"file": "src/index.ts"
}
}
}

Install the runtime package as a development dependency for its public TypeScript types:

Terminal window
bun add --dev @jrtilak-recall/runtime typescript

Import runtime definitions with import type. Recall supplies the actual context when the plugin runs.

Export an async init(ctx) function and, when the plugin owns long-lived resources, an async unload(ctx) function:

src/index.ts
import type {
RuntimeCtx,
RuntimePluginDisposable,
} from "@jrtilak-recall/runtime";
let command: RuntimePluginDisposable | undefined;
export async function init(ctx: RuntimeCtx) {
const { commands, toast } = ctx;
if (!commands || !toast) {
throw new Error("Focus Tools requires command and toast permissions.");
}
command = await commands.register({
id: "focus-tools.start-session",
name: "Start focus session",
description: "Starts a focused study session.",
async run() {
await toast.success({
title: "Focus session started",
description: "You are ready to study.",
});
},
});
}
export async function unload(_ctx: RuntimeCtx) {
await command?.dispose();
command = undefined;
}

init runs when an enabled plugin is loaded. If it throws, that plugin is not activated. unload is optional, but use it to dispose registrations, subscriptions, timers, and other resources created by the plugin. Recall also cleans up tracked runtime registrations if the sandbox fails.

ctx.app is always available and needs no permission. Other fields are present only when the corresponding permission is declared, so their TypeScript types are optional:

Manifest permissionContext API
registry.commandctx.commands
registry.menuctx.menus
registry.sorting-methodctx.sortingMethods
registry.highlight-colorctx.highlightColors
api.toastctx.toast
api.dialogctx.dialogs
api.app-statectx.appState

Context calls cross the sandbox boundary, so registry and host API methods return promises. Await registration, lookup, action, and disposal calls. Host callbacks such as command handlers and toast actions may also be async.

Plugins are headless: use commands, menus, dialogs, and toasts for supported interaction instead of importing React or React Native. Native modules, Node.js APIs, browser networking globals, and private app modules are not part of the plugin contract.

See Core Concepts for individual API guides and Plugin Lifecycle for initialization and cleanup details.

Plugin Creator bundles the entry into the module format required by the mobile sandbox:

Terminal window
bunx @jrtilak-recall/plugin-creator build ./focus-tools --minify --zip

Upload the resulting dist.zip. Rebuild existing plugin archives after changing source or upgrading to a Plugin Creator release that changes the runtime bundle format.