QUESTPIE
SchemaHooks

Transition hooks

A workflow stage move is not a write, so the two hooks around it carry their own context, and only the first of them can stop the move.

View markdown

transitionStage() moves a record from one stage to the next without changing a field on it. beforeTransition and afterTransition sit around that move. They live in the same .hooks({ ... }) block as everything else, but almost nothing about their context is the same.

Turning them on

Stage moves are stored as version snapshots, so a workflow needs versioning.

src/questpie/server/collections/posts.ts
.options({ versioning: { workflow: true } })   // stages: draft, published

Leave the workflow off and transitionStage() throws, so neither hook ever fires. Options covers custom stages and the moves they allow.

The pair

src/questpie/server/collections/posts.ts
import { ApiError } from "questpie/errors";

.hooks({
	beforeTransition: ({ data, toStage }) => {
		if (toStage === "published" && !data.publishedAt) {
			throw ApiError.badRequest("Set publishedAt before publishing.");
		}
	},
	afterTransition: async ({ data, toStage, queue }) => {
		await queue.announce.publish(
			{ postId: data.id, stage: toStage },
			{ idempotencyKey: `announce:${data.id}:${toStage}` },
		);
	},
})

Both run inside the transaction that writes the snapshot, and both receive the same object.

KeyWhat you get
dataThe record, as it stands before the move.
recordIdIts id.
fromStage, toStageThe move.
scheduledAtWhatever date the caller passed, if they passed one.
expectedRevisionThe revision the caller asserted, under optimistic concurrency.
locale, accessModeAs on any hook.

Everything else is your app context: db, collections, queue, session, your own services. There is no operation, no original, and no onAfterCommit, so a non-transactional side effect has no safe home here. Put it in a durable job instead.

Only `beforeTransition` can refuse

Throwing from it aborts the move. An error from afterTransition is caught and logged, and the transition still commits.

A future date skips both

Pass a scheduledAt in the future and QUESTPIE queues the move rather than running it. That decision is itself a beforeTransition, and it runs ahead of yours, so neither of your hooks fires on that call. They fire when the queued job performs the move.

On this page