Skip to content

App State

App state is a small shared store available through ctx.appState. Use it for serializable settings and runtime preferences that need to be shared with the host or other runtime features.

App state is not a database or private plugin storage. Do not store secrets, large content, or values that other permitted plugins must not read. Use a stable plugin-owned path prefix to avoid collisions.

const ENABLED_PATH = "plugins.focus-tools.enabled";

Plugins must request api.app-state before using ctx.appState:

{
"recall": {
"permissions": ["api.app-state"]
}
}

The host makes ctx.appState available only when that permission is granted.

Use get(path) to read the current value at a dot-notation path:

const enabled = await ctx.appState.get("plugins.focus-tools.enabled");

Dot paths address nested state. For example, plugins.focus-tools.preferences.compactMode refers to compactMode inside the preferences object owned by the focus-tools plugin.

Use set(path, value) to write a value:

await ctx.appState.set("plugins.focus-tools.enabled", true);

set also accepts an updater that receives the current value:

await ctx.appState.set("plugins.focus-tools.sessionCount", (count) =>
typeof count === "number" ? count + 1 : 1,
);

Keep stored values small and serializable, such as strings, numbers, booleans, arrays, and plain objects.

Pass a path and listener to observe changes affecting that path:

const unsubscribe = await ctx.appState.subscribe(
"plugins.focus-tools.enabled",
(enabled) => {
console.log("Focus tools enabled:", enabled);
},
);
await unsubscribe();

Always call the returned function when the listener is no longer needed, usually from the plugin’s unload lifecycle.

You can also subscribe to every app-state change:

const unsubscribe = await ctx.appState.subscribe((state) => {
console.log("App state changed", state);
});

Prefer a path subscription when possible so the plugin only processes relevant changes.

list() returns a cloned snapshot of the complete state:

const snapshot = await ctx.appState.list();

replace(nextState) is reserved for internal app operations such as restoring a validated snapshot. It is not part of the plugin API because replacing the complete state could overwrite data owned by the host or other plugins.

All plugin app-state calls are asynchronous because they cross the sandbox boundary. An updater passed to set() reads the latest value and writes its result in two steps; it is not an atomic transaction.