Skip to content

Learning Algorithms

Learning algorithms own flashcard scheduling decisions. Recall owns the cards, review history, persistence, settings, gestures, and review UI. An algorithm contributes serializable metadata plus callbacks that create state, evaluate a queue, preview every action, and schedule a completed review.

JavaScript plugins access the registry through ctx.learningAlgorithms after requesting registry.learning-algorithm:

{
"recall": {
"permissions": ["registry.learning-algorithm"]
}
}

Recall bundles two first-party algorithm packages and registers them through the same validated contract available to plugins:

PackageStable idBehavior
@jrtilak/fsrs-learning-algorithmfsrsUses the Free Spaced Repetition Scheduler and retrievability-based progress. This is the default for new cards.
@jrtilak/sm-2-learning-algorithmsm-2Uses conventional SM-2 intervals and an ease factor with a minimum of 1.3.

Both are bundled with the app as host fallbacks so their scheduling code remains available offline and before downloaded plugins initialize. Installing and loading a plugin that registers the same stable id overrides its bundled fallback for that plugin’s lifetime. Unloading the plugin restores the fallback without changing card ownership or state.

Actions are defined by the selected algorithm instead of being hard-coded by the review screen. The bundled algorithms expose the same four stable action ids:

Action idDisplay labelToneDefault swipeSuccessful review
easyEasypositiveLeftYes
goodMediumneutralDownYes
hardHardwarningUpNo
againVery HardnegativeRightNo

The action id is durable data stored in review history. Labels are display copy, order controls the button order, and tone lets the host map semantic meaning to theme colors. successful controls Recall’s accuracy statistics; it does not have to match an algorithm’s internal scheduling threshold. For example, conventional SM-2 advances its repetition sequence for quality 3, while Recall’s hard action still counts as an unsuccessful review.

defaultSwipeDirection can be left, right, up, or down. A direction may belong to only one action in an algorithm. Actions without a direction remain available as buttons.

Every card stores the stable algorithm id, its due timestamp, and opaque algorithm-owned state. Changing the learning algorithm in Review Settings changes the default for cards created afterward. Existing cards keep their current algorithm and state; Recall does not silently reinterpret or migrate them to the new selection.

If the plugin that owns an existing card’s algorithm is unavailable, Recall preserves the card and reports that the algorithm is unavailable. It does not fall back to FSRS, SM-2, or another installed algorithm. Re-enable or reinstall the owning plugin before evaluating or reviewing that card.

A LearningAlgorithm combines discovery metadata with four callbacks:

import type { LearningAlgorithm } from "@jrtilak-recall/runtime";
const algorithm = {
id: "example.scheduler",
name: "Example Scheduler",
description: "Schedules cards with an example policy.",
stateSchemaVersion: 1,
actions: [
{
id: "good",
label: "Good",
order: 0,
tone: "positive",
successful: true,
defaultSwipeDirection: "right",
},
],
createInitialState({ now }) {
return {
due: now,
state: { schemaVersion: 1, data: {} },
};
},
evaluateCards({ cards }) {
return cards.map((card, priority) => ({
bucket: "new",
cardId: card.id,
priority,
progress: 0,
status: "untouched",
}));
},
previewReview({ reviewedAt }) {
return [{ actionId: "good", due: reviewedAt }];
},
scheduleReview({ card, actionId, reviewedAt }) {
return {
card: { due: reviewedAt, state: card.state },
log: { actionId },
};
},
} satisfies LearningAlgorithm;
CallbackRequired result
createInitialStateA due ISO timestamp and state matching stateSchemaVersion.
evaluateCardsOne result per input card with a new or review bucket, normalized progress from 0 through 1, and a non-negative priority sorted ascending by the host.
previewReviewOne due ISO timestamp for every registered action id.
scheduleReviewThe next due timestamp, versioned state, and an algorithm-specific JSON log.

An evaluation with bucket new must use status untouched. A review result uses learning or mastered. Result arrays may be returned in any order; Recall validates their ids and restores input-card or action order.

Downloaded JavaScript plugins run in isolated sandboxes. Calls through RuntimeCtx are asynchronous RPC, even when the corresponding host registry method is synchronous. Always await register, discovery, execution, subscription, and disposal methods.

Recall groups queue evaluation by algorithm and sends bounded batches rather than an entire mixed deck. The current app uses at most 200 cards per batch. Treat the batch size as host-owned: return one result for each supplied card and never depend on seeing every card in a deck. Only the cards in that batch and their scoped recent-review records are exposed to the algorithm.

Values crossing this boundary must follow the public contract. State data and schedule log are plain JSON objects containing only finite numbers, strings, booleans, null, arrays, and plain objects. Do not return Date, Map, class instances, functions, undefined, NaN, or infinity. Timestamps are ISO strings. Recall validates and freezes normalized inputs and results.

Downloaded JavaScript plugin callbacks have a 9-second execution limit. The in-process registry uses a 10-second guard, leaving time to tear down a stalled sandbox before the outer deadline expires. A rejection, invalid result, or timeout fails the operation; Recall does not persist an apparent successful review after that failure.

Algorithm state has this durable envelope:

type LearningAlgorithmState = {
schemaVersion: number;
data: Record<string, JsonValue>;
};

stateSchemaVersion declares the version currently produced by the algorithm. Recall requires createInitialState and scheduleReview results to use that version, but the algorithm still owns the fields inside data. Validate the algorithm id, schema version, exact keys, and value ranges before scheduling.

Bump stateSchemaVersion when the persisted shape or meaning changes. An updated plugin must deliberately handle any older state it still supports or fail with an actionable error. Never silently treat another algorithm’s state, or an unknown schema version, as current state.

Register one or more algorithms atomically and retain the returned handle:

const registration = await ctx.learningAlgorithms.register({
algorithms: [algorithm],
});
await registration.dispose();

Algorithm ids must be unique across normal plugin registrations. A normal registration may override a host fallback with the same id, but it cannot replace another plugin’s active registration. A plugin may remove only algorithms it owns. get(id) and list() return callback-free descriptors, while on("listChange", listener) observes descriptor changes. Dispose registrations and subscriptions in unload; disposing an override restores its host fallback.

Continue with the learning-algorithm plugin tutorial for a complete package example.