Context Keys
Context keys provide type-safe key-value storage on the dispatch context. Middleware sets values; handlers read them.
Creating Keys
createKey<T>(name) creates a phantom-typed symbol. The name is for debugging only -- uniqueness comes from the underlying Symbol.
ts
const UserKey = createKey<{ id: string; role: string }>("user");
const RequestIdKey = createKey<string>("requestId");Two calls to createKey with the same name produce different keys.
Setting Values
Use set(key, value) in middleware:
ts
keyRouter.use(async ({ set }, next) => {
set(UserKey, { id: "user-1", role: "admin" });
set(RequestIdKey, crypto.randomUUID());
await next();
});The value must match the key's type parameter -- set(UserKey, "string") is a type error.
Reading Values
get(key) -- throws if missing
ts
const user = get(UserKey);
// user is typed as { id: string; role: string }
// throws if UserKey was never setgetOrNull(key) -- returns undefined if missing
ts
const user = getOrNull(UserKey);
// user is typed as { id: string; role: string } | undefinedComplete Example: Auth Middleware + Handler
ts
// 1. Define a typed key
const AuthKey = createKey<{ userId: string; role: "viewer" | "editor" | "admin" }>("auth");
// 2. Define workflow
const articleWorkflow = defineWorkflow("article", {
states: {
Draft: z.object({ title: z.string(), body: z.string().optional() }),
Published: z.object({ title: z.string(), body: z.string(), publishedAt: z.coerce.date() }),
},
commands: {
Publish: z.object({}),
},
events: {
ArticlePublished: z.object({ articleId: z.string(), publishedBy: z.string() }),
},
errors: {
Unauthorized: z.object({ required: z.string() }),
BodyRequired: z.object({}),
},
});
// 3. Create router
const router = new WorkflowRouter(articleWorkflow);
// 4. Auth middleware sets the key
router.use(async ({ set }, next) => {
// In a real app: validate JWT, look up session, etc.
const auth = { userId: "user-1", role: "editor" as const };
set(AuthKey, auth);
await next();
});
// 5. Handler reads the key
router.state("Draft", ({ on }) => {
on("Publish", ({ get, error, data, transition, emit, workflow }) => {
const auth = get(AuthKey);
if (auth.role === "viewer") {
error({ code: "Unauthorized", data: { required: "editor" } });
}
if (!data.body) {
error({ code: "BodyRequired", data: {} });
}
transition("Published", {
title: data.title,
// biome-ignore lint/style/noNonNullAssertion: guarded by error() check above
body: data.body!,
publishedAt: new Date(),
});
emit({
type: "ArticlePublished",
data: { articleId: workflow.id, publishedBy: auth.userId },
});
});
});When to Use Context Keys
Use context keys when middleware needs to pass computed data to handlers:
- Auth -- middleware authenticates, handler checks permissions
- Request tracing -- middleware generates a trace ID, handler includes it in events
- Timing -- middleware records start time, post-handler logic calculates duration
For static services that don't change per-request, prefer dependency injection instead.