



























Learn how Workflow SDK uses event sourcing internally for debugging and observability.
This guide explores how the Workflow SDK uses event sourcing internally. Understanding these concepts is helpful for debugging and building observability tools, but is not required to use workflows. For getting started with workflows, see the getting started guides for your framework.
The Workflow SDK uses event sourcing to track all state changes in workflow executions. Every mutation creates an event that is persisted to the event log, and entity state is derived by replaying these events.
This page explains the event sourcing model and entity lifecycles.
Event sourcing is a persistence pattern where state changes are stored as a sequence of events rather than by updating records in place. The current state of any entity is reconstructed by replaying its events from the beginning.
Benefits for durable workflows:
In the Workflow SDK, the following entity types are managed through events:
Each entity type follows a specific lifecycle defined by the events that can affect it. Events transition entities between states, and certain states are terminal—once reached, no further transitions are possible.
In the diagrams below, purple nodes indicate terminal states that cannot be transitioned out of.
A run represents a single execution of a workflow function. Runs begin in pending state when created, transition to running when execution starts, and end in one of three terminal states.
Run states:
pending: Created but not yet executingrunning: Actively executing workflow codecompleted: Finished successfully with an output valuefailed: Terminated due to an unrecoverable errorcancelled: Explicitly cancelled by the user or systemA step represents a single invocation of a step function. Steps can retry on failure, either transitioning back to pending via step_retrying or being re-executed directly with another step_started event.
Step states:
pending: Created but not yet executing, or waiting to retryrunning: Actively executing step codecompleted: Finished successfully with a result valuefailed: Terminated after exhausting all retry attemptscancelled: Reserved for future use (not currently emitted)The step_retrying event is optional. Steps can retry without it - the retry mechanism works regardless of whether this event is emitted. You may see back-to-back step_started events in logs when a step retries after a timeout or when the error is not explicitly captured. See Errors and Retries for more on how retries work.
When present, the step_retrying event moves a step back to pending state and records the error that caused the retry. This provides two benefits:
step_started eventsA hook represents a suspension point that can receive external data, created by createHook(). Hooks enable workflows to pause and wait for external events, user interactions, or HTTP requests. Webhooks (created with createWebhook()) are a higher-level abstraction built on hooks that adds automatic HTTP request/response handling.
Hooks can receive multiple payloads while active and are disposed when no longer needed.
Hook states:
active: Ready to receive payloads (hook exists in storage)disposed: No longer accepting payloads (hook is deleted from storage)conflicted: Hook creation failed because the token is already in use by another workflowUnlike other entities, hooks don't have a status field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a hook_disposed event is created, the hook record is removed rather than updated.
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a hook_conflict event is recorded instead of hook_created. Current worlds include the token and the run ID that currently owns it, though older persisted events or world implementations may only include the token. This causes hook.getConflict() to resolve with the conflicting run and the hook's payload promise to reject with a HookConflictError, which you can detect with HookConflictError.is(error). See the hook-conflict error documentation for more details.
When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (completed, failed, or cancelled). The hook_disposed event is only needed for explicit disposal before workflow completion.
See Hooks & Webhooks for more on how hooks and webhooks work.
A wait represents a sleep operation created by sleep(). Waits track when a delay period has elapsed.
Wait states:
waiting: Delay period has not yet elapsedcompleted: Delay period has elapsed, workflow can resumeLike Runs, Steps, and Hooks, waits are materialized as entities in storage. When a wait_created event is processed, a wait entity is created with status waiting. When a wait_completed event is processed, the wait entity is atomically transitioned to completed — this guarantees that a wait can only be completed exactly once, even if multiple concurrent invocations attempt to complete it simultaneously.
Events are categorized by the entity type they affect. Each event contains metadata including a timestamp and a correlationId that links the event to a specific entity:
stepId as the correlation IDhookId as the correlation IDwaitId as the correlation IDrunId itself identifies the entity| Event | Description |
|---|---|
run_created | Creates a new workflow run in pending state. Contains the deployment ID, workflow name, input arguments, and optional execution context. |
run_started | Transitions the run to running state when execution begins. |
run_completed | Transitions the run to completed state with the workflow's return value. |
run_failed | Transitions the run to failed state with error details and optional error code. |
run_cancelled | Transitions the run to cancelled state. Can be triggered from pending or running states. |
| Event | Description |
|---|---|
step_created | Creates a new step in pending state. Contains the step name and serialized input arguments. |
step_started | Transitions the step to running state. Includes the current attempt number for retries. |
step_completed | Transitions the step to completed state with the step's return value. |
step_failed | Transitions the step to failed state with error details. The step will not be retried. |
step_retrying | (Optional) Transitions the step back to pending state for retry. Contains the error that caused the retry and optional delay before the next attempt. When not emitted, retries appear as consecutive step_started events. |
| Event | Description |
|---|---|
hook_created | Creates a new hook in active state. Contains the hook token and optional metadata. |
hook_conflict | Records that hook creation failed because the token is already in use by another active hook. Contains the token and, for current worlds, the active hook owner's run ID. The hook is not created: hook.getConflict() resolves with the conflicting run, and awaiting the hook payload rejects with a HookConflictError. |
hook_received | Records that a payload was delivered to the hook. The hook remains active and can receive more payloads. |
hook_disposed | Deletes the hook from storage (conceptually transitioning to disposed state). The token is released for reuse by future workflows. |
| Event | Description |
|---|---|
wait_created | Creates a new wait in waiting state. Contains the timestamp when the wait should complete. |
wait_completed | Transitions the wait to completed state when the delay period has elapsed. |
Terminal states represent the end of an entity's lifecycle. Once an entity reaches a terminal state, no further events can transition it to another state.
Run terminal states:
completed: Workflow finished successfullyfailed: Workflow encountered an unrecoverable errorcancelled: Workflow was explicitly cancelledStep terminal states:
completed: Step finished successfullyfailed: Step failed after all retry attemptsHook terminal states:
disposed: Hook has been deleted from storage and is no longer activeconflicted: Hook creation failed due to token conflict (hook was never created)Wait terminal states:
completed: Delay period has elapsedAttempting to create an event that would transition an entity out of a terminal state will result in an error. This prevents inconsistent state and ensures the integrity of the event log.
Events use a correlationId to link related events together. For step, hook, and wait events, the correlation ID identifies the specific entity instance:
correlationId (the step ID) across all events for that step executioncorrelationId (the hook ID) across all events for that hookcorrelationId (the wait ID) across creation and completionRun events do not require a correlation ID since the runId itself provides the correlation.
This correlation enables:
Some World implementations also attach a requestId to events for platform-log correlation. This is different from correlationId:
correlationId links together events for the same entity lifecyclerequestId tells you which inbound platform request created or updated the eventOn Vercel, requestId is the platform request ID when available. Other worlds are not expected to provide a requestId.
{
"eventType": "step_started",
"correlationId": "step_01JQEXAMPLE1234567890",
"requestId": "iad1::abc123-1712345678901-xyz987"
}All entities in the Workflow SDK use a consistent ID format: a 4-character prefix followed by an underscore and a ULID (Universally Unique Lexicographically Sortable Identifier).
| Entity | Prefix | Example |
|---|---|---|
| Run | wrun_ | wrun_01HXYZ123ABC456DEF789GHJ |
| Step | step_ | step_01HXYZ123ABC456DEF789GHJ |
| Hook | hook_ | hook_01HXYZ123ABC456DEF789GHJ |
| Wait | wait_ | wait_01HXYZ123ABC456DEF789GHJ |
| Event | evnt_ | evnt_01HXYZ123ABC456DEF789GHJ |
| Stream | strm_ | strm_01HXYZ123ABC456DEF789GHJ |
Why this format?
Prefixes enable introspection: Given any ID, you can immediately identify what type of entity it refers to. This makes debugging, logging, and cross-referencing entities across the system straightforward.
ULIDs enable chronological ordering: Unlike UUIDs, ULIDs encode a timestamp in their first 48 bits, making them lexicographically sortable by creation time. This property is essential for the event log—events are always stored and retrieved in the correct chronological order simply by sorting their IDs.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。