Type-safe by construction
Schemas drive runtime validation and the inferred input and output types at the same time.
A type-safe scheduler framework for TypeScript. Start with FSRS-7, then choose your models, clocks, and middleware without changing API families.
import { function DefaultScheduler<Version extends DefaultSchedulerVersion = "FSRS-7">(options?: DefaultSchedulerOptions<Version>): Promise<DefaultScheduler<Version>>
type DefaultScheduler<Version extends DefaultSchedulerVersion = "FSRS-7"> = (Version extends "FSRS-7" ? Omit<SchedulerCore<SchedulerCoreEnv<SchedulerEnvFor<FSRS7DefaultSchedulerModel, Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>, "definition"> : Omit<...> & {
...;
}) & {
...;
}
DefaultScheduler } from 'ts-fsrs'
const createdAt = new Date('2026-01-01T00:00:00.000Z')
export const now = new Date('2026-01-02T09:30:00.000Z')
export const const scheduler: DefaultScheduler<"FSRS-7"> scheduler = await DefaultScheduler<"FSRS-7">(options?: DefaultSchedulerOptions<"FSRS-7"> | undefined): Promise<DefaultScheduler<"FSRS-7">> DefaultScheduler ({
DefaultSchedulerOptions<Version extends DefaultSchedulerVersion = DefaultSchedulerVersion>.desiredRetention?: number | undefined desiredRetention : 0.9,
})
export const const card: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
card = const scheduler: DefaultScheduler<"FSRS-7"> scheduler .newCard: (options?: ({
readonly cardId?: string | number | undefined;
} & {
readonly now?: Date | undefined;
}) | undefined) => {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
}
newCard ({
cardId: 'landing-demo',
now: createdAt,
})
// One `{ grade, card, revlog }` row per grade — the panel renders exactly this.
export const const outcomes: PreviewResult<{
card: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reps: number;
lapses: number;
scheduledDays: number;
learningStep: number;
state: State;
scheduleStatus: "review" | "new" | "learning";
};
revlog: {
cardId: string | number;
stabilityFast: number;
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
scheduledDays: number;
learningStep: number;
reviewTime: Date;
state: State;
scheduleStatus: "review" | "new" | "learning";
rating: Grade;
};
}>
outcomes = const scheduler: DefaultScheduler<"FSRS-7"> scheduler .preview: (input: {
readonly card: Readonly<Omit<Readonly<{
stabilityFast: number;
stability: number;
difficulty: number;
} & {
dueAt: Date;
lastReviewAt: Date | null;
} & {
readonly cardId: string | number;
readonly reps: number;
readonly lapses: number;
readonly scheduledDays: number;
readonly learningStep?: number | undefined;
} & SchedulerCoreFields$1<"review" | "new" | "learning">>, keyof SchedulerCoreFields$1<"review" | "new" | "learning">> & SchedulerCoreFields$1<"review" | "new" | "learning">>;
readonly now?: Date | undefined;
}) => PreviewResult<...>
preview ({ card, now })Schemas drive runtime validation and the inferred input and output types at the same time.
Start with the default scheduler, then replace models, clocks, and policies on the same core.
Lazy previews, version-based code splitting, and tree-shakable subpath exports.
Progressive API
Use the default FSRS scheduler, customize your scheduling policy, or implement an entirely new model — all using the same foundation.
DefaultScheduler provides an FSRS scheduler with sensible defaults. It supports FSRS-3 through FSRS-7 and defaults to FSRS-7.DefaultScheduler
Pick a model and a chrono, then add the policies your project needs, one middleware at a time.defineScheduler.use(middleware)
Write your own middleware, chrono, or model when the built-ins run out.defineMiddlewaredefineChrono
Type safety
Standard Schema provides a shared runtime-validation and type contract for configuration, cards, review logs, memory state, and time values.
scheduleStatus: 'new' | 'review' | 'suspended'
Built-ins
Adds and maintains the reps and lapses counters on cards.
Passes the configured retention target into interval calculation.
Adapts retention to memory state and review cost. Use instead of the fixed desired-retention policy.
Applies deterministic interval variation, seeded by card ID and review count.
Schedules learning and relearning steps in minutes, hours, or days.
Suspends a card whenever its lapse count reaches a multiple of the configured threshold.
Clamps the final schedule to the configured maximum.
Keeps higher ratings from producing shorter schedules.
Stores scheduled days and their rollback values, preserving fractional intervals when enabled.
Architecture
Each component is cross-compatible
use() returns a new definition. Existing definitions, and the schedulers created from them, stay untouched.
Performance
The API is designed around lazy evaluation, code splitting, and immutable results.
DefaultScheduler loads the selected FSRS version on demand, helping keep unused model code out of the initial load.
preview() only schedules the grades you actually iterate.
Scheduler schemas are built on first access, not when the definition is declared.
sideEffects is false, and models and middleware ship as subpath exports.
Reviews return new cards; nothing in the input is mutated.
rollback() restores the previous card from its revlog, middleware fields included.
Contributors
Models, middleware, and improvements across the project benefit from ideas and contributions shared through community pull requests.
Sponsors
Every sponsorship helps sustain ts-fsrs maintenance and its ongoing alignment with FSRS research.
The default scheduler covers common cases. When your requirements call for it, create your own with a different model, chrono, or middleware.