For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt, and this page is available as Markdown at https://open-spaced-repetition.github.io/ts-fsrs/index.md.
ts-fsrs
Fully customisable spaced repetition.

A type-safe scheduler framework for TypeScript. Start with FSRS-7, then choose your models, clocks, and middleware without changing API families.

npm
yarn
pnpm
bun
deno
npm install ts-fsrs
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 })
Stability
3.9221
Difficulty
3.5307
stabilityFast
3.13768

Type-safe by construction

Schemas drive runtime validation and the inferred input and output types at the same time.

Customize your algorithm without having to migrate

Start with the default scheduler, then replace models, clocks, and policies on the same core.

Lazy by design

Lazy previews, version-based code splitting, and tree-shakable subpath exports.

Progressive API

Start simple. Extend without starting over.

Use the default FSRS scheduler, customize your scheduling policy, or implement an entirely new model — all using the same foundation.

01

Use

DefaultScheduler provides an FSRS scheduler with sensible defaults. It supports FSRS-3 through FSRS-7 and defaults to FSRS-7.DefaultScheduler

02

Customize

Pick a model and a chrono, then add the policies your project needs, one middleware at a time.defineScheduler.use(middleware)

03

Extend

Write your own middleware, chrono, or model when the built-ins run out.defineMiddlewaredefineChrono

Scheduler Core

Type safety

One contract. End-to-end types.

Standard Schema provides a shared runtime-validation and type contract for configuration, cards, review logs, memory state, and time values.

  • A single schema validates at runtime and infers the TypeScript types, so the two can never drift apart.
  • Input cards stay readonly; only the scheduled result is mutable.
  • Fields and statuses that middleware adds appear in the inferred types automatically.
StandardSchemaV1zod
Model
Chrono
Middleware
Readonly<Card>input
Mutable<Card>scheduled

scheduleStatus: 'new' | 'review' | 'suspended'

Built-ins

Built-in and custom scheduling policies.

Stats

Adds and maintains the reps and lapses counters on cards.

DesiredRetention

Passes the configured retention target into interval calculation.

CostAdr

Adapts retention to memory state and review cost. Use instead of the fixed desired-retention policy.

Fuzzing

Applies deterministic interval variation, seeded by card ID and review count.

LearningSteps

Schedules learning and relearning steps in minutes, hours, or days.

Leech

Suspends a card whenever its lapse count reaches a multiple of the configured threshold.

MaximumInterval

Clamps the final schedule to the configured maximum.

MonotonicInterval

Keeps higher ratings from producing shorter schedules.

ScheduledDays

Stores scheduled days and their rollback values, preserving fractional intervals when enabled.

Architecture

Use the Model, Time and Policy which best suits your needs.

Each component is cross-compatible

Models

  • FSRS-3
  • FSRS-4
  • FSRS-4.5
  • FSRS-5
  • FSRS-6
  • FSRS-7
  • defineModel

Chrono

  • dateChrono
  • numericChrono
  • temporalInstantChrono
  • defineChrono

Middleware

  • learningSteps
  • desiredRetention
  • costAdr
  • fuzzing
  • leech
  • stats
  • defineMiddleware
Scheduler CorenewCard · preview · review · rollback
Card + Revlog + ScheduleStatus
definition
.use(fuzzing).use(learningSteps)

use() returns a new definition. Existing definitions, and the schedulers created from them, stay untouched.

Performance

Performance is part of the architecture.

The API is designed around lazy evaluation, code splitting, and immutable results.

Version-based code splitting

DefaultScheduler loads the selected FSRS version on demand, helping keep unused model code out of the initial load.

Lazy preview

preview() only schedules the grades you actually iterate.

Lazy schema creation

Scheduler schemas are built on first access, not when the definition is declared.

Tree-shakable exports

sideEffects is false, and models and middleware ship as subpath exports.

Immutable results

Reviews return new cards; nothing in the input is mutated.

Reversible reviews

rollback() restores the previous card from its revlog, middleware fields included.

Contributors

Built by open source contributers.

Models, middleware, and improvements across the project benefit from ideas and contributions shared through community pull requests.

Sponsors

Thank you for every bit of support.

Every sponsorship helps sustain ts-fsrs maintenance and its ongoing alignment with FSRS research.

Current sponsors

  • L-M-Sherlock

Past sponsors

  • tomerbeil

Become a sponsor

Start with FSRS. Build beyond it.

The default scheduler covers common cases. When your requirements call for it, create your own with a different model, chrono, or middleware.