cloudflare-workflows by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-workflows状态:生产就绪 ✅(自 2025 年 4 月起 GA) 最后更新:2026-01-09 依赖项:cloudflare-worker-base(用于 Worker 设置) 最新版本:wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0
近期更新(2025 年):
# 1. 创建项目脚手架
npm create cloudflare@latest my-workflow -- --template cloudflare/workflows-starter --git --deploy false
cd my-workflow
# 2. 配置 wrangler.jsonc
{
"name": "my-workflow",
"main": "src/index.ts",
"compatibility_date": "2025-11-25",
"workflows": [{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}]
}
# 3. 创建工作流 (src/index.ts)
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const result = await step.do('process', async () => { /* work */ });
await step.sleep('wait', '1 hour');
await step.do('continue', async () => { /* more work */ });
}
}
# 4. 部署和测试
npm run deploy
npx wrangler workflows instances list my-workflow
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键点:继承 WorkflowEntrypoint,使用 step 方法实现 run(),在 wrangler.jsonc 中配置绑定
此技能可预防 Cloudflare Workflows 中 12 个已记录的错误。
错误:在 waitForEvent() 超时后发送的事件在后续的 waitForEvent() 调用中被忽略 环境:仅限本地开发(wrangler dev)- 在生产环境中正常工作 来源:GitHub Issue #11740
原因:miniflare 中的一个 bug,已在生产环境中修复(2025 年 5 月),但未移植到本地模拟器。超时后,该实例的事件队列会损坏。
预防措施:
waitForEvent()错误示例:
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
for (let i = 0; i < 3; i++) {
try {
const evt = await step.waitForEvent(`wait-${i}`, {
type: 'user-action',
timeout: '5 seconds'
});
console.log(`Iteration ${i}: Received event`);
} catch {
console.log(`Iteration ${i}: Timeout`);
}
}
}
}
// 在 wrangler dev 中:
// - 迭代 1:✅ 接收到事件
// - 迭代 2:⏱️ 超时(预期)
// - 迭代 3:❌ 未接收到事件(BUG - 事件已发送但被忽略)
状态:已知 bug,等待 miniflare 修复。
错误:MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start 消息:Worker 的绑定引用了具有命名入口点的服务,但该服务没有此入口点 来源:GitHub Issue #9402
原因:来自 wrangler 包的 getPlatformProxy() 不支持 Workflow 绑定(类似于其处理 Durable Objects 的方式)。这会阻碍 Next.js 集成和本地 CLI 脚本。
预防措施:
选项 1:在使用 getPlatformProxy() 时注释掉 workflow 绑定
选项 2:为 CLI 脚本创建单独的、不包含 workflows 的 wrangler.cli.jsonc
选项 3:直接通过已部署的 worker 访问 workflow 绑定,而非通过代理
// 变通方案:为 CLI 脚本使用单独的配置 // wrangler.cli.jsonc(无 workflows) { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-20" // workflows 被注释掉 }
// 在脚本中使用: import { getPlatformProxy } from 'wrangler'; const { env } = await getPlatformProxy({ configPath: './wrangler.cli.jsonc' });
状态:已知限制,计划修复(类似于 DOs 的方式过滤 workflows)。
错误:返回了实例 ID,但查询时显示 instance.not_found 环境:仅限本地开发(wrangler dev)- 在生产环境中正常工作 来源:GitHub Issue #10806
原因:在 workflow.create() 之后立即返回重定向会导致请求在 workflow 初始化完成之前“软中止”(开发环境中的单线程执行)。
预防措施:使用 ctx.waitUntil() 确保 workflow 初始化在重定向前完成:
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const workflow = await env.MY_WORKFLOW.create({ params: { userId: '123' } });
// ✅ 确保 workflow 初始化完成
ctx.waitUntil(workflow.status());
return Response.redirect('/dashboard', 302);
}
};
状态:在最近的 wrangler 版本中已修复(2025 年 9 月后),但仍建议使用变通方案以确保兼容性。
错误:[vitest-worker]: Timeout calling "resolveId" 环境:CI/CD 流水线(GitLab、GitHub Actions)- 本地工作正常 来源:GitHub Issue #10600
原因:@cloudflare/vitest-pool-workers 在 CI 容器中存在资源限制问题,对 workflow 测试的影响大于其他 worker 类型。
预防措施:
在 vitest 配置中增加 testTimeout:
export default defineWorkersConfig({ test: { testTimeout: 60_000 // 默认值:5000ms } });
检查 CI 资源限制(CPU/内存)
如果不测试存储隔离,请使用 isolatedStorage: false
对于关键 workflows,考虑测试已部署的实例而非使用 vitest
状态:已知问题,正在调查(内部:WOR-945)。
错误:调用 instance.restart() 或 instance.terminate() 时出现 Error: Not implemented yet 环境:仅限本地开发(wrangler dev)- 在生产环境中工作 来源:GitHub Issue #11312
原因:实例管理 API 尚未在 miniflare 中实现。此外,即使 workflow 处于休眠状态,实例状态仍显示为 running。
预防措施:在生产或预发布环境中测试实例生命周期管理(暂停/恢复/终止),直到本地开发支持添加为止。
const instance = await env.MY_WORKFLOW.get(instanceId);
// ❌ 在 wrangler dev 中失败
await instance.restart(); // 错误:尚未实现
await instance.terminate(); // 错误:尚未实现
// ✅ 在生产环境中工作
状态:已知限制,本地开发支持暂无时间表。
错误:"Cannot perform I/O on behalf of a different request" 来源:Cloudflare 运行时行为
原因:尝试在一个请求上下文中使用来自另一个请求处理程序的 I/O 对象。
预防措施:始终在 step.do() 回调内部执行 I/O 操作:
// ❌ 错误 - I/O 在 step 外部
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await step.do('use data', async () => {
return data; // 这将失败!
});
// ✅ 正确 - I/O 在 step 内部
const data = await step.do('fetch data', async () => {
const response = await fetch('https://api.example.com/data');
return await response.json();
});
错误:带有空消息的 NonRetryableError 在开发模式下会导致重试,但在生产环境中工作正常 环境:开发环境特定的 bug 来源:GitHub Issue #10113
原因:空错误消息在 miniflare 和生产运行时之间的处理方式不同。
预防措施:始终为 NonRetryableError 提供消息:
// ❌ 在开发环境中重试,在生产环境中退出
throw new NonRetryableError('');
// ✅ 在两个环境中都退出
throw new NonRetryableError('Validation failed');
状态:已知问题,已记录变通方案。
错误:在 step.do() 外部声明的变量在休眠/休眠后重置为初始值 来源:Cloudflare Workflows 规则
原因:当引擎检测到没有待处理的工作时,Workflows 会休眠。所有内存中的状态在休眠期间都会丢失。
预防措施:仅使用从 step.do() 返回的状态 - 其他所有内容都是临时的:
// ❌ 错误 - 内存变量在休眠时丢失
let counter = 0;
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
counter = await step.do('increment', async () => counter + 1);
await step.sleep('wait', '1 hour'); // ← 在此处休眠,内存状态丢失
console.log(counter); // ❌ 将是 0,而不是 1!
}
}
// ✅ 正确 - 来自 step.do() 返回值的状态会持久化
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const counter = await step.do('increment', async () => 1);
await step.sleep('wait', '1 hour');
console.log(counter); // ✅ 仍然是 1
}
}
错误:步骤不必要地重新运行,性能下降 来源:Cloudflare Workflows 规则
原因:步骤名称充当缓存键。使用 Date.now()、Math.random() 或其他非确定性值会导致每次运行都生成新的缓存键。
预防措施:使用静态的、确定性的步骤名称:
// ❌ 错误 - 非确定性步骤名称
await step.do(`fetch-data-${Date.now()}`, async () => {
return await fetchExpensiveData();
});
// 每次执行都创建新的缓存键 → 步骤总是重新运行
// ✅ 正确 - 确定性步骤名称
await step.do('fetch-data', async () => {
return await fetchExpensiveData();
});
// 相同的缓存键 → 在重启/重试时重用结果
错误:不同的 Promise 在重启时解析,行为不一致 来源:Cloudflare Workflows 规则
原因:步骤外部的非确定性操作在重启时会再次运行,可能导致不同的结果。
预防措施:将所有非确定性逻辑保持在 step.do() 内部:
// ❌ 错误 - 在 step 外部进行竞争
const fastest = await Promise.race([fetchA(), fetchB()]);
await step.do('use result', async () => fastest);
// 重启时:竞争再次运行,不同的 Promise 可能获胜
// ✅ 正确 - 在 step 内部进行竞争
const fastest = await step.do('fetch fastest', async () => {
return await Promise.race([fetchA(), fetchB()]);
});
// 重启时:使用缓存结果,行为一致
错误:workflow 重启后出现重复的日志、指标或操作 来源:Cloudflare Workflows 规则
原因:如果 workflow 在执行过程中重启,step.do() 外部的代码会执行多次。
预防措施:将日志记录、指标和其他副作用放在 step.do() 内部:
// ❌ 错误 - 副作用在 step 外部
console.log('Workflow started'); // ← 重启时记录多次
await step.do('work', async () => { /* work */ });
// ✅ 正确 - 副作用在 step 内部
await step.do('log start', async () => {
console.log('Workflow started'); // ← 记录一次(已缓存)
});
错误:步骤超时后出现重复扣款、重复数据库写入 来源:Cloudflare Workflows 规则
原因:步骤会单独重试。如果 API 调用成功但步骤在返回之前超时,重试将再次调用 API。
预防措施:使用存在性检查来保护非幂等操作:
// ❌ 错误 - 未检查即扣款
await step.do('charge', async () => {
return await stripe.charges.create({ amount: 1000, customer: customerId });
});
// 如果步骤在扣款成功后超时,重试会再次扣款!
// ✅ 正确 - 首先检查是否存在扣款记录
await step.do('charge', async () => {
const existing = await stripe.charges.list({ customer: customerId, limit: 1 });
if (existing.data.length > 0) return existing.data[0]; // 幂等
return await stripe.charges.create({ amount: 1000, customer: customerId });
});
step.do<T>(name: string, config?: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>
参数:
name - 步骤名称(用于可观测性)config(可选)- 重试配置(重试次数、超时、退避)callback - 执行工作的异步函数返回: 回调函数返回的值(必须是可序列化的)
示例:
const result = await step.do('call API', { retries: { limit: 10, delay: '10s', backoff: 'exponential' }, timeout: '5 min' }, async () => {
return await fetch('https://api.example.com/data').then(r => r.json());
});
关键点 - 序列化:
step.sleep(name: string, duration: WorkflowDuration): Promise<void>
参数:
name - 步骤名称duration - 数字(毫秒)或字符串:"second"、"minute"、"hour"、"day"、"week"、"month"、"year"(接受复数形式)示例:
await step.sleep('wait 5 minutes', '5 minutes');
await step.sleep('wait 1 hour', '1 hour');
await step.sleep('wait 2 days', '2 days');
await step.sleep('wait 30 seconds', 30000); // 毫秒
注意: 恢复 workflows 的优先级高于新实例。休眠不计入步骤限制。
step.sleepUntil(name: string, timestamp: Date | number): Promise<void>
参数:
name - 步骤名称timestamp - Date 对象或 UNIX 时间戳(毫秒)示例:
await step.sleepUntil('wait for launch', new Date('2025-12-25T00:00:00Z'));
await step.sleepUntil('wait until time', Date.parse('24 Oct 2024 13:00:00 UTC'));
step.waitForEvent<T>(name: string, options: { type: string; timeout?: string | number }): Promise<T>
参数:
name - 步骤名称options.type - 要匹配的事件类型options.timeout(可选)- 最大等待时间(默认:24 小时,最大:30 天)返回: 通过 instance.sendEvent() 发送的事件负载
示例:
export class PaymentWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('create payment', async () => { /* Stripe API */ });
const webhookData = await step.waitForEvent<StripeWebhook>(
'wait for payment confirmation',
{ type: 'stripe-webhook', timeout: '1 hour' }
);
if (webhookData.status === 'succeeded') {
await step.do('fulfill order', async () => { /* fulfill */ });
}
}
}
// Worker 向 workflow 发送事件
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.url.includes('/webhook/stripe')) {
const instance = await env.PAYMENT_WORKFLOW.get(instanceId);
await instance.sendEvent({ type: 'stripe-webhook', payload: await req.json() });
return new Response('OK');
}
}
};
超时处理:
try {
const event = await step.waitForEvent('wait for user', { type: 'user-submitted', timeout: '10 minutes' });
} catch (error) {
await step.do('send reminder', async () => { /* reminder */ });
}
interface WorkflowStepConfig {
retries?: {
limit: number; // 最大尝试次数(允许 Infinity)
delay: string | number; // 重试之间的延迟
backoff?: 'constant' | 'linear' | 'exponential';
};
timeout?: string | number; // 每次尝试的最长时间
}
默认值: { retries: { limit: 5, delay: 10000, backoff: 'exponential' }, timeout: '10 minutes' }
退避示例:
// 常量:30s, 30s, 30s
{ retries: { limit: 3, delay: '30 seconds', backoff: 'constant' } }
// 线性:1m, 2m, 3m, 4m, 5m
{ retries: { limit: 5, delay: '1 minute', backoff: 'linear' } }
// 指数(推荐):10s, 20s, 40s, 80s, 160s
{ retries: { limit: 10, delay: '10 seconds', backoff: 'exponential' }, timeout: '5 minutes' }
// 无限重试
{ retries: { limit: Infinity, delay: '1 minute', backoff: 'exponential' } }
// 无重试
{ retries: { limit: 0 } }
强制 workflow 立即失败而不重试:
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('validate input', async () => {
if (!event.payload.userId) {
throw new NonRetryableError('userId is required');
}
// 验证用户是否存在
const user = await this.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(event.payload.userId).first();
if (!user) {
// 终止性错误 - 重试无济于事
throw new NonRetryableError('User not found');
}
return user;
});
}
}
何时使用 NonRetryableError:
通过捕获可选步骤错误来防止 workflow 失败:
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('process payment', async () => { /* critical */ });
try {
await step.do('send email', async () => { /* optional */ });
} catch (error) {
await step.do('log failure', async () => {
await this.env.DB.prepare('INSERT INTO failed_emails VALUES (?, ?)').bind(event.payload.userId, error.message).run();
});
}
await step.do('update status', async () => { /* continues */ });
}
}
优雅降级:
let result;
try {
result = await step.do('call primary API', async () => await callPrimaryAPI());
} catch {
result = await step.do('call backup API', async () => await callBackupAPI());
}
配置绑定(wrangler.jsonc):
{
"workflows": [{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow",
"script_name": "workflow-worker" // 如果 workflow 在不同的 Worker 中
}]
}
从 Worker 触发:
const instance = await env.MY_WORKFLOW.create({ params: { userId: '123' } });
return Response.json({ id: instance.id, status: await instance.status() });
实例管理:
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status(); // { status: 'running'|'complete'|'errored'|'queued', error, output }
await instance.sendEvent({ type: 'user-action', payload: { action: 'approved' } });
await instance.pause();
await instance.resume();
await instance.terminate();
Workflows 自动持久化从 step.do() 返回的状态:
✅ 可序列化:
string、number、boolean、null❌ 不可序列化:
示例:
// ✅ 正确
const result = await step.do('fetch data', async () => ({
users: [{ id: 1, name: 'Alice' }],
timestamp: Date.now(),
metadata: null
}));
// ❌ 错误 - 函数不可序列化
const bad = await step.do('bad', async () => ({ data: [1, 2, 3], transform: (x) => x * 2 })); // 抛出错误!
跨步骤访问状态:
const userData = await step.do('fetch user', async () => ({ id: 123, email: 'user@example.com' }));
const orderData = await step.do('create order', async () => ({ userId: userData.id, orderId: 'ORD-456' }));
await step.do('send email', async () => sendEmail({ to: userData.email, subject: `Order ${orderData.orderId}` }));
Workflows 自动跟踪:
通过 Cloudflare 仪表板访问:
指标包括:
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status();
console.log(status);
// {
// status: 'complete' | 'running' | 'errored' | 'queued' | 'waiting' | 'unknown',
// error: string | null,
// output: { userId: '123', status: 'processed' }
// }
CPU 时间配置(2025 年):
// wrangler.jsonc
{ "limits": { "cpu_ms": 300000 } } // 最长 5 分钟(默认:30 秒)
| 功能 | Workers 免费版 | Workers 付费版 |
|---|---|---|
| 每个 workflow 的最大步骤数 | 1,024 | 1,024 |
| 每个步骤的最大状态大小 | 1 MiB | 1 MiB |
| 每个实例的最大状态大小 | 100 MB | 1 GB |
| 最大事件负载大小 | 1 MiB | 1 MiB |
| 最大 sleep/sleepUntil 持续时间 | 365 天 | 365 天 |
| 最大 waitForEvent 超时时间 | 365 天 | 365 天 |
| 每个步骤的 CPU 时间 | 10 ms | 30 秒(默认),5 分钟(最大) |
| 每个步骤的持续时间(挂钟时间) | 无限制 | 无限制 |
| 最大 workflow 执行次数 | 100,000/天 | 无限制 |
| 并发实例数 | 25 | 10,000(2025 年 10 月,从 4,500 提升) |
| 实例创建速率 | 100/秒 | 100/秒(2025 年 10 月,快 10 倍) |
| 最大排队实例数 | 100,000 | 1,000,000 |
| 每个实例的最大子请求数 | 50/请求 | 1,000/请求 |
| 保留期(完成状态) | 3 天 | 30 天 |
| 最大 Workflow 名称长度 | 64 字符 | 64 字符 |
| 最大实例 ID 长度 | 100 字符 | 100 字符 |
关键注意事项:
step.sleep() 和 step.sleepUntil() 不计入 1,024 步限制wrangler.jsonc 配置 CPU 时间:{ "limits": { "cpu_ms": 300000 } }(最长 5 分钟)需要 Workers 付费计划($5/月)
Workflow 执行:
计为一步执行的情况:
step.do() 调用step.sleep()、step.sleepUntil()、step.waitForEvent() 不计入成本示例:
Workflows 通过 cloudflare:test 模块支持完整的测试集成。
npm install -D vitest@latest @cloudflare/vitest-pool-workers@latest
vitest.config.ts:
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({ test: { poolOptions: { workers: { miniflare: { bindings: { MY_WORKFLOW: { scriptName: 'workflow' } } } } } } });
import { env, introspectWorkflowInstance } from 'cloudflare:test';
it('should complete workflow', async () => {
const instance = await introspectWorkflowInstance(env.MY_WORKFLOW, 'test-123');
try {
await instance.modify(async (m) => {
await m.disableSleeps(); // 跳过所有休眠
await m.mockStepResult({ name: 'fetch data' }, { users: [{ id: 1 }] }); // 模拟步骤结果
await m.mockEvent({ type: 'approval', payload: { approved: true } }); // 发送模拟事件
await m.mockStepError({ name: 'call API' }, new Error('Network timeout'), 1); // 强制错误一次
});
await env.MY_WORKFLOW.create({ id: 'test-123' });
await expect(instance.waitForStatus('complete')).resolves.not.toThrow();
} finally {
await instance.dispose(); // 清理
}
});
disableSleeps(steps?) - 立即跳过休眠mockStepResult(step, result) - 模拟 step.do() 结果mockStepError(step, error, times?) - 强制 step.do() 抛出错误mockEvent(event) - 向 step.waitForEvent() 发送模拟事件forceStepTimeout(step, times?) - 强制 step.do() 超时forceEventTimeout(step) - 强制 step.waitForEvent() 超时mcp__cloudflare-docs__search_cloudflare_documentation 获取最新文档最后更新:2026-01-21 版本:2.0.
Status : Production Ready ✅ (GA since April 2025) Last Updated : 2026-01-09 Dependencies : cloudflare-worker-base (for Worker setup) Latest Versions : wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0
Recent Updates (2025) :
# 1. Scaffold project
npm create cloudflare@latest my-workflow -- --template cloudflare/workflows-starter --git --deploy false
cd my-workflow
# 2. Configure wrangler.jsonc
{
"name": "my-workflow",
"main": "src/index.ts",
"compatibility_date": "2025-11-25",
"workflows": [{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}]
}
# 3. Create workflow (src/index.ts)
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const result = await step.do('process', async () => { /* work */ });
await step.sleep('wait', '1 hour');
await step.do('continue', async () => { /* more work */ });
}
}
# 4. Deploy and test
npm run deploy
npx wrangler workflows instances list my-workflow
CRITICAL : Extends WorkflowEntrypoint, implements run() with step methods, bindings in wrangler.jsonc
This skill prevents 12 documented errors with Cloudflare Workflows.
Error : Events sent after a waitForEvent() timeout are ignored in subsequent waitForEvent() calls Environment : Local development (wrangler dev) only - works correctly in production Source : GitHub Issue #11740
Why It Happens : Bug in miniflare that was fixed in production (May 2025) but not ported to local emulator. After a timeout, the event queue becomes corrupted for that instance.
Prevention :
waitForEvent() calls where timeouts are expectedExample of Bug :
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
for (let i = 0; i < 3; i++) {
try {
const evt = await step.waitForEvent(`wait-${i}`, {
type: 'user-action',
timeout: '5 seconds'
});
console.log(`Iteration ${i}: Received event`);
} catch {
console.log(`Iteration ${i}: Timeout`);
}
}
}
}
// In wrangler dev:
// - Iteration 1: ✅ receives event
// - Iteration 2: ⏱️ times out (expected)
// - Iteration 3: ❌ does not receive event (BUG - event is sent but ignored)
Status : Known bug, fix pending for miniflare.
Error : MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start Message : Worker's binding refers to service with named entrypoint, but service has no such entrypoint Source : GitHub Issue #9402
Why It Happens : getPlatformProxy() from wrangler package doesn't support Workflow bindings (similar to how it handles Durable Objects). This blocks Next.js integration and local CLI scripts.
Prevention :
Option 1 : Comment out workflow bindings when using getPlatformProxy()
Option 2 : Create separate wrangler.cli.jsonc without workflows for CLI scripts
Option 3 : Access workflow bindings directly via deployed worker, not proxy
// Workaround: Separate config for CLI scripts // wrangler.cli.jsonc (no workflows) { "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-01-20" // workflows commented out }
// Use in script: import { getPlatformProxy } from 'wrangler'; const { env } = await getPlatformProxy({ configPath: './wrangler.cli.jsonc' });
Status : Known limitation, fix planned (filter workflows similar to DOs).
Error : Instance ID returned but instance.not_found when queried Environment : Local development (wrangler dev) only - works correctly in production Source : GitHub Issue #10806
Why It Happens : Returning a redirect immediately after workflow.create() causes request to "soft abort" before workflow initialization completes (single-threaded execution in dev).
Prevention : Use ctx.waitUntil() to ensure workflow initialization completes before redirect:
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const workflow = await env.MY_WORKFLOW.create({ params: { userId: '123' } });
// ✅ Ensure workflow initialization completes
ctx.waitUntil(workflow.status());
return Response.redirect('/dashboard', 302);
}
};
Status : Fixed in recent wrangler versions (post-Sept 2025), but workaround still recommended for compatibility.
Error : [vitest-worker]: Timeout calling "resolveId" Environment : CI/CD pipelines (GitLab, GitHub Actions) - works locally Source : GitHub Issue #10600
Why It Happens : @cloudflare/vitest-pool-workers has resource constraint issues in CI containers, affecting workflow tests more than other worker types.
Prevention :
Increase testTimeout in vitest config:
export default defineWorkersConfig({ test: { testTimeout: 60_000 // Default: 5000ms } });
Check CI resource limits (CPU/memory)
Use isolatedStorage: false if not testing storage isolation
Consider testing against deployed instances instead of vitest for critical workflows
Status : Known issue, investigating (Internal: WOR-945).
Error : Error: Not implemented yet when calling instance.restart() or instance.terminate() Environment : Local development (wrangler dev) only - works in production Source : GitHub Issue #11312
Why It Happens : Instance management APIs not yet implemented in miniflare. Additionally, instance status shows running even when workflow is sleeping.
Prevention : Test instance lifecycle management (pause/resume/terminate) in production or staging environment until local dev support is added.
const instance = await env.MY_WORKFLOW.get(instanceId);
// ❌ Fails in wrangler dev
await instance.restart(); // Error: Not implemented yet
await instance.terminate(); // Error: Not implemented yet
// ✅ Works in production
Status : Known limitation, no timeline for local dev support.
Error : "Cannot perform I/O on behalf of a different request" Source : Cloudflare runtime behavior
Why It Happens : Trying to use I/O objects created in one request context from another request handler.
Prevention : Always perform I/O within step.do() callbacks:
// ❌ Bad - I/O outside step
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await step.do('use data', async () => {
return data; // This will fail!
});
// ✅ Good - I/O inside step
const data = await step.do('fetch data', async () => {
const response = await fetch('https://api.example.com/data');
return await response.json();
});
Error : NonRetryableError with empty message causes retries in dev mode but works correctly in production Environment : Development-specific bug Source : GitHub Issue #10113
Why It Happens : Empty error messages are handled differently between miniflare and production runtime.
Prevention : Always provide a message to NonRetryableError:
// ❌ Retries in dev, exits in prod
throw new NonRetryableError('');
// ✅ Exits in both environments
throw new NonRetryableError('Validation failed');
Status : Known issue, workaround documented.
Error : Variables declared outside step.do() reset to initial values after sleep/hibernation Source : Cloudflare Workflows Rules
Why It Happens : Workflows hibernate when the engine detects no pending work. All in-memory state is lost during hibernation.
Prevention : Only use state returned from step.do() - everything else is ephemeral:
// ❌ BAD - In-memory variable lost on hibernation
let counter = 0;
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
counter = await step.do('increment', async () => counter + 1);
await step.sleep('wait', '1 hour'); // ← Hibernates here, in-memory state lost
console.log(counter); // ❌ Will be 0, not 1!
}
}
// ✅ GOOD - State from step.do() return values persists
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const counter = await step.do('increment', async () => 1);
await step.sleep('wait', '1 hour');
console.log(counter); // ✅ Still 1
}
}
Error : Steps re-run unnecessarily, performance degradation Source : Cloudflare Workflows Rules
Why It Happens : Step names act as cache keys. Using Date.now(), Math.random(), or other non-deterministic values causes new cache keys every run.
Prevention : Use static, deterministic step names:
// ❌ BAD - Non-deterministic step name
await step.do(`fetch-data-${Date.now()}`, async () => {
return await fetchExpensiveData();
});
// Every execution creates new cache key → step always re-runs
// ✅ GOOD - Deterministic step name
await step.do('fetch-data', async () => {
return await fetchExpensiveData();
});
// Same cache key → result reused on restart/retry
Error : Different promises resolve on restart, inconsistent behavior Source : Cloudflare Workflows Rules
Why It Happens : Non-deterministic operations outside steps run again on restart, potentially with different results.
Prevention : Keep all non-deterministic logic inside step.do():
// ❌ BAD - Race outside step
const fastest = await Promise.race([fetchA(), fetchB()]);
await step.do('use result', async () => fastest);
// On restart: race runs again, different promise might win
// ✅ GOOD - Race inside step
const fastest = await step.do('fetch fastest', async () => {
return await Promise.race([fetchA(), fetchB()]);
});
// On restart: cached result used, consistent behavior
Error : Duplicate logs, metrics, or operations after workflow restart Source : Cloudflare Workflows Rules
Why It Happens : Code outside step.do() executes multiple times if the workflow restarts mid-execution.
Prevention : Put logging, metrics, and other side effects inside step.do():
// ❌ BAD - Side effect outside step
console.log('Workflow started'); // ← Logs multiple times on restart
await step.do('work', async () => { /* work */ });
// ✅ GOOD - Side effects inside step
await step.do('log start', async () => {
console.log('Workflow started'); // ← Logs once (cached)
});
Error : Double charges, duplicate database writes after step timeout Source : Cloudflare Workflows Rules
Why It Happens : Steps retry individually. If an API call succeeds but the step times out before returning, the retry will call the API again.
Prevention : Guard non-idempotent operations with existence checks:
// ❌ BAD - Charge customer without check
await step.do('charge', async () => {
return await stripe.charges.create({ amount: 1000, customer: customerId });
});
// If step times out after charge succeeds, retry charges AGAIN!
// ✅ GOOD - Check for existing charge first
await step.do('charge', async () => {
const existing = await stripe.charges.list({ customer: customerId, limit: 1 });
if (existing.data.length > 0) return existing.data[0]; // Idempotent
return await stripe.charges.create({ amount: 1000, customer: customerId });
});
step.do<T>(name: string, config?: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>
Parameters:
name - Step name (for observability)config (optional) - Retry configuration (retries, timeout, backoff)callback - Async function that does the workReturns: Value from callback (must be serializable)
Example:
const result = await step.do('call API', { retries: { limit: 10, delay: '10s', backoff: 'exponential' }, timeout: '5 min' }, async () => {
return await fetch('https://api.example.com/data').then(r => r.json());
});
CRITICAL - Serialization:
step.sleep(name: string, duration: WorkflowDuration): Promise<void>
Parameters:
name - Step nameduration - Number (ms) or string: "second", "minute", "hour", "day", "week", "month", "year" (plural forms accepted)Examples:
await step.sleep('wait 5 minutes', '5 minutes');
await step.sleep('wait 1 hour', '1 hour');
await step.sleep('wait 2 days', '2 days');
await step.sleep('wait 30 seconds', 30000); // milliseconds
Note: Resuming workflows take priority over new instances. Sleeps don't count toward step limits.
step.sleepUntil(name: string, timestamp: Date | number): Promise<void>
Parameters:
name - Step nametimestamp - Date object or UNIX timestamp (milliseconds)Examples:
await step.sleepUntil('wait for launch', new Date('2025-12-25T00:00:00Z'));
await step.sleepUntil('wait until time', Date.parse('24 Oct 2024 13:00:00 UTC'));
step.waitForEvent<T>(name: string, options: { type: string; timeout?: string | number }): Promise<T>
Parameters:
name - Step nameoptions.type - Event type to matchoptions.timeout (optional) - Max wait time (default: 24 hours, max: 30 days)Returns: Event payload sent via instance.sendEvent()
Example:
export class PaymentWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('create payment', async () => { /* Stripe API */ });
const webhookData = await step.waitForEvent<StripeWebhook>(
'wait for payment confirmation',
{ type: 'stripe-webhook', timeout: '1 hour' }
);
if (webhookData.status === 'succeeded') {
await step.do('fulfill order', async () => { /* fulfill */ });
}
}
}
// Worker sends event to workflow
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.url.includes('/webhook/stripe')) {
const instance = await env.PAYMENT_WORKFLOW.get(instanceId);
await instance.sendEvent({ type: 'stripe-webhook', payload: await req.json() });
return new Response('OK');
}
}
};
Timeout handling:
try {
const event = await step.waitForEvent('wait for user', { type: 'user-submitted', timeout: '10 minutes' });
} catch (error) {
await step.do('send reminder', async () => { /* reminder */ });
}
interface WorkflowStepConfig {
retries?: {
limit: number; // Max attempts (Infinity allowed)
delay: string | number; // Delay between retries
backoff?: 'constant' | 'linear' | 'exponential';
};
timeout?: string | number; // Max time per attempt
}
Default: { retries: { limit: 5, delay: 10000, backoff: 'exponential' }, timeout: '10 minutes' }
Backoff Examples:
// Constant: 30s, 30s, 30s
{ retries: { limit: 3, delay: '30 seconds', backoff: 'constant' } }
// Linear: 1m, 2m, 3m, 4m, 5m
{ retries: { limit: 5, delay: '1 minute', backoff: 'linear' } }
// Exponential (recommended): 10s, 20s, 40s, 80s, 160s
{ retries: { limit: 10, delay: '10 seconds', backoff: 'exponential' }, timeout: '5 minutes' }
// Unlimited retries
{ retries: { limit: Infinity, delay: '1 minute', backoff: 'exponential' } }
// No retries
{ retries: { limit: 0 } }
Force workflow to fail immediately without retrying:
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('validate input', async () => {
if (!event.payload.userId) {
throw new NonRetryableError('userId is required');
}
// Validate user exists
const user = await this.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(event.payload.userId).first();
if (!user) {
// Terminal error - retrying won't help
throw new NonRetryableError('User not found');
}
return user;
});
}
}
When to use NonRetryableError:
Prevent workflow failure by catching optional step errors:
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
await step.do('process payment', async () => { /* critical */ });
try {
await step.do('send email', async () => { /* optional */ });
} catch (error) {
await step.do('log failure', async () => {
await this.env.DB.prepare('INSERT INTO failed_emails VALUES (?, ?)').bind(event.payload.userId, error.message).run();
});
}
await step.do('update status', async () => { /* continues */ });
}
}
Graceful Degradation:
let result;
try {
result = await step.do('call primary API', async () => await callPrimaryAPI());
} catch {
result = await step.do('call backup API', async () => await callBackupAPI());
}
Configure binding (wrangler.jsonc):
{
"workflows": [{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow",
"script_name": "workflow-worker" // If workflow in different Worker
}]
}
Trigger from Worker:
const instance = await env.MY_WORKFLOW.create({ params: { userId: '123' } });
return Response.json({ id: instance.id, status: await instance.status() });
Instance Management:
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status(); // { status: 'running'|'complete'|'errored'|'queued', error, output }
await instance.sendEvent({ type: 'user-action', payload: { action: 'approved' } });
await instance.pause();
await instance.resume();
await instance.terminate();
Workflows automatically persist state returned from step.do():
✅ Serializable:
string, number, boolean, null❌ Non-Serializable:
Example:
// ✅ Good
const result = await step.do('fetch data', async () => ({
users: [{ id: 1, name: 'Alice' }],
timestamp: Date.now(),
metadata: null
}));
// ❌ Bad - function not serializable
const bad = await step.do('bad', async () => ({ data: [1, 2, 3], transform: (x) => x * 2 })); // Throws error!
Access State Across Steps:
const userData = await step.do('fetch user', async () => ({ id: 123, email: 'user@example.com' }));
const orderData = await step.do('create order', async () => ({ userId: userData.id, orderId: 'ORD-456' }));
await step.do('send email', async () => sendEmail({ to: userData.email, subject: `Order ${orderData.orderId}` }));
Workflows automatically track:
Access via Cloudflare dashboard:
Metrics include:
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status();
console.log(status);
// {
// status: 'complete' | 'running' | 'errored' | 'queued' | 'waiting' | 'unknown',
// error: string | null,
// output: { userId: '123', status: 'processed' }
// }
CPU Time Configuration (2025):
// wrangler.jsonc
{ "limits": { "cpu_ms": 300000 } } // 5 minutes max (default: 30 seconds)
| Feature | Workers Free | Workers Paid |
|---|---|---|
| Max steps per workflow | 1,024 | 1,024 |
| Max state per step | 1 MiB | 1 MiB |
| Max state per instance | 100 MB | 1 GB |
| Max event payload size | 1 MiB | 1 MiB |
| Max sleep/sleepUntil duration | 365 days | 365 days |
| Max waitForEvent timeout | 365 days | 365 days |
| CPU time per step | 10 ms | 30 sec (default), 5 min (max) |
| Duration (wall clock) per step | Unlimited |
CRITICAL Notes:
step.sleep() and step.sleepUntil() do NOT count toward 1,024 step limitwrangler.jsonc: { "limits": { "cpu_ms": 300000 } } (5 min max)Requires Workers Paid plan ($5/month)
Workflow Executions:
What counts as a step execution:
step.do() callstep.sleep(), step.sleepUntil(), step.waitForEvent() do NOT countCost examples:
Workflows support full testing integration via cloudflare:test module.
npm install -D vitest@latest @cloudflare/vitest-pool-workers@latest
vitest.config.ts:
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({ test: { poolOptions: { workers: { miniflare: { bindings: { MY_WORKFLOW: { scriptName: 'workflow' } } } } } } });
import { env, introspectWorkflowInstance } from 'cloudflare:test';
it('should complete workflow', async () => {
const instance = await introspectWorkflowInstance(env.MY_WORKFLOW, 'test-123');
try {
await instance.modify(async (m) => {
await m.disableSleeps(); // Skip all sleeps
await m.mockStepResult({ name: 'fetch data' }, { users: [{ id: 1 }] }); // Mock step result
await m.mockEvent({ type: 'approval', payload: { approved: true } }); // Send mock event
await m.mockStepError({ name: 'call API' }, new Error('Network timeout'), 1); // Force error once
});
await env.MY_WORKFLOW.create({ id: 'test-123' });
await expect(instance.waitForStatus('complete')).resolves.not.toThrow();
} finally {
await instance.dispose(); // Cleanup
}
});
disableSleeps(steps?) - Skip sleeps instantlymockStepResult(step, result) - Mock step.do() resultmockStepError(step, error, times?) - Force step.do() to throwmockEvent(event) - Send mock event to step.waitForEvent()forceStepTimeout(step, times?) - Force step.do() timeoutforceEventTimeout(step) - Force step.waitForEvent() timeoutOfficial Docs : https://developers.cloudflare.com/workers/testing/vitest-integration/
Last Updated : 2026-01-21 Version : 2.0.0 Changes : Added 12 documented Known Issues (TIER 1-2 research findings): waitForEvent timeout bug, getPlatformProxy failure, redirect instance loss, Vitest CI issues, local dev limitations, state persistence rules, caching gotchas, and idempotency patterns Maintainer : Jeremy Dawes | jeremy@jezweb.net
Weekly Installs
341
Repository
GitHub Stars
643
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
claude-code279
opencode231
gemini-cli231
cursor209
codex205
antigravity203
移动应用设计开发指南:移动优先、触控优先设计系统与性能优化最佳实践
1,600 周安装
Next.js DCE-Edge 死代码消除与边缘运行时优化指南 | Vercel
277 周安装
前端设计技能:生成独特UI、审核CSS、提取设计令牌、检查无障碍性,告别AI垃圾美学
1,600 周安装
主动自我改进智能体:AI智能体自动化经验学习与安全进化框架
1,600 周安装
精英PowerPoint设计师 | 2024-2025演示设计趋势,打造苹果/谷歌风格专业幻灯片
1,600 周安装
Convex HTTP Actions 教程:构建Webhook、API集成与自定义路由端点
1,600 周安装
| Unlimited |
| Max workflow executions | 100,000/day | Unlimited |
| Concurrent instances | 25 | 10,000 (Oct 2025, up from 4,500) |
| Instance creation rate | 100/second | 100/second (Oct 2025, 10x faster) |
| Max queued instances | 100,000 | 1,000,000 |
| Max subrequests per instance | 50/request | 1,000/request |
| Retention (completed state) | 3 days | 30 days |
| Max Workflow name length | 64 chars | 64 chars |
| Max instance ID length | 100 chars | 100 chars |
mcp__cloudflare-docs__search_cloudflare_documentation