AgentStack
SKILL verified MIT Self-run

Cloudflare Workflows

skill-jackspace-claudeskillz-cloudflare-workflows · by jackspace

|

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

Install

$ agentstack add skill-jackspace-claudeskillz-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 ✅ Last Updated: 2025-10-22 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: wrangler@4.44.0, @cloudflare/workers-types@4.20251014.0


Quick Start (10 Minutes)

1. Create a Workflow

Use the Cloudflare Workflows starter template:

npm create cloudflare@latest my-workflow -- --template cloudflare/workflows-starter --git --deploy false
cd my-workflow

What you get:

  • WorkflowEntrypoint class template
  • Worker to trigger workflows
  • Complete wrangler.jsonc configuration

2. Understand the Basic Structure

src/index.ts:

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

type Env = {
  MY_WORKFLOW: Workflow;
};

type Params = {
  userId: string;
  email: string;
};

export class MyWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    // Access params from event.payload
    const { userId, email } = event.payload;

    // Step 1: Do some work
    const result = await step.do('process user', async () => {
      return { processed: true, userId };
    });

    // Step 2: Wait before next action
    await step.sleep('wait 1 hour', '1 hour');

    // Step 3: Continue workflow
    await step.do('send email', async () => {
      // Send email logic
      return { sent: true, email };
    });

    // Optional: return final state
    return { completed: true, userId };
  }
}

// Worker to trigger workflow
export default {
  async fetch(req: Request, env: Env): Promise {
    // Create new workflow instance
    const instance = await env.MY_WORKFLOW.create({
      params: { userId: '123', email: 'user@example.com' }
    });

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

3. Configure Wrangler

wrangler.jsonc:

{
  "name": "my-workflow",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-22",
  "workflows": [
    {
      "name": "my-workflow",
      "binding": "MY_WORKFLOW",
      "class_name": "MyWorkflow"
    }
  ]
}

4. Deploy and Test

# Deploy workflow
npm run deploy

# Trigger workflow (visit in browser or curl)
curl https://my-workflow..workers.dev/

# View workflow instances
npx wrangler workflows instances list my-workflow

# Check instance status
npx wrangler workflows instances describe my-workflow 

WorkflowEntrypoint Class

Extend WorkflowEntrypoint

Every Workflow must extend WorkflowEntrypoint and implement a run() method:

export class MyWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    // Workflow steps here
  }
}

Type Parameters:

  • Env - Environment bindings (KV, D1, R2, etc.)
  • Params - Type of workflow parameters passed via event.payload

run() Method

async run(
  event: WorkflowEvent,
  step: WorkflowStep
): Promise

Parameters:

  • event - Contains workflow metadata and payload
  • step - Provides step methods (do, sleep, sleepUntil, waitForEvent)

Returns:

  • Optional return value (must be serializable)
  • Return value available via instance.status()

Example:

export class OrderWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    const { orderId, customerId } = event.payload;

    // Access bindings via this.env
    const order = await this.env.DB.prepare(
      'SELECT * FROM orders WHERE id = ?'
    ).bind(orderId).first();

    const result = await step.do('process payment', async () => {
      // Payment processing
      return { paid: true, amount: order.total };
    });

    // Return final state
    return {
      orderId,
      status: 'completed',
      paidAmount: result.amount
    };
  }
}

Step Methods

step.do() - Execute Work

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

OR (config is optional):

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

Parameters:

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

Returns:

  • The value returned from callback (must be serializable)

Example:

// Simple step
const files = await step.do('fetch files', async () => {
  const response = await fetch('https://api.example.com/files');
  return await response.json();
});

// Step with retry config
const result = await step.do(
  'call payment API',
  {
    retries: {
      limit: 10,
      delay: '10 seconds',
      backoff: 'exponential'
    },
    timeout: '5 minutes'
  },
  async () => {
    const response = await fetch('https://payment-api.example.com/charge', {
      method: 'POST',
      body: JSON.stringify({ amount: 100 })
    });
    return await response.json();
  }
);

CRITICAL - Serialization:

  • Return value must be JSON serializable
  • ✅ Allowed: string, number, boolean, Array, Object, null
  • ❌ Forbidden: Function, Symbol, circular references, undefined
  • Step will throw error if return value isn't serializable

step.sleep() - Relative Sleep

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

Parameters:

  • name - Step name
  • duration - Number (milliseconds) or human-readable string

Accepted units:

  • "second" / "seconds"
  • "minute" / "minutes"
  • "hour" / "hours"
  • "day" / "days"
  • "week" / "weeks"
  • "month" / "months"
  • "year" / "years"

Examples:

// Sleep for 5 minutes
await step.sleep('wait 5 minutes', '5 minutes');

// Sleep for 1 hour
await step.sleep('hourly delay', '1 hour');

// Sleep for 2 days
await step.sleep('wait 2 days', '2 days');

// Sleep using milliseconds
await step.sleep('wait 30 seconds', 30000);

// Common pattern: schedule daily task
await step.do('send daily report', async () => {
  // Send report
});
await step.sleep('wait until tomorrow', '1 day');
// Workflow continues next day

Priority:

  • Workflows resuming from sleep take priority over new instances
  • Ensures older workflows complete before new ones start

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:

// Sleep until specific date
const launchDate = new Date('2025-12-25T00:00:00Z');
await step.sleepUntil('wait for launch', launchDate);

// Sleep until UNIX timestamp
const timestamp = Date.parse('24 Oct 2024 13:00:00 UTC');
await step.sleepUntil('wait until time', timestamp);

// Sleep until next Monday 9am UTC
const nextMonday = new Date();
nextMonday.setDate(nextMonday.getDate() + ((1 + 7 - nextMonday.getDay()) % 7 || 7));
nextMonday.setUTCHours(9, 0, 0, 0);
await step.sleepUntil('wait until Monday 9am', nextMonday);

// Schedule work at specific time
await step.do('prepare campaign', async () => {
  // Prepare marketing campaign
});

const campaignLaunch = new Date('2025-11-01T12:00:00Z');
await step.sleepUntil('wait for campaign launch', campaignLaunch);

await step.do('launch campaign', async () => {
  // Launch campaign
});

step.waitForEvent() - Wait for External Event

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)

Returns:

  • The event payload sent via instance.sendEvent()

Example:

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

    // Wait for webhook from Stripe (max 1 hour)
    const webhookData = await step.waitForEvent(
      'wait for payment confirmation',
      { type: 'stripe-webhook', timeout: '1 hour' }
    );

    // Continue based on webhook
    if (webhookData.status === 'succeeded') {
      await step.do('fulfill order', async () => {
        // Fulfill order
      });
    } else {
      await step.do('handle failed payment', async () => {
        // Handle failure
      });
    }
  }
}

// Worker receives webhook and sends event to workflow
export default {
  async fetch(req: Request, env: Env): Promise {
    if (req.url.includes('/webhook/stripe')) {
      const webhookData = await req.json();

      // Get workflow instance by ID (stored when created)
      const instance = await env.PAYMENT_WORKFLOW.get(instanceId);

      // Send event to waiting workflow
      await instance.sendEvent({
        type: 'stripe-webhook',
        payload: webhookData
      });

      return new Response('OK');
    }
  }
};

Timeout behavior:

  • If timeout expires, throws error and workflow can retry or fail
  • Wrap in try-catch if timeout should not fail workflow
try {
  const event = await step.waitForEvent('wait for user input', {
    type: 'user-submitted',
    timeout: '10 minutes'
  });
} catch (error) {
  // Timeout occurred - handle gracefully
  await step.do('send reminder', async () => {
    // Send reminder to user
  });
}

WorkflowStepConfig

Configure retry behavior for individual steps:

interface WorkflowStepConfig {
  retries?: {
    limit: number;          // Max retry attempts (Infinity allowed)
    delay: string | number; // Delay between retries
    backoff?: 'constant' | 'linear' | 'exponential';
  };
  timeout?: string | number; // Max time per attempt
}

Default Configuration

If no config provided, Workflows uses:

{
  retries: {
    limit: 5,
    delay: 10000,      // 10 seconds
    backoff: 'exponential'
  },
  timeout: '10 minutes'
}

Retry Examples

Constant Backoff (same delay each time):

await step.do(
  'send email',
  {
    retries: {
      limit: 3,
      delay: '30 seconds',
      backoff: 'constant'  // Always wait 30 seconds
    }
  },
  async () => {
    // Send email
  }
);

Linear Backoff (increasing delay):

await step.do(
  'poll API',
  {
    retries: {
      limit: 5,
      delay: '1 minute',
      backoff: 'linear'  // 1m, 2m, 3m, 4m, 5m
    }
  },
  async () => {
    // Poll API
  }
);

Exponential Backoff (recommended for most cases):

await step.do(
  'call rate-limited API',
  {
    retries: {
      limit: 10,
      delay: '10 seconds',
      backoff: 'exponential'  // 10s, 20s, 40s, 80s, 160s, ...
    },
    timeout: '5 minutes'
  },
  async () => {
    // API call
  }
);

Unlimited Retries:

await step.do(
  'critical operation',
  {
    retries: {
      limit: Infinity,  // Retry forever
      delay: '1 minute',
      backoff: 'exponential'
    }
  },
  async () => {
    // Operation that must succeed eventually
  }
);

No Retries:

await step.do(
  'non-idempotent operation',
  {
    retries: {
      limit: 0  // Fail immediately on error
    }
  },
  async () => {
    // One-time operation
  }
);

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

  • ✅ Authentication/authorization failures
  • ✅ Invalid input that won't change
  • ✅ Resource doesn't exist (404)
  • ✅ Validation errors
  • ❌ Network failures (should retry)
  • ❌ Rate limits (should retry with backoff)
  • ❌ Temporary service outages (should retry)

Catch Errors to Continue Workflow

Prevent entire workflow from failing by catching step errors:

export class MyWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    // Critical step - workflow fails if this fails
    await step.do('process payment', async () => {
      // Payment processing
    });

    // Optional step - workflow continues even if it fails
    try {
      await step.do('send confirmation email', async () => {
        // Email sending
      });
    } catch (error) {
      console.log(`Email failed: ${error.message}`);

      // Do cleanup or alternative action
      await step.do('log email failure', async () => {
        await this.env.DB.prepare(
          'INSERT INTO failed_emails (user_id, error) VALUES (?, ?)'
        ).bind(event.payload.userId, error.message).run();
      });
    }

    // Workflow continues
    await step.do('update order status', async () => {
      // Update status
    });
  }
}

Pattern: Graceful degradation:

// Try primary service, fall back to secondary
let result;

try {
  result = await step.do('call primary API', async () => {
    return await callPrimaryAPI();
  });
} catch (error) {
  console.log('Primary API failed, trying backup');

  result = await step.do('call backup API', async () => {
    return await callBackupAPI();
  });
}

Triggering Workflows

From Workers

Configure binding in wrangler.jsonc:

{
  "name": "trigger-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-22",
  "workflows": [
    {
      "name": "my-workflow",
      "binding": "MY_WORKFLOW",
      "class_name": "MyWorkflow",
      "script_name": "workflow-worker"  // If workflow is in different Worker
    }
  ]
}

Trigger from Worker:

type Env = {
  MY_WORKFLOW: Workflow;
};

export default {
  async fetch(req: Request, env: Env): Promise {
    // Create new workflow instance
    const instance = await env.MY_WORKFLOW.create({
      params: {
        userId: '123',
        email: 'user@example.com'
      }
    });

    // Return instance ID
    return Response.json({
      id: instance.id,
      status: await instance.status()
    });
  }
};

Get Instance Status

// Get instance by ID
const instance = await env.MY_WORKFLOW.get(instanceId);

// Get status
const status = await instance.status();

console.log(status);
// {
//   status: 'running' | 'complete' | 'errored' | 'queued' | 'unknown',
//   error: string | null,
//   output: any  // Return value from run() if complete
// }

Send Events to Running Instance

// Get instance
const instance = await env.MY_WORKFLOW.get(instanceId);

// Send event (will be received by step.waitForEvent)
await instance.sendEvent({
  type: 'user-action',
  payload: { action: 'approved' }
});

Pause and Resume

// Pause instance
await instance.pause();

// Resume instance
await instance.resume();

// Terminate instance
await instance.terminate();

Workflow Patterns

Pattern 1: Long-Running Process

export class VideoProcessingWorkflow extends WorkflowEntrypoint {
  async run(event: WorkflowEvent, step: WorkflowStep) {
    const { videoId } = event.payload;

    // Step 1: Upload to processing service
    const uploadResult = await step.do('upl

…

## Source & license

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

- **Author:** [jackspace](https://github.com/jackspace)
- **Source:** [jackspace/ClaudeSkillz](https://github.com/jackspace/ClaudeSkillz)
- **License:** MIT
- **Homepage:** http://claudeskillz.jackspace.com/

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.