Events
Simple pub/sub handlers. You publish a message with a type, and the matching handler executes. Each event is independent — there is no ordering guarantee between different event types.
Synkro offers two primary abstractions for organizing your application logic.
Events
Simple pub/sub handlers. You publish a message with a type, and the matching handler executes. Each event is independent — there is no ordering guarantee between different event types.
Workflows
Multi-step sequential pipelines. A workflow defines an ordered list of steps that execute one after another. State is persisted between steps, and the workflow tracks its overall progress.
| Events | Workflows | |
|---|---|---|
| Pattern | Fire-and-forget, fan-out | Sequential pipeline |
| State | Stateless | Persisted across steps |
| Use cases | Notifications, logging, cache invalidation | Order processing, onboarding flows, ETL pipelines |
A transport is the abstraction layer that handles message delivery and state storage. You choose a transport when calling Synkro.start().
// Developmentconst synkro = await Synkro.start({ transport: "in-memory" });
// Productionconst synkro = await Synkro.start({ transport: "redis", connectionUrl: "redis://localhost:6379",});Map-based state storage. No external dependencies. Ideal for development and testing.See the Transport System page for a deep dive.
Every event publication and workflow execution is associated with a requestId. This is a correlation ID that ties together all steps in a workflow or traces a single event through the system.
publish() call.// Auto-generatedawait synkro.publish("UserSignedUp", { email: "user@example.com" });
// User-providedawait synkro.publish("UserSignedUp", { email: "user@example.com" }, { requestId: "req-abc-123",});In workflows, the same requestId flows through every step, making it straightforward to trace and debug the full pipeline.
Every event and workflow step handler receives a context object with the following properties:
handler: async (ctx) => { ctx.requestId; // string — correlation ID for this execution ctx.payload; // unknown — the data published with the event ctx.publish(); // emit another event from within this handler ctx.setPayload(); // merge data into the payload for subsequent workflow steps};Emit additional events from inside a handler. This is how you chain side effects or trigger downstream processes.
handler: async (ctx) => { const user = ctx.payload as { email: string }; // Trigger a follow-up event await ctx.publish("SendWelcomeEmail", { to: user.email });};In workflows, use setPayload() to pass data from one step to the next. The object you provide is merged into the existing payload.
steps: [ { type: "ValidateStock", handler: async (ctx) => { const available = true; ctx.setPayload({ stockAvailable: available }); }, }, { type: "ProcessPayment", handler: async (ctx) => { // ctx.payload now includes { stockAvailable: true } const payload = ctx.payload as { stockAvailable: boolean }; if (!payload.stockAvailable) return; console.log("Charging customer..."); }, },];Workflows are backed by a state machine that the transport layer persists automatically. Each workflow execution tracks:
| Property | Description |
|---|---|
currentStep | The index of the step currently being executed |
status | One of running, completed, failed, or cancelled |
requestId | The correlation ID for this execution |
payload | The current accumulated payload |
The state machine transitions look like this:
publish() → running → step 1 → step 2 → ... → step N → completed ↓ (error) ↓ failedYou never interact with the state machine directly — Synkro manages it for you. The state is available through the dashboard (@synkro/ui) and the synkro.introspect() API for programmatic access.