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 schema.
router.state("Todo", ({ on }) => {
on("Complete", ({ data, transition, emit, workflow }) => {
transition("Done", {
title: data.title,
completedAt: new Date(),
});
emit("TaskCompleted", { 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("TaskStarted", { taskId: workflow.id, assignee: command.payload.assignee });
emit("AssigneeNotified", { 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, "Complete", {});
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("TaskCompleted", { 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, "Start", { assignee: "alice" });
// r1.events: [{ type: "TaskStarted", ... }, { type: "AssigneeNotified", ... }]
if (!r1.ok) throw new Error("dispatch failed");
const _r2 = await router.dispatch(r1.workflow, "Complete", {});
// 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 result = await router.dispatch(task, "Start", { assignee: "alice" });
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.