From 42f51cdfc37b70b67922990a08e64e42859040f2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 8 Feb 2026 16:12:14 +0000 Subject: [PATCH 1/2] Fix Workflows docs code & API errors Co-authored-by: elithrar --- .../cloudflare/references/workflows/README.md | 2 +- skills/cloudflare/references/workflows/api.md | 17 ++---- .../references/workflows/gotchas.md | 2 +- .../references/workflows/patterns.md | 29 +++++----- .../workflows/2025-04-07-workflows-ga.mdx | 2 +- .../workflows/build/events-and-parameters.mdx | 6 +- .../workflows/build/rules-of-workflows.mdx | 58 +++++++++---------- .../workflows/build/sleeping-and-retrying.mdx | 6 +- .../docs/workflows/build/workers-api.mdx | 4 +- 9 files changed, 61 insertions(+), 65 deletions(-) diff --git a/skills/cloudflare/references/workflows/README.md b/skills/cloudflare/references/workflows/README.md index 561f907bee1..1f482594440 100644 --- a/skills/cloudflare/references/workflows/README.md +++ b/skills/cloudflare/references/workflows/README.md @@ -31,7 +31,7 @@ export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { const user = await step.do('fetch user', async () => { return await this.env.DB.prepare('SELECT * FROM users WHERE id = ?') - .bind(event.params.userId).first(); + .bind(event.payload.userId).first(); }); await step.sleep('wait 7 days', '7 days'); diff --git a/skills/cloudflare/references/workflows/api.md b/skills/cloudflare/references/workflows/api.md index 7ac56257894..44ba5df1cb6 100644 --- a/skills/cloudflare/references/workflows/api.md +++ b/skills/cloudflare/references/workflows/api.md @@ -15,8 +15,8 @@ await step.sleep('description', 5000); // ms await step.sleepUntil('description', Date.parse('2024-12-31')); // step.waitForEvent() -const data = await step.waitForEvent('wait', {event: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d -try { const event = await step.waitForEvent('wait', { event: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ } +const data = await step.waitForEvent('wait', {type: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d +try { const event = await step.waitForEvent('wait', { type: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ } ``` ## Instance Management @@ -25,13 +25,6 @@ try { const event = await step.waitForEvent('wait', { event: 'approval', timeout // Create single const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); // id optional, auto-generated if omitted -// Create with custom retention (default: 3 days free, 30 days paid) -const instance = await env.MY_WORKFLOW.create({ - id: crypto.randomUUID(), - params: { userId: 'user123' }, - retention: '30 days' // Override default retention period -}); - // Batch (max 100, idempotent: skips existing IDs) const instances = await env.MY_WORKFLOW.createBatch([{id: 'user1', params: {name: 'John'}}, {id: 'user2', params: {name: 'Jane'}}]); @@ -69,11 +62,11 @@ export class ParentWorkflow extends WorkflowEntrypoint { ## Error Handling ```typescript -import { NonRetryableError } from 'cloudflare:workers'; +import { NonRetryableError } from 'cloudflare:workflows'; // NonRetryableError await step.do('validate', async () => { - if (!event.params.paymentMethod) throw new NonRetryableError('Payment method required'); + if (!event.payload.paymentMethod) throw new NonRetryableError('Payment method required'); const res = await fetch('https://api.example.com/charge', { method: 'POST' }); if (res.status === 401) throw new NonRetryableError('Invalid credentials'); // Don't retry if (!res.ok) throw new Error('Retryable failure'); // Will retry @@ -146,7 +139,7 @@ const instance = await env.MY_WORKFLOW.create({ **Access in Workflow:** ```typescript async run(event: WorkflowEvent, step: WorkflowStep) { - const userId = event.params.userId; + const userId = event.payload.userId; const instanceId = event.instanceId; const createdAt = event.timestamp; } diff --git a/skills/cloudflare/references/workflows/gotchas.md b/skills/cloudflare/references/workflows/gotchas.md index 6f854446fdf..8909126d02d 100644 --- a/skills/cloudflare/references/workflows/gotchas.md +++ b/skills/cloudflare/references/workflows/gotchas.md @@ -66,7 +66,7 @@ | Instance state | 100 MB | 1 GB | Total state per workflow instance | | Steps per workflow | 1,024 | 1,024 | `step.sleep()` doesn't count | | Executions per day | 100k | Unlimited | Daily execution limit | -| Concurrent instances | 25 | 10k | Maximum concurrent workflows; waiting state excluded | +| Concurrent instances | 100 | 10k | Maximum concurrent workflows; waiting state excluded | | Queued instances | 100k | 1M | Maximum queued workflow instances | | Subrequests per step | 50 | 1,000 | Maximum outbound requests per step | | State retention | 3 days | 30 days | How long completed instances kept | diff --git a/skills/cloudflare/references/workflows/patterns.md b/skills/cloudflare/references/workflows/patterns.md index 72ce024658c..043148b3683 100644 --- a/skills/cloudflare/references/workflows/patterns.md +++ b/skills/cloudflare/references/workflows/patterns.md @@ -5,12 +5,12 @@ ```typescript export class ImageProcessingWorkflow extends WorkflowEntrypoint { async run(event, step) { - const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.params.imageKey)).arrayBuffer()); + const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.payload.imageKey)).arrayBuffer()); const description = await step.do('generate description', async () => await this.env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {image: Array.from(new Uint8Array(imageData)), prompt: 'Describe this image', max_tokens: 50}) ); - await step.waitForEvent('await approval', { event: 'approved', timeout: '24h' }); - await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.params.imageKey}`, imageData)); + await step.waitForEvent('await approval', { type: 'approved', timeout: '24h' }); + await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.payload.imageKey}`, imageData)); } } ``` @@ -20,13 +20,13 @@ export class ImageProcessingWorkflow extends WorkflowEntrypoint { ```typescript export class UserLifecycleWorkflow extends WorkflowEntrypoint { async run(event, step) { - await step.do('welcome email', async () => await sendEmail(event.params.email, 'Welcome!')); + await step.do('welcome email', async () => await sendEmail(event.payload.email, 'Welcome!')); await step.sleep('trial period', '7 days'); const hasConverted = await step.do('check conversion', async () => { - const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.params.userId).first(); + const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.payload.userId).first(); return user.subscription_status === 'active'; }); - if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.params.email, 'Trial ending')); + if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.payload.email, 'Trial ending')); } } ``` @@ -37,7 +37,7 @@ export class UserLifecycleWorkflow extends WorkflowEntrypoint { export class DataPipelineWorkflow extends WorkflowEntrypoint { async run(event, step) { const rawData = await step.do('extract', {retries: { limit: 10, delay: '30s', backoff: 'exponential' }}, async () => { - const res = await fetch(event.params.sourceUrl); + const res = await fetch(event.payload.sourceUrl); if (!res.ok) throw new Error('Fetch failed'); return res.json(); }); @@ -66,9 +66,9 @@ export class DataPipelineWorkflow extends WorkflowEntrypoint { ```typescript export class ApprovalWorkflow extends WorkflowEntrypoint { async run(event, step) { - await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.params.userId, 'pending').run()); + await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.payload.userId, 'pending').run()); try { - const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { event: 'approval-response', timeout: '48h' }); + const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { type: 'approval-response', timeout: '48h' }); if (approval.approved) { await step.do('process approval', async () => {}); } else { await step.do('handle rejection', async () => {}); } } catch (e) { @@ -154,10 +154,13 @@ await step.do('other work', async () => console.log(`Child started: ${child.id}` ### Race Pattern ```typescript -const winner = await Promise.race([ - step.do('option A', async () => slowOperation()), - step.do('option B', async () => fastOperation()) -]); +// Wrap Promise.race in a step.do to ensure deterministic caching behavior +const winner = await step.do('race step', async () => { + return await Promise.race([ + step.do('option A', async () => slowOperation()), + step.do('option B', async () => fastOperation()) + ]); +}); ``` ### Scheduled Workflow Chain diff --git a/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx b/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx index b08e30f91b4..2d9d1a6b085 100644 --- a/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx +++ b/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx @@ -29,7 +29,7 @@ For example, if you wanted to implement a human-in-the-loop approval process, yo ```ts -import { Workflow, WorkflowEvent } from "cloudflare:workflows"; +import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:workers"; export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { diff --git a/src/content/docs/workflows/build/events-and-parameters.mdx b/src/content/docs/workflows/build/events-and-parameters.mdx index 6d8c6b6541b..55778fc9152 100644 --- a/src/content/docs/workflows/build/events-and-parameters.mdx +++ b/src/content/docs/workflows/build/events-and-parameters.mdx @@ -92,7 +92,7 @@ For example, to wait for billing webhook: export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // Other steps in your Workflow - let event = await step.waitForEvent( + let stripeEvent = await step.waitForEvent( "receive invoice paid webhook from Stripe", { type: "stripe-webhook", timeout: "1 hour" }, ); @@ -223,13 +223,13 @@ export class MyWorkflow extends WorkflowEntrypoint { // Pass your type as a type parameter to WorkflowEvent // The 'payload' property will have the type of your parameter. async run(event: WorkflowEvent, step: WorkflowStep) { - let state = step.do("my first step", async () => { + let state = await step.do("my first step", async () => { // Access your properties via event.payload let userEmail = event.payload.userEmail let createdTimestamp = event.payload.createdTimestamp }) - step.do("my second step", async () => { /* your code here */ ) + await step.do("my second step", async () => { /* your code here */ }) } } ``` diff --git a/src/content/docs/workflows/build/rules-of-workflows.mdx b/src/content/docs/workflows/build/rules-of-workflows.mdx index bc75317e190..1d20dd3d7fe 100644 --- a/src/content/docs/workflows/build/rules-of-workflows.mdx +++ b/src/content/docs/workflows/build/rules-of-workflows.mdx @@ -83,7 +83,7 @@ export class MyWorkflow extends WorkflowEntrypoint { // timeout policies for each granular step - you might not to want to overload http.cat in // case of it being down. const httpCat = await step.do("get cutest cat from KV", async () => { - return await env.KV.get("cutest-http-cat"); + return await this.env.KV.get("cutest-http-cat"); }); const image = await step.do("fetch cat image from http.cat", async () => { @@ -111,7 +111,7 @@ export class MyWorkflow extends WorkflowEntrypoint { // some extra calls to the first service in case the second one fails, and in some cases, makes // the step non-idempotent altogether const image = await step.do("get cutest cat from KV", async () => { - const httpCat = await env.KV.get("cutest-http-cat"); + const httpCat = await this.env.KV.get("cutest-http-cat"); return fetch(`https://http.cat/${httpCat}`); }); } @@ -142,15 +142,15 @@ export class MyWorkflow extends WorkflowEntrypoint { const imageList: string[] = []; await step.do("get first cutest cat from KV", async () => { - const httpCat = await env.KV.get("cutest-http-cat-1"); + const httpCat = await this.env.KV.get("cutest-http-cat-1"); - imageList.append(httpCat); + imageList.push(httpCat); }); await step.do("get second cutest cat from KV", async () => { - const httpCat = await env.KV.get("cutest-http-cat-2"); + const httpCat = await this.env.KV.get("cutest-http-cat-2"); - imageList.append(httpCat); + imageList.push(httpCat); }); // A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here @@ -188,11 +188,11 @@ export class MyWorkflow extends WorkflowEntrypoint { // multiple engine lifetimes, imageList will be built accordingly const imageList: string[] = await Promise.all([ step.do("get first cutest cat from KV", async () => { - return await env.KV.get("cutest-http-cat-1"); + return await this.env.KV.get("cutest-http-cat-1"); }), step.do("get second cutest cat from KV", async () => { - return await env.KV.get("cutest-http-cat-2"); + return await this.env.KV.get("cutest-http-cat-2"); }), ]); @@ -221,7 +221,7 @@ It is not recommended to write code with any side effects outside of steps, unle For example, a `console.log()` outside of workflow steps may cause the logs to print twice when the engine restarts. -However, logic involving non-serializable resources, like a database connection, should be executed outside of steps. Operations ouside of a `step.do` might be repeated more than once, due to the nature of the Workflows' instance lifecycle. +However, logic involving non-serializable resources, like a database connection, should be executed outside of steps. Operations outside of a `step.do` might be repeated more than once, due to the nature of the Workflows' instance lifecycle. @@ -230,14 +230,14 @@ export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // 🔴 Bad: creating instances outside of steps // This might get called more than once creating more instances than expected - const myNewInstance = await this.env.ANOTHER_WORKFLOW.create(); + const badInstance = await this.env.ANOTHER_WORKFLOW.create(); // 🔴 Bad: using non-deterministic functions outside of steps // this will produce different results if the instance has to restart, different runs of the same instance // might go through different paths - const myRandom = Math.random(); + const badRandom = Math.random(); - if (myRandom > 0) { + if (badRandom > 0) { // do some stuff } @@ -253,21 +253,21 @@ export class MyWorkflow extends WorkflowEntrypoint { // ✅ Good: wrap non-deterministic function in a step // after running successfully will not run again - const myRandom = await step.do("create a random number", async () => { + const goodRandom = await step.do("create a random number", async () => { return Math.random(); }); // ✅ Good: calls that have no side effects can be done outside of steps const db = createDBConnection(this.env.DB_URL, this.env.DB_TOKEN); - // ✅ Good: run funtions with side effects inside of a step + // ✅ Good: run functions with side effects inside of a step // after running successfully will not run again - const myNewInstance = await step.do( + const goodInstance = await step.do( "good step that returns state", async () => { - const myNewInstance = await this.env.ANOTHER_WORKFLOW.create(); + const instance = await this.env.ANOTHER_WORKFLOW.create(); - return myNewInstance; + return instance; }, ); } @@ -294,14 +294,14 @@ export class MyWorkflow extends WorkflowEntrypoint { // This will not be persisted across steps and `event.payload` will // take on its original value. await step.do("bad step that mutates the incoming event", async () => { - let userData = await env.KV.get(event.payload.user); + let userData = await this.env.KV.get(event.payload.user); event.payload = userData; }); // ✅ Good: persist data by returning it as state from your step // Use that state in subsequent steps let userData = await step.do("good step that returns state", async () => { - return await env.KV.get(event.payload.user); + return await this.env.KV.get(event.payload.user); }); let someOtherData = await step.do( @@ -329,7 +329,7 @@ export class MyWorkflow extends WorkflowEntrypoint { // 🔴 Bad: Naming the step non-deterministically prevents it from being cached // This will cause the step to be re-run if subsequent steps fail. await step.do(`step #1 running at: ${Date.now()}`, async () => { - let userData = await env.KV.get(event.payload.user); + let userData = await this.env.KV.get(event.payload.user); // Do not mutate event.payload event.payload = userData; }); @@ -337,8 +337,8 @@ export class MyWorkflow extends WorkflowEntrypoint { // ✅ Good: give steps a deterministic name. // Return dynamic values in your state, or log them instead. let state = await step.do("fetch user data from KV", async () => { - let userData = await env.KV.get(event.payload.user); - console.log(`fetched at ${Date.now}`); + let userData = await this.env.KV.get(event.payload.user); + console.log(`fetched at ${Date.now()}`); return userData; }); @@ -347,12 +347,12 @@ export class MyWorkflow extends WorkflowEntrypoint { // traversed in a deterministic fashion (no shuffles or random accesses) so, // it's fine to dynamically name steps (e.g: create a step per list entry). let catList = await step.do("get cat list from KV", async () => { - return await env.KV.get("cat-list"); + return await this.env.KV.get("cat-list"); }); for (const cat of catList) { await step.do(`get cat: ${cat}`, async () => { - return await env.KV.get(cat); + return await this.env.KV.get(cat); }); } } @@ -490,14 +490,14 @@ This happens when you do not use the `await` keyword or fail to chain `.then()` export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns. - const issues = step.do(`fetch issues from GitHub`, async () => { + 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 issues = await step.do(`fetch issues from GitHub`, async () => { + const goodIssues = await step.do(`fetch issues from GitHub`, async () => { let issues = await getIssues(event.payload.repoName); return issues; }); @@ -557,7 +557,7 @@ export class MyWorkflow extends WorkflowEntrypoint { ### Batch multiple Workflow invocations -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. However, each individual instance in the batch will still count towards the [creation rate limit](/workflows/reference/limits/). +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. However, each individual instance in the batch will still count towards the [creation rate limit](/workflows/reference/limits/). Unlike `create`, `createBatch` is idempotent: if an existing instance with the same ID is still within its [retention limit](/workflows/reference/limits/), it will be skipped and excluded from the returned array. @@ -581,8 +581,8 @@ export default { // ✅ Good: Batch calls together // This improves throughput. - let instances = await env.MY_WORKFLOW.createBatch(instances); - return Response.json({ instances }); + let createdInstances = await env.MY_WORKFLOW.createBatch(instances); + return Response.json({ instances: createdInstances }); }, }; ``` diff --git a/src/content/docs/workflows/build/sleeping-and-retrying.mdx b/src/content/docs/workflows/build/sleeping-and-retrying.mdx index 2ea50e84431..fc8d93e931c 100644 --- a/src/content/docs/workflows/build/sleeping-and-retrying.mdx +++ b/src/content/docs/workflows/build/sleeping-and-retrying.mdx @@ -77,14 +77,14 @@ When providing your own `StepConfig`, you can configure: For example, to limit a step to 10 retries and have it apply an exponential delay (starting at 10 seconds) between each attempt, you would pass the following configuration as an optional object to `step.do`: ```ts -let someState = step.do("call an API", { +let someState = await step.do("call an API", { retries: { limit: 10, // The total number of attempts delay: "10 seconds", // Delay between each retry backoff: "exponential" // Any of "constant" | "linear" | "exponential"; }, timeout: "30 minutes", -}, async () => { /* Step code goes here /* } +}, async () => { /* Step code goes here */ }) ``` ## Force a Workflow instance to fail @@ -132,7 +132,7 @@ try { // work not to be retried throw new NonRetryableError('oh no'); }); -} catch(e as Error) { +} catch (e) { console.log(`Step failed: ${e.message}`); await step.do('clean-up-task', async () => { // Clean up code here diff --git a/src/content/docs/workflows/build/workers-api.mdx b/src/content/docs/workflows/build/workers-api.mdx index ed96b69b5dd..74d6d49cb8d 100644 --- a/src/content/docs/workflows/build/workers-api.mdx +++ b/src/content/docs/workflows/build/workers-api.mdx @@ -102,7 +102,7 @@ Objects that include `Function` or `Symbol` types, and objects with circular ref {/* prettier-ignore */} - step.sleepUntil(name: string, timestamp: Date | number): Promise<void> - `name` - the name of the step. - - `timestamp` - a JavaScript `Date` object or seconds from the Unix epoch to sleep the Workflow instance until. + - `timestamp` - a JavaScript `Date` object or milliseconds from the Unix epoch to sleep the Workflow instance until. :::note @@ -122,7 +122,7 @@ More information about the limits imposed on Workflow can be found in the [Workf export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // Other steps in your Workflow - let event = await step.waitForEvent( + let stripeEvent = await step.waitForEvent( "receive invoice paid webhook from Stripe", { type: "stripe-webhook", timeout: "1 hour" }, ); From a53ce58c9bb2f02edf6d3a988e0b7458ab1a3ff6 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 8 Feb 2026 16:27:36 +0000 Subject: [PATCH 2/2] Revert skills; fix changelog shadowing Co-authored-by: elithrar --- .../cloudflare/references/workflows/README.md | 2 +- skills/cloudflare/references/workflows/api.md | 17 +++++++---- .../references/workflows/gotchas.md | 2 +- .../references/workflows/patterns.md | 29 +++++++++---------- .../workflows/2025-04-07-workflows-ga.mdx | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/skills/cloudflare/references/workflows/README.md b/skills/cloudflare/references/workflows/README.md index 1f482594440..561f907bee1 100644 --- a/skills/cloudflare/references/workflows/README.md +++ b/skills/cloudflare/references/workflows/README.md @@ -31,7 +31,7 @@ export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { const user = await step.do('fetch user', async () => { return await this.env.DB.prepare('SELECT * FROM users WHERE id = ?') - .bind(event.payload.userId).first(); + .bind(event.params.userId).first(); }); await step.sleep('wait 7 days', '7 days'); diff --git a/skills/cloudflare/references/workflows/api.md b/skills/cloudflare/references/workflows/api.md index 44ba5df1cb6..7ac56257894 100644 --- a/skills/cloudflare/references/workflows/api.md +++ b/skills/cloudflare/references/workflows/api.md @@ -15,8 +15,8 @@ await step.sleep('description', 5000); // ms await step.sleepUntil('description', Date.parse('2024-12-31')); // step.waitForEvent() -const data = await step.waitForEvent('wait', {type: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d -try { const event = await step.waitForEvent('wait', { type: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ } +const data = await step.waitForEvent('wait', {event: 'webhook-type', timeout: '24h'}); // Default 24h, max 365d +try { const event = await step.waitForEvent('wait', { event: 'approval', timeout: '1h' }); } catch (e) { /* Timeout */ } ``` ## Instance Management @@ -25,6 +25,13 @@ try { const event = await step.waitForEvent('wait', { type: 'approval', timeout: // Create single const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); // id optional, auto-generated if omitted +// Create with custom retention (default: 3 days free, 30 days paid) +const instance = await env.MY_WORKFLOW.create({ + id: crypto.randomUUID(), + params: { userId: 'user123' }, + retention: '30 days' // Override default retention period +}); + // Batch (max 100, idempotent: skips existing IDs) const instances = await env.MY_WORKFLOW.createBatch([{id: 'user1', params: {name: 'John'}}, {id: 'user2', params: {name: 'Jane'}}]); @@ -62,11 +69,11 @@ export class ParentWorkflow extends WorkflowEntrypoint { ## Error Handling ```typescript -import { NonRetryableError } from 'cloudflare:workflows'; +import { NonRetryableError } from 'cloudflare:workers'; // NonRetryableError await step.do('validate', async () => { - if (!event.payload.paymentMethod) throw new NonRetryableError('Payment method required'); + if (!event.params.paymentMethod) throw new NonRetryableError('Payment method required'); const res = await fetch('https://api.example.com/charge', { method: 'POST' }); if (res.status === 401) throw new NonRetryableError('Invalid credentials'); // Don't retry if (!res.ok) throw new Error('Retryable failure'); // Will retry @@ -139,7 +146,7 @@ const instance = await env.MY_WORKFLOW.create({ **Access in Workflow:** ```typescript async run(event: WorkflowEvent, step: WorkflowStep) { - const userId = event.payload.userId; + const userId = event.params.userId; const instanceId = event.instanceId; const createdAt = event.timestamp; } diff --git a/skills/cloudflare/references/workflows/gotchas.md b/skills/cloudflare/references/workflows/gotchas.md index 8909126d02d..6f854446fdf 100644 --- a/skills/cloudflare/references/workflows/gotchas.md +++ b/skills/cloudflare/references/workflows/gotchas.md @@ -66,7 +66,7 @@ | Instance state | 100 MB | 1 GB | Total state per workflow instance | | Steps per workflow | 1,024 | 1,024 | `step.sleep()` doesn't count | | Executions per day | 100k | Unlimited | Daily execution limit | -| Concurrent instances | 100 | 10k | Maximum concurrent workflows; waiting state excluded | +| Concurrent instances | 25 | 10k | Maximum concurrent workflows; waiting state excluded | | Queued instances | 100k | 1M | Maximum queued workflow instances | | Subrequests per step | 50 | 1,000 | Maximum outbound requests per step | | State retention | 3 days | 30 days | How long completed instances kept | diff --git a/skills/cloudflare/references/workflows/patterns.md b/skills/cloudflare/references/workflows/patterns.md index 043148b3683..72ce024658c 100644 --- a/skills/cloudflare/references/workflows/patterns.md +++ b/skills/cloudflare/references/workflows/patterns.md @@ -5,12 +5,12 @@ ```typescript export class ImageProcessingWorkflow extends WorkflowEntrypoint { async run(event, step) { - const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.payload.imageKey)).arrayBuffer()); + const imageData = await step.do('fetch', async () => (await this.env.BUCKET.get(event.params.imageKey)).arrayBuffer()); const description = await step.do('generate description', async () => await this.env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {image: Array.from(new Uint8Array(imageData)), prompt: 'Describe this image', max_tokens: 50}) ); - await step.waitForEvent('await approval', { type: 'approved', timeout: '24h' }); - await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.payload.imageKey}`, imageData)); + await step.waitForEvent('await approval', { event: 'approved', timeout: '24h' }); + await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.params.imageKey}`, imageData)); } } ``` @@ -20,13 +20,13 @@ export class ImageProcessingWorkflow extends WorkflowEntrypoint { ```typescript export class UserLifecycleWorkflow extends WorkflowEntrypoint { async run(event, step) { - await step.do('welcome email', async () => await sendEmail(event.payload.email, 'Welcome!')); + await step.do('welcome email', async () => await sendEmail(event.params.email, 'Welcome!')); await step.sleep('trial period', '7 days'); const hasConverted = await step.do('check conversion', async () => { - const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.payload.userId).first(); + const user = await this.env.DB.prepare('SELECT subscription_status FROM users WHERE id = ?').bind(event.params.userId).first(); return user.subscription_status === 'active'; }); - if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.payload.email, 'Trial ending')); + if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.params.email, 'Trial ending')); } } ``` @@ -37,7 +37,7 @@ export class UserLifecycleWorkflow extends WorkflowEntrypoint { export class DataPipelineWorkflow extends WorkflowEntrypoint { async run(event, step) { const rawData = await step.do('extract', {retries: { limit: 10, delay: '30s', backoff: 'exponential' }}, async () => { - const res = await fetch(event.payload.sourceUrl); + const res = await fetch(event.params.sourceUrl); if (!res.ok) throw new Error('Fetch failed'); return res.json(); }); @@ -66,9 +66,9 @@ export class DataPipelineWorkflow extends WorkflowEntrypoint { ```typescript export class ApprovalWorkflow extends WorkflowEntrypoint { async run(event, step) { - await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.payload.userId, 'pending').run()); + await step.do('create approval', async () => await this.env.DB.prepare('INSERT INTO approvals (id, user_id, status) VALUES (?, ?, ?)').bind(event.instanceId, event.params.userId, 'pending').run()); try { - const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { type: 'approval-response', timeout: '48h' }); + const approval = await step.waitForEvent<{ approved: boolean }>('wait for approval', { event: 'approval-response', timeout: '48h' }); if (approval.approved) { await step.do('process approval', async () => {}); } else { await step.do('handle rejection', async () => {}); } } catch (e) { @@ -154,13 +154,10 @@ await step.do('other work', async () => console.log(`Child started: ${child.id}` ### Race Pattern ```typescript -// Wrap Promise.race in a step.do to ensure deterministic caching behavior -const winner = await step.do('race step', async () => { - return await Promise.race([ - step.do('option A', async () => slowOperation()), - step.do('option B', async () => fastOperation()) - ]); -}); +const winner = await Promise.race([ + step.do('option A', async () => slowOperation()), + step.do('option B', async () => fastOperation()) +]); ``` ### Scheduled Workflow Chain diff --git a/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx b/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx index 2d9d1a6b085..98983021e18 100644 --- a/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx +++ b/src/content/changelog/workflows/2025-04-07-workflows-ga.mdx @@ -34,7 +34,7 @@ import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:work export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // Other steps in your Workflow - let event = await step.waitForEvent("receive invoice paid webhook from Stripe", { type: "stripe-webhook", timeout: "1 hour" }) + let stripeEvent = await step.waitForEvent("receive invoice paid webhook from Stripe", { type: "stripe-webhook", timeout: "1 hour" }) // Rest of your Workflow } }