Skip to content
97 changes: 96 additions & 1 deletion src/content/docs/workflows/build/rules-of-workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export class MyWorkflow extends WorkflowEntrypoint {

### Name steps deterministically

Steps should be named deterministically (ie, not using the current date/time, randomness, etc). This ensures that their state is cached, and prevents the step from being rerun unnecessarily. Step names act as the "cache key" in your Workflow.
Steps should be named deterministically (ie, not using the current date/time, randomness, etc). This ensures that their state is cached, and prevents the step from being rerun unnecessarily. Step names act as the "cache key" in your Workflow.

<TypeScriptExample filename="index.ts">
```ts
Expand Down Expand Up @@ -283,6 +283,101 @@ export class MyWorkflow extends WorkflowEntrypoint {
```
</TypeScriptExample>

### Take care with `Promise.race()` and `Promise.any()`

Workflows allows the usage steps within the `Promise.race()` or `Promise.any()` methods as a way to achieve concurrent steps execution. However, some considerations must be taken.

Due to the nature of Workflows' instance lifecycle, and given that a step inside a Promise will run until it finishes, the step that is returned during the first passage may not be the actual cached step, as [steps are cached by their names](#name-steps-deterministically).

<TypeScriptExample filename="index.ts">
```ts

// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: The `Promise.race` is not surround by a `step.do` which may cause undeterministic caching behavior.
const race_return = await Promise.race(
[
step.do(
'Promise first race',
async () => {
await sleep(1000);
return "first";
}
),
step.do(
'Promise second race',
async () => {
return "second";
}
),
]
);

await step.sleep("Sleep step", "2 hours");

return await step.do(
'Another step',
async () => {
// This step will return `first`, even though the `Promise.race` first returned `second`.
return race_return;
},
);
}
}
```
</TypeScriptExample>

To ensure consistency we suggest to surround the `Promise.race()` or `Promise.any()` within a `step.do()`, as this will ensure caching consistency across multiple passages.

<TypeScriptExample filename="index.ts">
```ts

// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// ✅ Good: The `Promise.race` is surround by a `step.do`, ensuring deterministic caching behavior.
const race_return = await step.do(
'Promise step',
async () => {
return await Promise.race(
[
step.do(
'Promise first race',
async () => {
await sleep(1000);
return "first";
}
),
step.do(
'Promise second race',
async () => {
return "second";
}
),
]
);
}
);

await step.sleep("Sleep step", "2 hours");

return await step.do(
'Another step',
async () => {
// This step will return `second`, as the `Promise.race` was surround by `step.do` method.
return race_return;
},
);
}
}
```
</TypeScriptExample>

### Instance IDs are unique

Workflow [instance IDs](/workflows/build/workers-api/#workflowinstance) are unique per Workflow. The ID is the unique identifier that associates logs, metrics, state and status of a run to a specific an instance, even after completion. Allowing ID re-use would make it hard to understand if a Workflow instance ID referred to an instance that run yesterday, last week or today.
Expand Down
Loading