|
| 1 | +--- |
| 2 | +title: Subscribe to events |
| 3 | +description: Receive historical and live events from a Workflow instance without polling its status. |
| 4 | +pcx_content_type: concept |
| 5 | +sidebar: |
| 6 | + order: 7 |
| 7 | +products: |
| 8 | + - workflows |
| 9 | +--- |
| 10 | + |
| 11 | +import { TypeScriptExample } from "~/components"; |
| 12 | + |
| 13 | +Use `WorkflowInstance.subscribe()` to receive events without polling [`status()`](/workflows/build/workers-api/#status). A subscription will always receive the entire history of events that occur in a instance and block waiting for new events if its still running. |
| 14 | + |
| 15 | +You can subscribe immediately after creating an instance or you can also retrieve an existing instance with [`get()`](/workflows/build/workers-api/#get) and subscribe while its state remains within the [retention period](/workflows/reference/limits/). |
| 16 | + |
| 17 | +## Subscribe to all events |
| 18 | + |
| 19 | +<TypeScriptExample> |
| 20 | + |
| 21 | +```ts |
| 22 | +interface Env { |
| 23 | + MY_WORKFLOW: Workflow; |
| 24 | +} |
| 25 | + |
| 26 | +export default { |
| 27 | + async fetch(_request: Request, env: Env) { |
| 28 | + const instance = await env.MY_WORKFLOW.create({ |
| 29 | + params: { reportId: "report-123" }, |
| 30 | + }); |
| 31 | + |
| 32 | + using subscription = await instance.subscribe(); |
| 33 | + |
| 34 | + while (true) { |
| 35 | + const result = await subscription.next(); |
| 36 | + if (result.done) { |
| 37 | + break; |
| 38 | + } |
| 39 | + |
| 40 | + console.log(result.value.type, result.value); |
| 41 | + } |
| 42 | + |
| 43 | + return Response.json({ instanceId: instance.id }); |
| 44 | + }, |
| 45 | +} satisfies ExportedHandler<Env>; |
| 46 | +``` |
| 47 | + |
| 48 | +</TypeScriptExample> |
| 49 | + |
| 50 | +The subscription ends after the instance emits `workflow_completed`, `workflow_errored`, or `workflow_terminated`. After that evey call to next returns `done: true`. |
| 51 | + |
| 52 | +## Filter events |
| 53 | + |
| 54 | +Use `filter` to receive only specific event types during `next` calls, avoiding processing of unwanted events. |
| 55 | + |
| 56 | +<TypeScriptExample> |
| 57 | + |
| 58 | +```ts |
| 59 | +const instance = await env.MY_WORKFLOW.get("report-123"); |
| 60 | + |
| 61 | +using subscription = await instance.subscribe({ |
| 62 | + filter: ["workflow_completed", "workflow_errored", "workflow_terminated"], |
| 63 | +}); |
| 64 | + |
| 65 | +const result = await subscription.next(); |
| 66 | +if (result.done) { |
| 67 | + throw new Error("The instance ended without a matching event."); |
| 68 | +} |
| 69 | + |
| 70 | +switch (result.value.type) { |
| 71 | + case "workflow_completed": |
| 72 | + console.log("Workflow output:", result.value.output); |
| 73 | + break; |
| 74 | + case "workflow_errored": |
| 75 | + console.error("Workflow errored:", result.value.error); |
| 76 | + break; |
| 77 | + case "workflow_terminated": |
| 78 | + console.log("Workflow terminated."); |
| 79 | + break; |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +</TypeScriptExample> |
| 84 | + |
| 85 | +A subscription still ends when it reaches a terminal event excluded by its filter. In that case, `next()` returns `done: true` without returning the excluded event. |
| 86 | + |
| 87 | +## Resume from a cursor |
| 88 | + |
| 89 | +Each event includes an `eventId`. To resume after a RPC call failure store the last processed event ID and pass it as `cursor`: |
| 90 | + |
| 91 | +<TypeScriptExample> |
| 92 | + |
| 93 | +```ts |
| 94 | +using subscription = await instance.subscribe({ |
| 95 | + cursor: lastProcessedEventId, |
| 96 | + filter: ["step_completed", "workflow_completed", "workflow_errored"], |
| 97 | +}); |
| 98 | + |
| 99 | +while (true) { |
| 100 | + const result = await subscription.next(); |
| 101 | + if (result.done) { |
| 102 | + break; |
| 103 | + } |
| 104 | + |
| 105 | + await processEvent(result.value); |
| 106 | + await saveLastProcessedEventId(result.value.eventId); |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +</TypeScriptExample> |
| 111 | + |
| 112 | +The cursor identifies the last event you processed. The subscription starts with the first event whose `eventId` is greater than the cursor. |
| 113 | + |
| 114 | +## Sensitive outputs |
| 115 | + |
| 116 | +If a step is marked as sensitive, the `output` field in its `step_completed` will be the string `"[REDACTED]"`. |
| 117 | + |
| 118 | +## Dispose of a subscription |
| 119 | + |
| 120 | +A subscription holds a Workers RPC resource. Declare it with `using` so the runtime disposes it when the scope exits. This also cleans up the subscription when you stop reading before the instance ends. |
| 121 | + |
| 122 | +If you cannot use `using`, call `subscription[Symbol.dispose]()` in a `finally` block. For more information, refer to [RPC lifecycle](/workers/runtime-apis/rpc/lifecycle/). |
| 123 | + |
| 124 | +## Event fields |
| 125 | + |
| 126 | +The public type definition shows the fields available on each event: |
| 127 | + |
| 128 | +```ts |
| 129 | +type WorkflowInstanceEvent = { |
| 130 | + instanceId: string; |
| 131 | + eventId: number; |
| 132 | + timestamp: number; |
| 133 | +} & ( |
| 134 | + | { type: "workflow_queued" } |
| 135 | + | { type: "workflow_started"; params?: unknown } |
| 136 | + | { type: "workflow_running" } |
| 137 | + | { type: "workflow_paused" } |
| 138 | + | { type: "workflow_waiting_for_pause" } |
| 139 | + | { type: "workflow_waiting" } |
| 140 | + | { type: "workflow_completed"; output?: unknown } |
| 141 | + | { type: "workflow_errored"; error: { name: string; message: string } } |
| 142 | + | { type: "workflow_terminated" } |
| 143 | + | { |
| 144 | + type: "step_started"; |
| 145 | + stepName: string; |
| 146 | + config?: { |
| 147 | + retries: { |
| 148 | + limit: number; |
| 149 | + delay: WorkflowSleepDuration | "[dynamic]"; |
| 150 | + backoff?: "constant" | "linear" | "exponential"; |
| 151 | + }; |
| 152 | + timeout: WorkflowSleepDuration; |
| 153 | + sensitive?: "output"; |
| 154 | + }; |
| 155 | + } |
| 156 | + | { type: "step_completed"; stepName: string; output?: unknown } |
| 157 | + | { type: "step_errored"; stepName: string } |
| 158 | + | { type: "attempt_started"; stepName: string; attempt: number } |
| 159 | + | { type: "attempt_completed"; stepName: string; attempt: number } |
| 160 | + | { |
| 161 | + type: "attempt_errored"; |
| 162 | + stepName: string; |
| 163 | + attempt: number; |
| 164 | + retryDelayMs?: number; |
| 165 | + error: { name: string; message: string }; |
| 166 | + } |
| 167 | + | { type: "sleep_started"; stepName: string; durationMs: number } |
| 168 | + | { type: "sleep_completed"; stepName: string } |
| 169 | + | { type: "wait_started"; stepName: string; eventType: string } |
| 170 | + | { type: "wait_completed"; stepName: string } |
| 171 | + | { type: "wait_timed_out"; stepName: string } |
| 172 | + | { type: "rollback_started" } |
| 173 | + | { |
| 174 | + type: "rollback_step_started"; |
| 175 | + stepName: string; |
| 176 | + config?: { |
| 177 | + retries: { |
| 178 | + limit: number; |
| 179 | + delay: WorkflowSleepDuration | "[dynamic]"; |
| 180 | + backoff?: "constant" | "linear" | "exponential"; |
| 181 | + }; |
| 182 | + timeout: WorkflowSleepDuration; |
| 183 | + sensitive?: "output"; |
| 184 | + }; |
| 185 | + } |
| 186 | + | { type: "rollback_step_completed"; stepName: string } |
| 187 | + | { |
| 188 | + type: "rollback_step_errored"; |
| 189 | + stepName: string; |
| 190 | + error: { name: string; message: string }; |
| 191 | + } |
| 192 | + | { type: "rollback_attempt_started"; stepName: string; attempt: number } |
| 193 | + | { type: "rollback_attempt_completed"; stepName: string; attempt: number } |
| 194 | + | { |
| 195 | + type: "rollback_attempt_errored"; |
| 196 | + stepName: string; |
| 197 | + attempt: number; |
| 198 | + retryDelayMs?: number; |
| 199 | + error: { name: string; message: string }; |
| 200 | + } |
| 201 | + | { type: "rollback_completed" } |
| 202 | + | { type: "rollback_errored" } |
| 203 | +); |
| 204 | +``` |
| 205 | + |
| 206 | +The following tables describe when each event is emitted. |
| 207 | + |
| 208 | +### Workflow lifecycle events |
| 209 | + |
| 210 | +| Event type | Emitted when | |
| 211 | +| ---------------------------- | ------------------------------------------------ | |
| 212 | +| `workflow_queued` | The instance enters the execution queue | |
| 213 | +| `workflow_started` | The instance starts | |
| 214 | +| `workflow_running` | The instance starts or resumes execution | |
| 215 | +| `workflow_paused` | The instance pauses | |
| 216 | +| `workflow_waiting_for_pause` | The instance waits for current work before pause | |
| 217 | +| `workflow_waiting` | The instance enters waiting state | |
| 218 | +| `workflow_completed` | The instance completes successfully | |
| 219 | +| `workflow_errored` | The instance ends with an error | |
| 220 | +| `workflow_terminated` | The instance is terminated | |
| 221 | + |
| 222 | +### Step and attempt events |
| 223 | + |
| 224 | +| Event type | Emitted when | |
| 225 | +| ------------------- | ---------------------------- | |
| 226 | +| `step_started` | A `step.do()` call starts | |
| 227 | +| `step_completed` | A `step.do()` call completes | |
| 228 | +| `step_errored` | A `step.do()` call errors | |
| 229 | +| `attempt_started` | A step attempt starts | |
| 230 | +| `attempt_completed` | A step attempt completes | |
| 231 | +| `attempt_errored` | A step attempt errors | |
| 232 | + |
| 233 | +### Sleep and wait events |
| 234 | + |
| 235 | +| Event type | Emitted when | |
| 236 | +| ----------------- | --------------------------------------------------- | |
| 237 | +| `sleep_started` | A `step.sleep()` or `step.sleepUntil()` call starts | |
| 238 | +| `sleep_completed` | A sleep finishes | |
| 239 | +| `wait_started` | A `step.waitForEvent()` call starts | |
| 240 | +| `wait_completed` | A matching event reaches `step.waitForEvent()` | |
| 241 | +| `wait_timed_out` | A `step.waitForEvent()` call times out | |
| 242 | + |
| 243 | +### Rollback events |
| 244 | + |
| 245 | +| Event type | Emitted when | |
| 246 | +| ---------------------------- | --------------------------------------- | |
| 247 | +| `rollback_started` | The Workflow starts a rollback | |
| 248 | +| `rollback_step_started` | A rollback handler starts | |
| 249 | +| `rollback_step_completed` | A rollback handler completes | |
| 250 | +| `rollback_step_errored` | A rollback handler errors | |
| 251 | +| `rollback_attempt_started` | A rollback attempt starts | |
| 252 | +| `rollback_attempt_completed` | A rollback attempt completes | |
| 253 | +| `rollback_attempt_errored` | A rollback attempt errors | |
| 254 | +| `rollback_completed` | All required rollback handlers complete | |
| 255 | +| `rollback_errored` | The rollback operation errors | |
| 256 | + |
| 257 | +For method signatures and option types, refer to [`WorkflowInstance.subscribe()`](/workflows/build/workers-api/#subscribe). |
0 commit comments