> 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.

# Middleware overview

Middleware adds scheduling policies and application fields to a scheduler. The model calculates memory states and intervals, and a chrono preset turns those intervals into due times. Middleware sits between them: it can adjust the target retention or interval, manage learning steps, and maintain extra card data that the model never sees. See [Architecture](https://open-spaced-repetition.github.io/ts-fsrs/guide/architecture.md) for how these parts fit together.

:::note Middleware boundary
A scheduler combines a model, a chrono, and middleware. You should keep the model limited to memory-state updates and interval math, and let the chrono component handle time values and day arithmetic. Middleware supplies inputs before the core operation runs, adjusts the result afterward, and carries the fields those two parts do not define.
:::


For your Agent

Implement or explain a middleware policy

Copy Prompt

Use the current ts-fsrs Middleware contract for this task: [describe the task].

1. Read https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/index.md, https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/lifecycle.md, and https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/built-in.md for how middleware extends a scheduler.
2. Declare the config, card, revlog, and cardInitInput schemas before defining behavior.
3. Keep the middleware limited to schemas, defaultValue, scheduleStatus declarations for custom statuses, and review/rollback/nextInterval handlers. Let the model own memory-state updates and interval math, and let the chrono own time values and day arithmetic.
4. Call next() exactly once. Set inputs such as ctx.candidate.desiredRetention[ctx.input.grade] before it; read or adjust the computed result after it.
5. For rollback, read ctx.config, ctx.input.card, and ctx.input.revlog, and write only ctx.result.card. Restore the old values the review handler saved according to the configured policy.
6. Keep handlers free of database writes, notifications, and other external effects; let the application persist the selected review result.
7. Reuse a built-in policy when it already covers the behavior.
8. Use the current public API and add focused tests for any handler you write.

## Define fields and behavior

A `Middleware` definition has a required `name` and optional schemas, defaults, and handlers. `defineMiddleware()` infers the handler types from those schemas. Import it from `ts-fsrs`; the built-in policies are also exported from `ts-fsrs/middlewares`.

| Definition                              | Purpose                                                                                                                                                                       |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema.config`                         | Validates configuration passed to `create({ config })`. Middleware shares the scheduler's config object.                                                                      |
| `schema.card`                           | Declares and validates fields on input and output cards.                                                                                                                      |
| `schema.revlog`                         | Declares and validates fields on review logs, including values needed for rollback.                                                                                           |
| `schema.cardInitInput`                  | Declares additional input accepted by `newCard()`, such as a card ID.                                                                                                         |
| `scheduleStatus`                        | Registers custom schedule statuses, for example `scheduleStatus: ['buried']`, so the scheduler types and result validation accept them.                                       |
| `handlers.nextInterval`                 | Adjusts `ctx.candidate.desiredRetention[ctx.input.grade]` before `next()` or `ctx.scheduledDays` afterward. Runs during review and direct `scheduler.nextInterval()` queries. |
| `defaultValue.card`                     | Supplies fields when creating or forgetting a card. `ctx.operation` distinguishes the two operations.                                                                         |
| `handlers.review` / `handlers.rollback` | Update `ctx.result` around the next handler in the chain. See [Lifecycle](https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/lifecycle.md) for how these run.  |

Schemas provide runtime validation as well as TypeScript inference. This example uses Zod, which implements Standard Schema. Install it alongside ts-fsrs to run the example in your application:


```sh [npm]
npm install zod
```

```sh [yarn]
yarn add zod
```

```sh [pnpm]
pnpm add zod
```

```sh [bun]
bun add zod
```

```sh [deno]
deno add npm:zod
```

## Add a streak counter with rollback

Here, any rating except Again increments `streak`, up to `streakLimit`. Again resets it to zero. The review handler saves the previous streak in the revlog so rollback can restore it even after a reset:

```ts twoslash file="../../../snippets/run-code/middleware.ts" fold height=520
import { dateChrono, defineMiddleware, defineScheduler, Rating } from 'ts-fsrs'
import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6'
import { z } from 'zod'

const streakFields = z.object({ streak: z.int().nonnegative() })
const streakMiddleware = defineMiddleware({
  name: Symbol('guide.streak'),
  schema: {
    config: z.object({ streakLimit: z.int().positive() }),
    card: streakFields,
    revlog: streakFields,
  },
  defaultValue: { card: () => ({ streak: 0 }) },
  handlers: {
    review(ctx, next) {
      next()
      const { card, grade } = ctx.input
      ctx.result.revlog.streak = card.streak
      ctx.result.card.streak =
        grade === Rating.Again
          ? 0
          : Math.min(card.streak + 1, ctx.config.streakLimit)
    },
    rollback(ctx, next) {
      next()
      ctx.result.card.streak = ctx.input.revlog.streak
    },
  },
})

const base = defineScheduler({ model: FSRS6Model, chrono: dateChrono })
const scheduler = base.use(streakMiddleware).create({
  config: {
    weights: FSRS6_DEFAULT_WEIGHTS,
    enableShortTerm: false,
    numRelearningSteps: 0,
    streakLimit: 2,
  },
})

const now = new Date('2026-01-01T00:00:00.000Z')
const initial = scheduler.newCard({ now })
const good = scheduler.review({ card: initial, grade: Rating.Good, now })
const again = scheduler.review({
  card: good.card,
  grade: Rating.Again,
  now: good.card.dueAt,
})
const restored = scheduler.rollback(again)
const preview = Array.from(
  scheduler.preview({ card: restored, now: good.card.dueAt })
)

console.log(
  JSON.stringify(
    {
      initial: initial.streak,
      afterGood: good.card.streak,
      afterAgain: again.card.streak,
      savedBeforeAgain: again.revlog.streak,
      restored: restored.streak,
      preview: preview.map(({ card }) => card.streak),
      afterPreview: restored.streak,
    },
    null,
    2
  )
)

```


▶ Run code

The counter goes from `0` to `1`, then back to `0` after Again. Rollback restores `1`. Preview produces `0, 2, 2, 2` for Again, Hard, Good, and Easy while leaving the input counter at `1`.

`base.use(streakMiddleware)` returns a new scheduler definition; `base` stays unchanged. Keep the return value or chain `.create()` as above. Handlers registered through `.use(...)` run in a fixed order around the scheduler core — see [Lifecycle](https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/lifecycle.md).

Ready-made policies cover the common cases, and `DefaultScheduler` already includes them. See [Built-in policies](https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware/built-in.md) or use [Quick start](https://open-spaced-repetition.github.io/ts-fsrs/guide/quick-start.md) for the ready-made scheduler directly.
