Skip to content

Commit 1a51c72

Browse files
elithrarmia303
andauthored
workflows: incorporate user feedback (#26871)
* workflows: incorporate user feedback * add TypeScriptExample to other code examples * Update src/content/docs/workflows/reference/limits.mdx Co-authored-by: mia303 <[email protected]> --------- Co-authored-by: mia303 <[email protected]>
1 parent b56251e commit 1a51c72

File tree

4 files changed

+152
-2
lines changed

4 files changed

+152
-2
lines changed

src/content/docs/workflows/build/events-and-parameters.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,29 @@ let event = await step.waitForEvent(
120120

121121
You can specify a timeout between 1 second and up to 365 days.
122122

123+
:::caution[Timeout behavior]
124+
125+
When `waitForEvent` times out, the Workflow will throw an error and the instance will fail. If you want your Workflow to continue even if the event is not received, wrap the `waitForEvent` call in a try-catch block:
126+
127+
<TypeScriptExample>
128+
129+
```ts
130+
try {
131+
const event = await step.waitForEvent("wait for approval", {
132+
type: "approval",
133+
timeout: "1 hour",
134+
});
135+
// Handle the received event
136+
} catch (e) {
137+
// Timeout occurred - handle the case where no event was received
138+
console.log("No approval received, proceeding with default action");
139+
}
140+
```
141+
142+
</TypeScriptExample>
143+
144+
:::
145+
123146
### Send events to running workflows
124147

125148
Workflow instances that are waiting on events using the `waitForEvent` API can be sent events using the `instance.sendEvent` API:
@@ -153,6 +176,12 @@ export default {
153176
- To send multiple events to a Workflow that has multiple `waitForEvent` calls, call `sendEvent` with the corresponding `type` property set (up to 100 characters [^1]).
154177
- Events can also be sent using the REST API (HTTP API)'s [Events endpoint](/api/resources/workflows/subresources/instances/subresources/events/methods/create/).
155178

179+
:::note[Event timing]
180+
181+
You can send an event to a Workflow instance _before_ it reaches the corresponding `waitForEvent` call, as long as the instance has been created. The event will be buffered and delivered when the Workflow reaches the `waitForEvent` step with the matching `type`.
182+
183+
:::
184+
156185
## TypeScript and type parameters
157186

158187
By default, the `WorkflowEvent` passed to the `run` method of your Workflow definition has a type that conforms to the following, with `payload` (your data), `timestamp`, and `instanceId` properties:

src/content/docs/workflows/build/rules-of-workflows.mdx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,52 @@ export class MyWorkflow extends WorkflowEntrypoint {
509509

510510
</TypeScriptExample>
511511

512+
### Use conditional logic carefully
513+
514+
You can use `if` statements, loops, and other control flow outside of steps. However, conditions must be based on **deterministic values** — either values from `event.payload` or return values from previous steps. Non-deterministic conditions (such as `Math.random()` or `Date.now()`) outside of steps can cause unexpected behavior if the Workflow restarts.
515+
516+
<TypeScriptExample filename="index.ts">
517+
518+
```ts
519+
export class MyWorkflow extends WorkflowEntrypoint {
520+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
521+
const config = await step.do("fetch config", async () => {
522+
return await this.env.KV.get("feature-flags", { type: "json" });
523+
});
524+
525+
// ✅ Good: Condition based on step output (deterministic)
526+
if (config.enableEmailNotifications) {
527+
await step.do("send email", async () => {
528+
// Send email logic
529+
});
530+
}
531+
532+
// ✅ Good: Condition based on event payload (deterministic)
533+
if (event.payload.userType === "premium") {
534+
await step.do("premium processing", async () => {
535+
// Premium-only logic
536+
});
537+
}
538+
539+
// 🔴 Bad: Condition based on non-deterministic value outside a step
540+
// This could behave differently if the Workflow restarts
541+
if (Math.random() > 0.5) {
542+
await step.do("maybe do something", async () => {});
543+
}
544+
545+
// ✅ Good: Wrap non-deterministic values in a step
546+
const shouldProcess = await step.do("decide randomly", async () => {
547+
return Math.random() > 0.5;
548+
});
549+
if (shouldProcess) {
550+
await step.do("conditionally do something", async () => {});
551+
}
552+
}
553+
}
554+
```
555+
556+
</TypeScriptExample>
557+
512558
### Batch multiple Workflow invocations
513559

514560
When creating multiple Workflow instances, use the [`createBatch`](/workflows/build/workers-api/#createBatch) method to batch the invocations together. This allows you to create multiple Workflow instances in a single request, which will reduce the number of requests made to the Workflows API and increase the number of instances you can create per minute.
@@ -542,3 +588,40 @@ export default {
542588
```
543589

544590
</TypeScriptExample>
591+
592+
### Keep step return values under 1 MiB
593+
594+
Each step can persist up to 1 MiB (2^20 bytes) of state. If your step returns data exceeding this limit, the step will fail. This is a common issue when fetching large API responses or processing large files.
595+
596+
To work around this limit, store large data externally (for example, in R2 or KV) and return only a reference:
597+
598+
<TypeScriptExample filename="index.ts">
599+
600+
```ts
601+
export class MyWorkflow extends WorkflowEntrypoint {
602+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
603+
// 🔴 Bad: Returning a large response that may exceed 1 MiB
604+
const largeData = await step.do("fetch large dataset", async () => {
605+
const response = await fetch("https://api.example.com/large-dataset");
606+
return await response.json(); // Could exceed 1 MiB
607+
});
608+
609+
// ✅ Good: Store large data externally and return a reference
610+
const dataRef = await step.do("fetch and store large dataset", async () => {
611+
const response = await fetch("https://api.example.com/large-dataset");
612+
const data = await response.json();
613+
// Store in R2 and return a reference
614+
await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
615+
return { key: "dataset-123" };
616+
});
617+
618+
// Retrieve the data in a later step when needed
619+
const data = await step.do("process dataset", async () => {
620+
const stored = await this.env.MY_BUCKET.get(dataRef.key);
621+
return processData(await stored.json());
622+
});
623+
}
624+
}
625+
```
626+
627+
</TypeScriptExample>

src/content/docs/workflows/build/trigger-workflows.mdx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ sidebar:
77
order: 3
88
---
99

10-
import { WranglerConfig } from "~/components";
10+
import { TypeScriptExample, WranglerConfig } from "~/components";
1111

1212
You can trigger Workflows both programmatically and via the Workflows APIs, including:
1313

@@ -162,6 +162,42 @@ await instance.restart(); // Returns Promise<void>
162162

163163
Restarting an instance will immediately cancel any in-progress steps, erase any intermediate state, and treat the Workflow as if it was run for the first time.
164164

165+
### Trigger a Workflow from another Workflow
166+
167+
You can create a new Workflow instance from within a step of another Workflow. The parent Workflow will not block waiting for the child Workflow to complete — it continues execution immediately after the child instance is successfully created.
168+
169+
<TypeScriptExample>
170+
171+
```ts
172+
export class ParentWorkflow extends WorkflowEntrypoint<Env, Params> {
173+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
174+
// Perform initial work
175+
const result = await step.do("initial processing", async () => {
176+
// ... processing logic
177+
return { fileKey: "output.pdf" };
178+
});
179+
180+
// Trigger a child workflow for additional processing
181+
const childInstance = await step.do("trigger child workflow", async () => {
182+
return await this.env.CHILD_WORKFLOW.create({
183+
id: `child-${event.instanceId}`,
184+
params: { fileKey: result.fileKey },
185+
});
186+
});
187+
188+
// Parent continues immediately - not blocked by child workflow
189+
await step.do("continue with other work", async () => {
190+
console.log(`Started child workflow: ${childInstance.id}`);
191+
// This runs right away, regardless of child workflow status
192+
});
193+
}
194+
}
195+
```
196+
197+
</TypeScriptExample>
198+
199+
If the child Workflow fails to start, the step will fail and be retried according to your retry configuration. Once the child instance is successfully created, it runs independently from the parent.
200+
165201
## REST API (HTTP)
166202

167203
Refer to the [Workflows REST API documentation](/api/resources/workflows/subresources/instances/methods/create/).

src/content/docs/workflows/reference/limits.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ Many limits are inherited from those applied to Workers scripts and as documente
4949

5050
### `waiting` instances do not count towards instance concurrency limits
5151

52-
Instances that are on a `waiting` state - either sleeping, waiting for a retry, or waiting for an event - do **not** count towards concurrency limits. This means that other `queued` instances will be scheduled when an instance goes from a `running` state to a `waiting` one, usually the oldest instance queued, on a best-effort basis. This state transition - `running` to `waiting` - may not occur if the wait duration is too short.
52+
Instances that are in a `waiting` state — either sleeping via `step.sleep`, waiting for a retry, or waiting for an event via `step.waitForEvent` — do **not** count towards concurrency limits. This means you can have millions of Workflow instances sleeping or waiting for events simultaneously, as only actively `running` instances count toward the 10,000 concurrent instance limit.
53+
However, if there are 10,000 concurrent instances actively running, an instance that has been in a `waiting` state will be queued instead of resuming immediately.
54+
When an instance transitions from `running` to `waiting`, other `queued` instances will be scheduled (usually the oldest queued instance, on a best-effort basis). This state transition may not occur if the wait duration is very short.
5355

5456
For example, consider a Workflow that does some work, waits for 30 days, and then continues with more work:
5557

0 commit comments

Comments
 (0)