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"] }}Bundled algorithms
Section titled “Bundled algorithms”Recall bundles two first-party algorithm packages and registers them through the same validated contract available to plugins:
| Package | Stable id | Behavior |
|---|---|---|
@jrtilak/fsrs-learning-algorithm | fsrs | Uses the Free Spaced Repetition Scheduler and retrievability-based progress. This is the default for new cards. |
@jrtilak/sm-2-learning-algorithm | sm-2 | Uses 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.
Review actions and gestures
Section titled “Review actions and gestures”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 id | Display label | Tone | Default swipe | Successful review |
|---|---|---|---|---|
easy | Easy | positive | Left | Yes |
good | Medium | neutral | Down | Yes |
hard | Hard | warning | Up | No |
again | Very Hard | negative | Right | No |
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.
Per-card ownership and selection
Section titled “Per-card ownership and selection”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.
Algorithm contract
Section titled “Algorithm contract”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;| Callback | Required result |
|---|---|
createInitialState | A due ISO timestamp and state matching stateSchemaVersion. |
evaluateCards | One 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. |
previewReview | One due ISO timestamp for every registered action id. |
scheduleReview | The 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.
Async, batched JSON boundary
Section titled “Async, batched JSON boundary”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.
Versioned state
Section titled “Versioned state”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.
Registration and cleanup
Section titled “Registration and cleanup”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.