# Checkpointed seeds (/docs/schema/seeds/steps)

---
title: Checkpointed seeds
description: seed.steps() trades one transaction for a checkpoint per step, so a seed that fails on its two hundredth upload resumes there instead of starting over.
kind: guide
package: questpie
---

A seed that imports a large fixture, uploads files or calls a paid API is the
wrong shape for all-or-nothing. This page turns one into a seed that picks up
where it stopped.

## Wrapping the work in steps

`seed.steps({...})` adds `step(name, fn)` to the seed context. Everything else
about the definition is the same.

```ts title="src/questpie/server/seeds/demo-content.ts"
import { seed } from "questpie/services";

export default seed.steps({
	id: "demoContent",
	description: "Demo posts, resumable",
	category: "dev",
	async run({ step }) {
		const fixture = await step("fetch-fixture", async () => {
			const res = await fetch("https://example.test/fixture.json");
			return (await res.json()) as { title: string; slug: string }[];
		});

		await step("create-posts", async ({ collections }) => {
			for (const post of fixture) {
				await collections.posts.create(post);
			}
		});
	},
});
```

Run it, kill it halfway, run it again. `fetch-fixture` does not call the network
a second time. It returns the value it returned the first time, and only
`create-posts` executes.

## What a step is

Each `step()` call runs its callback in its own transaction and writes one row
to `questpie_seed_steps`, keyed by seed id and step name. The row holds the
callback's return value as JSONB. On the next entry into `run`, a step whose row
is already there skips the callback and hands back the stored value.

The callback receives a seed context bound to that step's transaction, which is
why the example destructures `collections` from it rather than from the outer
`run`. A callback that needs nothing can take no argument at all.

### What the return value may be

The type is JSON or nothing: `null`, booleans, numbers, strings, arrays and
plain objects. A step that returns nothing is fine, and on replay it hands back
`undefined`. Name steps deliberately, because the name is the key and a renamed
step is a new step.

### `step` is not on a plain seed

`step()` exists only on the context `seed.steps()` builds. Reaching for it
inside a `seed({...})` handler does not compile.

## What you give up

A checkpointed seed has no seed-wide transaction. Work outside a `step()` call
is not checkpointed and not rolled back when a later step throws, so keep every
database write inside one. The tracking row in `questpie_seeds` is written after
`run` returns, so a seed that fails midway stays pending with its finished steps
recorded.

<Callout type="warn" title="A failed step runs again in full">
	The checkpoint is written only once the callback returns. A step that uploads
	a file and then throws leaves the file behind and no row, so the next run
	uploads it again. Keep external work in small steps.
</Callout>

## Clearing the checkpoints

| Command                    | What happens to the step rows                     |
| -------------------------- | ------------------------------------------------- |
| `questpie seed --force`    | Cleared with the tracking row. Every step reruns. |
| `questpie seed:undo`       | The `undo` handler runs, then the rows go.        |
| `questpie seed:reset`      | Rows go. No handler runs and no data changes.     |
| `questpie seed --validate` | Written inside the dry run, thrown away with it.  |

A forced run that fails partway leaves the seed pending with whatever steps it
did finish, so the next run resumes from there rather than from the top.

## Related

- **[Seeds](/docs/schema/seeds)** for the plain form and its single transaction.
- **[Running seeds](/docs/schema/seeds/running)** for the flags in that table.
- **[Jobs](/docs/code/jobs)** for work that belongs on a queue instead of in a
  seed.
