AgentStack
SKILL verified MIT Self-run

Cloudflare Workflows

skill-kgeminic-claude-skills-1-cloudflare-workflows · by Kgeminic

|

No reviews yet
0 installs
3 views
0.0% view→install

Install

$ agentstack add skill-kgeminic-claude-skills-1-cloudflare-workflows

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Cloudflare Workflows? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Cloudflare Workflows

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):

  • April 2025: Workflows GA release - waitForEvent API, Vitest testing, CPU time metrics, 4,500 concurrent instances
  • October 2025: Instance creation rate 10x faster (100/sec), concurrency increased to 10,000
  • 2025 Limits: Max steps 1,024, state persistence 1MB/step (100MB-1GB per instance), event payloads 1MB, CPU time 5 min max
  • Testing: cloudflare:test module with introspectWorkflowInstance, disableSleeps, mockStepResult, mockEvent modifiers
  • Platform: Waiting instances don't count toward concurrency, retention 3-30 days, subrequests 50-1,000

Quick Start (5 Minutes)

# 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 {
  async run(event: WorkflowEvent, 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


Known Issues Prevention

This skill prevents 12 documented errors with Cloudflare Workflows.

Issue #1: waitForEvent Skips Events After Timeout in Local Dev

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:

  • Test waitForEvent timeout scenarios in production/staging, not local dev
  • Avoid chaining multiple waitForEvent() calls where timeouts are expected

Example of Bug:

export class MyWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    for (let i = 0; i  {
    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.


Issue #4: Vitest Tests Unreliable in CI Environments

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:

  1. Increase testTimeout in vitest config:

``typescript export default defineWorkersConfig({ test: { testTimeout: 60_000 // Default: 5000ms } }); ``

  1. Check CI resource limits (CPU/memory)
  2. Use isolatedStorage: false if not testing storage isolation
  3. Consider testing against deployed instances instead of vitest for critical workflows

Status: Known issue, investigating (Internal: WOR-945).


Issue #5: Instance restart() and terminate() Not Implemented in Local Dev

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.


Issue #6: I/O Must Be Inside step.do() Callbacks

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();
});

Issue #7: NonRetryableError Behaves Differently in Dev vs Production

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.


Issue #8: In-Memory State Lost on Hibernation

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 {
  async run(event: WorkflowEvent, 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 {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    const counter = await step.do('increment', async () => 1);
    await step.sleep('wait', '1 hour');
    console.log(counter); // ✅ Still 1
  }
}

Issue #9: Non-Deterministic Step Names Break Caching

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

Issue #10: Promise.race/any Outside step.do() Causes Inconsistency

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

Issue #11: Side Effects Repeat on Restart

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)
});

Issue #12: Non-Idempotent Operations Can Repeat

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 Methods

step.do() - Execute Work

step.do(name: string, config?: WorkflowStepConfig, callback: () => Promise): Promise

Parameters:

  • name - Step name (for observability)
  • config (optional) - Retry configuration (retries, timeout, backoff)
  • callback - Async function that does the work

Returns: 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:

  • ✅ Allowed: string, number, boolean, Array, Object, null
  • ❌ Forbidden: Function, Symbol, circular references, undefined
  • Throws error if return value isn't JSON serializable

step.sleep() - Relative Sleep

step.sleep(name: string, duration: WorkflowDuration): Promise

Parameters:

  • name - Step name
  • duration - 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() - Sleep to Specific Date

step.sleepUntil(name: string, timestamp: Date | number): Promise

Parameters:

  • name - Step name
  • timestamp - 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() - Wait for External Event (GA April 2025)

step.waitForEvent(name: string, options: { type: string; timeout?: string | number }): Promise

Parameters:

  • name - Step name
  • options.type - Event type to match
  • options.timeout (optional) - Max wait time (default: 24 hours, max: 30 days)

Returns: Event payload sent via instance.sendEvent()

Example:

export class PaymentWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    await step.do('create payment', async () => { /* Stripe API */ });

    const webhookData = await step.waitForEvent(
      '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 {
    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 */ });
}

WorkflowStepConfig

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 } }

Error Handling

NonRetryableError

Force workflow to fail immediately without retrying:

import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';

export class MyWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    await step.do('validate input', async () => {
      if (!event

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Kgeminic](https://github.com/Kgeminic)
- **Source:** [Kgeminic/claude-skills-1](https://github.com/Kgeminic/claude-skills-1)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.