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.
Create the manifest
Section titled “Create the manifest”Use the js runtime and request only the capabilities the plugin uses. This
example registers a command and shows a toast:
{ "$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:
bun add --dev @jrtilak-recall/runtime typescriptImport runtime definitions with import type. Recall supplies the actual
context when the plugin runs.
Implement the lifecycle
Section titled “Implement the lifecycle”Export an async init(ctx) function and, when the plugin owns long-lived
resources, an async unload(ctx) function:
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.
Use the runtime context
Section titled “Use the runtime context”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 permission | Context API |
|---|---|
registry.command | ctx.commands |
registry.menu | ctx.menus |
registry.sorting-method | ctx.sortingMethods |
registry.highlight-color | ctx.highlightColors |
api.toast | ctx.toast |
api.dialog | ctx.dialogs |
api.app-state | ctx.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.
Build the plugin
Section titled “Build the plugin”Plugin Creator bundles the entry into the module format required by the mobile sandbox:
bunx @jrtilak-recall/plugin-creator build ./focus-tools --minify --zipUpload the resulting dist.zip. Rebuild existing plugin archives after
changing source or upgrading to a Plugin Creator release that changes the
runtime bundle format.