Events
Events are side effects emitted during dispatch. They are schema-validated, accumulated per dispatch, and returned in the result.
Emitting Events
Use emit() inside a handler. The event data is validated against the event's Zod schema.
router.state("Todo", ({ on }) => {
on("Complete", ({ data, transition, emit, workflow }) => {
transition("Done", {
title: data.title,
completedAt: new Date(),
});
emit({
type: "TaskCompleted",
data: { taskId: workflow.id },
});
});
});You can emit multiple events in a single handler:
on("Start", ({ data, command, transition, emit, workflow }) => {
transition("InProgress", {
title: data.title,
assignee: command.payload.assignee,
});
emit({
type: "TaskStarted",
data: { taskId: workflow.id, assignee: command.payload.assignee },
});
emit({ type: "AssigneeNotified", data: { assignee: command.payload.assignee } });
});Reading Events After Dispatch
Events are returned in result.events on success:
(async () => {
const task = taskWorkflow.createWorkflow("task-1", {
initialState: "Todo",
data: { title: "Write docs" },
});
const result = await router.dispatch(task, { type: "Complete", payload: {} });
if (result.ok) {
for (const event of result.events) {
console.log(event.type, event.data);
// "TaskCompleted" { taskId: "task-1" }
}
}
})();Schema Validation
Event data must match the schema defined in the workflow. If it doesn't, dispatch fails with a validation error:
const workflow = defineWorkflow("task", {
states: {
Todo: z.object({ title: z.string() }),
Done: z.object({ title: z.string(), completedAt: z.coerce.date() }),
},
commands: {
Complete: z.object({}),
},
events: {
TaskCompleted: z.object({ taskId: z.string() }),
},
errors: {},
});
const validationRouter = new WorkflowRouter(workflow);
validationRouter.state("Todo", ({ on }) => {
on("Complete", ({ data, transition, emit, workflow: _wf }) => {
transition("Done", { title: data.title, completedAt: new Date() });
// @ts-expect-error taskId must be a string, not a number
emit({ type: "TaskCompleted", data: { taskId: 123 } });
});
});This produces a validation error with source: "event".
Per-Dispatch Isolation
Each dispatch starts with an empty events list. Events from one dispatch never appear in another.
(async () => {
const task = taskWorkflow.createWorkflow("task-2", {
initialState: "Todo",
data: { title: "Write more docs" },
});
const r1 = await router.dispatch(task, { type: "Start", payload: { assignee: "alice" } });
// r1.events: [{ type: "TaskStarted", ... }, { type: "AssigneeNotified", ... }]
if (!r1.ok) throw new Error("dispatch failed");
const _r2 = await router.dispatch(r1.workflow, { type: "Complete", payload: {} });
// r2.events: [{ type: "TaskCompleted", ... }]
// TaskStarted is NOT in r2.events
})();Handling Events
Ryte does not prescribe how you handle events after dispatch. Common patterns:
(async () => {
const task = taskWorkflow.createWorkflow("task-3", {
initialState: "Todo",
data: { title: "Handle events" },
});
const command = { type: "Start" as const, payload: { assignee: "alice" } };
const result = await router.dispatch(task, command);
if (result.ok) {
for (const event of result.events) {
switch (event.type) {
case "TaskCompleted":
await sendNotification(event.data);
break;
case "TaskStarted":
await updateDashboard(event.data);
break;
}
}
}
})();Events are data -- publish them to a message bus, write them to an event store, or handle them inline. Ryte gives you validated, typed events and lets you decide what to do with them.