Rules of Workflows·续2

// ✅ Good: use an ID that is unique // e.g. a transaction ID, order ID, or task ID are good options let instanceId = getTransactionId(); // e.g. assuming transaction IDs are unique // or: compose a composite ID and store it in your database // so that you can track all instances associated with a specific user or merchant. instanceId = ${getUserId(req)}-${crypto.randomUUID().slice(0, 6)}; let { result } = await addNewInstanceToDB(userId, instanceId); let goodInstance = await env.MY_WORKFLOW.create({ id: instanceId, params: payload, });

return Response.json({ id: goodInstance.id, details: await goodInstance.status(), }); }, };


### `await` your steps

When calling `step.do` or `step.sleep`, use `await` to avoid introducing bugs and race conditions into your Workflow code.

If you don't call `await step.do` or `await step.sleep`, you create a dangling Promise. This occurs when a Promise is created but not properly `await`ed, leading to potential bugs and race conditions.

This happens when you do not use the `await` keyword or fail to chain `.then()` methods to handle the result of a Promise. For example, calling `fetch(GITHUB_URL)` without awaiting its response will cause subsequent code to execute immediately, regardless of whether the fetch completed. This can cause issues like premature logging, exceptions being swallowed (and not terminating the Workflow), and lost return values (state).

*index.jsjs*

```js
export class MyWorkflow extends WorkflowEntrypoint {
    async run(event, step) {
        // 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
        const badIssues = step.do(`fetch issues from GitHub`, async () => {
            // The step will return before this call is done
            let issues = await getIssues(event.payload.repoName);
            return issues;
        });

// ✅ Good: The step is correctly await'ed.
        const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
            let issues = await getIssues(event.payload.repoName);
            return issues;
        });

// Rest of your Workflow goes here!
    }
}

index.tsts

export class MyWorkflow extends WorkflowEntrypoint {
    async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
        // 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
        const badIssues = step.do(`fetch issues from GitHub`, async () => {
            // The step will return before this call is done
            let issues = await getIssues(event.payload.repoName);
            return issues;
        });

// ✅ Good: The step is correctly await'ed.
        const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
            let issues = await getIssues(event.payload.repoName);
            return issues;
        });

// Rest of your Workflow goes here!
    }
}

Use conditional logic carefully

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.

index.jsjs

export class MyWorkflow extends WorkflowEntrypoint {
    async run(event, step) {
        const config = await step.do("fetch config", async () => {
            return await this.env.KV.get("feature-flags", { type: "json" });
        });

// ✅ Good: Condition based on step output (deterministic)
        if (config.enableEmailNotifications) {
            await step.do("send email", async () => {
                // Send email logic
            });
        }

// ✅ Good: Condition based on event payload (deterministic)
        if (event.payload.userType === "premium") {
            await step.do("premium processing", async () => {
                // Premium-only logic
            });
        }

// 🔴 Bad: Condition based on non-deterministic value outside a step
        // This could behave differently if the Workflow restarts
        if (Math.random() > 0.5) {
            await step.do("maybe do something", async () => {});
        }

// ✅ Good: Wrap non-deterministic values in a step
        const shouldProcess = await step.do("decide randomly", async () => {
            return Math.random() > 0.5;
        });
        if (shouldProcess) {
            await step.do("conditionally do something", async () => {});
        }
    }
}

index.tsts

export class MyWorkflow extends WorkflowEntrypoint {
    async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
        const config = await step.do("fetch config", async () => {
            return await this.env.KV.get("feature-flags", { type: "json" });
        });

// ✅ Good: Condition based on step output (deterministic)
        if (config.enableEmailNotifications) {
            await step.do("send email", async () => {
                // Send email logic
            });
        }

// ✅ Good: Condition based on event payload (deterministic)
        if (event.payload.userType === "premium") {
            await step.do("premium processing", async () => {
                // Premium-only logic
            });
        }

// 🔴 Bad: Condition based on non-deterministic value outside a step
        // This could behave differently if the Workflow restarts
        if (Math.random() > 0.5) {
            await step.do("maybe do something", async () => {});
        }

// ✅ Good: Wrap non-deterministic values in a step
        const shouldProcess = await step.do("decide randomly", async () => {
            return Math.random() > 0.5;
        });
        if (shouldProcess) {
            await step.do("conditionally do something", async () => {});
        }
    }
}

Batch multiple Workflow invocations

When creating multiple Workflow instances, use the 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. However, each individual instance in the batch will still count towards the creation rate limit. Unlike create, createBatch does not throw if an existing instance with the same ID is still within its retention limit. Instead, it skips that instance and reports it in the errors array of the result.

index.jsjs

export default {
    async fetch(req, env) {
        let instances = [
            { id: "user1", params: { name: "John" } },
            { id: "user2", params: { name: "Jane" } },
            { id: "user3", params: { name: "Alice" } },
            { id: "user4", params: { name: "Bob" } },
        ];

// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
        for (let instance of instances) {
            await env.MY_WORKFLOW.create({
                id: instance.id,
                params: instance.params,
            });
        }

// ✅ Good: Batch calls together
        // This improves throughput.
        let { created, errors } = await env.MY_WORKFLOW.createBatch({ instances });
        return Response.json({
            instances: created.map((instance) => instance.id),
            errors,
        });
    },
};

index.tsts

export default {
    async fetch(req: Request, env: Env): Promise<Response> {
        let instances = [
            { id: "user1", params: { name: "John" } },
            { id: "user2", params: { name: "Jane" } },
            { id: "user3", params: { name: "Alice" } },
            { id: "user4", params: { name: "Bob" } },
        ];

// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
        for (let instance of instances) {
            await env.MY_WORKFLOW.create({
                id: instance.id,
                params: instance.params,
            });
        }

// ✅ Good: Batch calls together
        // This improves throughput.
        let { created, errors } = await env.MY_WORKFLOW.createBatch({ instances });
        return Response.json({
            instances: created.map((instance) => instance.id),
            errors,
        });
    },
};

Limit timeouts to 30 minutes or less

When setting a WorkflowStep timeout, ensure that its duration is 30 minutes or less. If your use case requires a timeout greater than 30 minutes, consider using step.waitForEvent() instead.

Keep non-stream step return values under 1 MiB

A non-stream step.do() return value can persist up to 1 MiB (2^20 bytes). If your step returns structured data exceeding this limit, the step will fail. This is a common issue when fetching large API responses or processing large files.

In JavaScript Workflows, ReadableStream<Uint8Array> is a supported serializable return type for larger binary output. When persisting this kind of output, you should:

  • Return a new stream from the step callback.
  • Keep individual chunks under 16 MB.
  • Do not return a locked stream or a stream that has already been read.
  • Rely only on streams returned from steps.

Note

Only byte streams are supported - use ReadableStream<Uint8Array>.

BYOB streams and BYOB readers are not supported. :::

Note that streamed outputs are still considered part of the Workflow instance storage limit.

If these storage limits still do not work for you, consider storing your step outputs externally (for example, in R2) and saving a reference to it.

index.jsjs

export class MyWorkflow extends WorkflowEntrypoint {
    async run(event, step) {
        // 🔴 Bad: Returning a large response that may exceed 1 MiB
        const largeData = await step.do("fetch large dataset", async () => {
            const response = await fetch("https://api.example.com/large-dataset");
            return await response.json(); // Could exceed 1 MiB
        });

// ✅ Good: Store large structured data externally and return a reference
        const dataRef = await step.do("fetch and store large dataset", async () => {
            const response = await fetch("https://api.example.com/large-dataset");
            const data = await response.json();
            // Store in R2 and return a reference
            await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
            return { key: "dataset-123" };
        });

// Retrieve the data in a later step when needed
        const data = await step.do("process dataset", async () => {
            const stored = await this.env.MY_BUCKET.get(dataRef.key);
            return processData(await stored.json());
        });
    }
}

index.tsts

export class MyWorkflow extends WorkflowEntrypoint {
    async run(event: WorkflowEvent&lt;Params>, step: WorkflowStep) {
        // 🔴 Bad: Returning a large response that may exceed 1 MiB
        const largeData = await step.do("fetch large dataset", async () => {
            const response = await fetch("https://api.example.com/large-dataset");
            return await response.json(); // Could exceed 1 MiB
        });

// ✅ Good: Store large structured data externally and return a reference
        const dataRef = await step.do("fetch and store large dataset", async () => {
            const response = await fetch("https://api.example.com/large-dataset");
            const data = await response.json();
            // Store in R2 and return a reference
            await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
            return { key: "dataset-123" };
        });

// Retrieve the data in a later step when needed
        const data = await step.do("process dataset", async () => {
            const stored = await this.env.MY_BUCKET.get(dataRef.key);
            return processData(await stored.json());
        });
    }
}
  • Workers Best Practices: code patterns for request handling, observability, and security that apply to the Workers triggering your Workflows.
  • Rules of Durable Objects: best practices for stateful, coordinated applications — useful when combining Durable Objects with Workflows.

Was this helpful?

YesNo

On this page

Rules of Workflows·续2 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workflows/build/rules-of-workflows/#page","headline":"Rules of Workflows","description":"Best practices for building resilient Workflows, including idempotency, state management, and error handling.","url":"https://developers.cloudflare.com/workflows/build/rules-of-workflows/","inLanguage":"en","image":"https://developers.cloudflare.com/workflows/build/rules-of-workflows/og.png?v=d41ea2e8f47b17f3","dateModified":"2026-10-15","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Configure sleep durations and retry logic for Workflows steps, including relative and absolute sleep timers. title: Sleeping and retrying image: https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/og.png?v=729dfea88ce53cd9


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workflows/llms.txt
Use this file to discover all available pages before exploring further.