update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View File

@@ -0,0 +1,69 @@
# Cloudflare Workflows
Durable multi-step applications with automatic retries, state persistence, and long-running execution.
## What It Does
- Chain steps with automatic retry logic
- Persist state between steps (minutes → weeks)
- Handle failures without losing progress
- Wait for external events/approvals
- Sleep without consuming resources
**Available:** Free & Paid Workers plans
## Core Concepts
**Workflow**: Class extending `WorkflowEntrypoint` with `run` method
**Instance**: Single execution with unique ID & independent state
**Steps**: Independently retriable units via `step.do()` - API calls, DB queries, AI invocations
**State**: Persisted from step returns; step name = cache key
## Quick Start
```typescript
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
type Env = { MY_WORKFLOW: Workflow; DB: D1Database };
type Params = { userId: string };
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, 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();
});
await step.sleep('wait 7 days', '7 days');
await step.do('send reminder', async () => {
await sendEmail(user.email, 'Reminder!');
});
}
}
```
## Key Features
- **Durability**: Failed steps don't re-run successful ones
- **Retries**: Configurable backoff (constant/linear/exponential)
- **Events**: `waitForEvent()` for webhooks/approvals (timeout: 1h → 365d)
- **Sleep**: `sleep()` / `sleepUntil()` for scheduling (max 365d)
- **Parallel**: `Promise.all()` for concurrent steps
- **Idempotency**: Check-then-execute patterns
## Reading Order
**Getting Started:** configuration.md → api.md → patterns.md
**Troubleshooting:** gotchas.md
## In This Reference
- [configuration.md](./configuration.md) - wrangler.jsonc setup, step config, bindings
- [api.md](./api.md) - Step APIs, instance management, sleep/parameters
- [patterns.md](./patterns.md) - Common workflows, testing, orchestration
- [gotchas.md](./gotchas.md) - Timeouts, limits, debugging strategies
## See Also
- [durable-objects](../durable-objects/) - Alternative stateful approach
- [queues](../queues/) - Message-driven workflows
- [workers](../workers/) - Entry point for workflow instances

View File

@@ -0,0 +1,185 @@
# Workflow APIs
## Step APIs
```typescript
// step.do()
const result = await step.do('step name', async () => { /* logic */ });
const result = await step.do('step name', { retries, timeout }, async () => {});
// step.sleep()
await step.sleep('description', '1 hour');
await step.sleep('description', 5000); // ms
// step.sleepUntil()
await step.sleepUntil('description', Date.parse('2024-12-31'));
// step.waitForEvent()
const data = await step.waitForEvent<PayloadType>('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
```typescript
// 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'}}]);
// Get & Status
const instance = await env.MY_WORKFLOW.get('instance-id');
const status = await instance.status(); // {status: 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown', error?, output?}
// Control
await instance.pause(); await instance.resume(); await instance.terminate(); await instance.restart();
// Send Events
await instance.sendEvent({type: 'approval', payload: { approved: true }}); // Must match waitForEvent type
```
## Triggering Workflows
```typescript
// From Worker
export default { async fetch(req, env) { const instance = await env.MY_WORKFLOW.create({id: crypto.randomUUID(), params: { userId: 'user123' }}); return Response.json({ id: instance.id }); }};
// From Queue
export default { async queue(batch, env) { for (const msg of batch.messages) { await env.MY_WORKFLOW.create({id: `job-${msg.id}`, params: msg.body}); } }};
// From Cron
export default { async scheduled(event, env) { await env.CLEANUP_WORKFLOW.create({id: `cleanup-${Date.now()}`, params: { timestamp: event.scheduledTime }}); }};
// From Another Workflow (non-blocking)
export class ParentWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event, step) {
const child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: {}}));
}
}
```
## Error Handling
```typescript
import { NonRetryableError } from 'cloudflare:workers';
// NonRetryableError
await step.do('validate', async () => {
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
return res.json();
});
// Catching Errors
try { await step.do('risky op', async () => { throw new NonRetryableError('Failed'); }); } catch (e) { await step.do('cleanup', async () => {}); }
// Idempotency
await step.do('charge', async () => {
const sub = await fetch(`https://api/subscriptions/${id}`).then(r => r.json());
if (sub.charged) return sub; // Already done
return await fetch(`https://api/subscriptions/${id}`, {method: 'POST', body: JSON.stringify({ amount: 10.0 })}).then(r => r.json());
});
```
## Type Constraints
Params and step returns must be `Rpc.Serializable<T>`:
```typescript
// ✅ Valid types
type ValidParams = {
userId: string;
count: number;
tags: string[];
metadata: Record<string, unknown>;
};
// ❌ Invalid types
type InvalidParams = {
callback: () => void; // Functions not serializable
symbol: symbol; // Symbols not serializable
circular: any; // Circular references not allowed
};
// Step returns follow same rules
const result = await step.do('fetch', async () => {
return { userId: '123', data: [1, 2, 3] }; // ✅ Plain object
});
```
## Sleep & Scheduling
```typescript
// Relative
await step.sleep('wait 1 hour', '1 hour');
await step.sleep('wait 30 days', '30 days');
await step.sleep('wait 5s', 5000); // ms
// Absolute
await step.sleepUntil('launch date', Date.parse('24 Oct 2024 13:00:00 UTC'));
await step.sleepUntil('deadline', new Date('2024-12-31T23:59:59Z'));
```
Units: second, minute, hour, day, week, month, year. Max: 365 days.
Sleeping instances don't count toward concurrency.
## Parameters
**Pass from Worker:**
```typescript
const instance = await env.MY_WORKFLOW.create({
id: crypto.randomUUID(),
params: { userId: 'user123', email: 'user@example.com' }
});
```
**Access in Workflow:**
```typescript
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const userId = event.params.userId;
const instanceId = event.instanceId;
const createdAt = event.timestamp;
}
```
**CLI Trigger:**
```bash
npx wrangler workflows trigger my-workflow '{"userId":"user123"}'
```
## Wrangler CLI
```bash
npm create cloudflare@latest my-workflow -- --template "cloudflare/workflows-starter"
npx wrangler deploy
npx wrangler workflows list
npx wrangler workflows trigger my-workflow '{"userId":"user123"}'
npx wrangler workflows instances list my-workflow
npx wrangler workflows instances describe my-workflow instance-id
npx wrangler workflows instances pause/resume/terminate my-workflow instance-id
```
## REST API
```bash
# Create
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances" -H "Authorization: Bearer {token}" -d '{"id":"custom-id","params":{"userId":"user123"}}'
# Status
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/status" -H "Authorization: Bearer {token}"
# Send Event
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/workflows/{workflow_name}/instances/{instance_id}/events" -H "Authorization: Bearer {token}" -d '{"type":"approval","payload":{"approved":true}}'
```
See: [configuration.md](./configuration.md), [patterns.md](./patterns.md)

View File

@@ -0,0 +1,151 @@
# Workflow Configuration
## wrangler.jsonc Setup
```jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01", // Use current date for new projects
"observability": {
"enabled": true // Enables Workflows dashboard + structured logs
},
"workflows": [
{
"name": "my-workflow", // Workflow name
"binding": "MY_WORKFLOW", // Env binding
"class_name": "MyWorkflow" // TS class name
// "script_name": "other-worker" // For cross-script calls
}
],
"limits": {
"cpu_ms": 300000 // 5 min max (default 30s)
}
}
```
## Step Configuration
```typescript
// Basic step
const data = await step.do('step name', async () => ({ result: 'value' }));
// With retry config
await step.do('api call', {
retries: {
limit: 10, // Default: 5, or Infinity
delay: '10 seconds', // Default: 10000ms
backoff: 'exponential' // constant | linear | exponential
},
timeout: '30 minutes' // Per-attempt timeout (default: 10min)
}, async () => {
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error('Failed');
return res.json();
});
```
### Parallel Steps
```typescript
const [user, settings] = await Promise.all([
step.do('fetch user', async () => this.env.KV.get(`user:${id}`)),
step.do('fetch settings', async () => this.env.KV.get(`settings:${id}`))
]);
```
### Conditional Steps
```typescript
const config = await step.do('fetch config', async () =>
this.env.KV.get('flags', { type: 'json' })
);
// ✅ Deterministic (based on step output)
if (config.enableEmail) {
await step.do('send email', async () => sendEmail());
}
// ❌ Non-deterministic (Date.now outside step)
if (Date.now() > deadline) { /* BAD */ }
```
### Dynamic Steps (Loops)
```typescript
const files = await step.do('list files', async () =>
this.env.BUCKET.list()
);
for (const file of files.objects) {
await step.do(`process ${file.key}`, async () => {
const obj = await this.env.BUCKET.get(file.key);
return processData(await obj.arrayBuffer());
});
}
```
## Multiple Workflows
```jsonc
{
"workflows": [
{"name": "user-onboarding", "binding": "USER_ONBOARDING", "class_name": "UserOnboarding"},
{"name": "data-processing", "binding": "DATA_PROCESSING", "class_name": "DataProcessing"}
]
}
```
Each class extends `WorkflowEntrypoint` with its own `Params` type.
## Cross-Script Bindings
Worker A defines workflow. Worker B calls it by adding `script_name`:
```jsonc
// Worker B (caller)
{
"workflows": [{
"name": "billing-workflow",
"binding": "BILLING",
"script_name": "billing-worker" // Points to Worker A
}]
}
```
## Bindings
Workflows access Cloudflare bindings via `this.env`:
```typescript
type Env = {
MY_WORKFLOW: Workflow;
KV: KVNamespace;
DB: D1Database;
BUCKET: R2Bucket;
AI: Ai;
VECTORIZE: VectorizeIndex;
};
await step.do('use bindings', async () => {
const kv = await this.env.KV.get('key');
const db = await this.env.DB.prepare('SELECT * FROM users').first();
const file = await this.env.BUCKET.get('file.txt');
const ai = await this.env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hi' });
});
```
## Pages Functions Binding
Pages Functions can trigger Workflows via service bindings:
```typescript
// functions/_middleware.ts
export const onRequest: PagesFunction<Env> = async ({ env, request }) => {
const instance = await env.MY_WORKFLOW.create({
params: { url: request.url }
});
return new Response(`Started ${instance.id}`);
};
```
Configure in wrangler.jsonc under `service_bindings`.
See: [api.md](./api.md), [patterns.md](./patterns.md)

View File

@@ -0,0 +1,97 @@
# Gotchas & Debugging
## Common Errors
### "Step Timeout"
**Cause:** Step execution exceeding 10 minute default timeout or configured timeout
**Solution:** Set custom timeout with `step.do('long operation', {timeout: '30 minutes'}, async () => {...})` or increase CPU limit in wrangler.jsonc (max 5min CPU time)
### "waitForEvent Timeout"
**Cause:** Event not received within timeout period (default 24h, max 365d)
**Solution:** Wrap in try-catch to handle timeout gracefully and proceed with default behavior
### "Non-Deterministic Step Names"
**Cause:** Using dynamic values like `Date.now()` in step names causes replay issues
**Solution:** Use deterministic values like `event.instanceId` for step names
### "State Lost in Variables"
**Cause:** Using module-level or local variables to store state which is lost on hibernation
**Solution:** Return values from `step.do()` which are automatically persisted: `const total = await step.do('step 1', async () => 10)`
### "Non-Deterministic Conditionals"
**Cause:** Using non-deterministic logic (like `Date.now()`) outside steps in conditionals
**Solution:** Move non-deterministic operations inside steps: `const isLate = await step.do('check', async () => Date.now() > deadline)`
### "Large Step Returns Exceeding Limit"
**Cause:** Returning data >1 MiB from step
**Solution:** Store large data in R2 and return only reference: `{ key: 'r2-object-key' }`
### "Step Exceeded CPU Limit But Ran for < 30s"
**Cause:** Confusion between CPU time (active compute) and wall-clock time (includes I/O waits)
**Solution:** Network requests, database queries, and sleeps don't count toward CPU. 30s limit = 30s of active processing
### "Idempotency Violation"
**Cause:** Step operations not idempotent, causing duplicate charges or actions on retry
**Solution:** Check if operation already completed before executing (e.g., check if customer already charged)
### "Instance ID Collision"
**Cause:** Reusing instance IDs causing conflicts
**Solution:** Use unique IDs with timestamp: `await env.MY_WORKFLOW.create({ id: \`${userId}-${Date.now()}\`, params: {} })`
### "Instance Data Disappeared After Completion"
**Cause:** Completed/errored instances are automatically deleted after retention period (3 days free / 30 days paid)
**Solution:** Export critical data to KV/R2/D1 before workflow completes
### "Missing await on step.do"
**Cause:** Forgetting to await step.do() causing fire-and-forget behavior
**Solution:** Always await step operations: `await step.do('task', ...)`
## Limits
| Limit | Free | Paid | Notes |
|-------|------|------|-------|
| CPU per step | 10ms | 30s (default), 5min (max) | Set via `limits.cpu_ms` in wrangler.jsonc |
| Step state | 1 MiB | 1 MiB | Per step return value |
| 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 |
| 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 |
| Step timeout default | 10 min | 10 min | Per attempt |
| waitForEvent timeout default | 24h | 24h | Maximum 365 days |
| waitForEvent timeout max | 365 days | 365 days | Maximum wait time |
**Note:** Instances in `waiting` state (from `step.sleep` or `step.waitForEvent`) don't count toward concurrent instance limit, allowing millions of sleeping workflows.
## Pricing
| Metric | Free | Paid | Notes |
|--------|------|------|-------|
| Requests | 100k/day | 10M/mo + $0.30/M | Workflow invocations |
| CPU time | 10ms/invoke | 30M CPU-ms/mo + $0.02/M CPU-ms | Actual CPU usage |
| Storage | 1 GB | 1 GB/mo + $0.20/GB-mo | All instances (running/errored/sleeping/completed) |
## References
- [Official Docs](https://developers.cloudflare.com/workflows/)
- [Get Started Guide](https://developers.cloudflare.com/workflows/get-started/guide/)
- [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/)
- [REST API](https://developers.cloudflare.com/api/resources/workflows/)
- [Examples](https://developers.cloudflare.com/workflows/examples/)
- [Limits](https://developers.cloudflare.com/workflows/reference/limits/)
- [Pricing](https://developers.cloudflare.com/workflows/reference/pricing/)
See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)

View File

@@ -0,0 +1,175 @@
# Workflow Patterns
## Image Processing Pipeline
```typescript
export class ImageProcessingWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event, step) {
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', { event: 'approved', timeout: '24h' });
await step.do('publish', async () => await this.env.BUCKET.put(`public/${event.params.imageKey}`, imageData));
}
}
```
## User Lifecycle
```typescript
export class UserLifecycleWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event, step) {
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.params.userId).first();
return user.subscription_status === 'active';
});
if (!hasConverted) await step.do('trial expiration email', async () => await sendEmail(event.params.email, 'Trial ending'));
}
}
```
## Data Pipeline
```typescript
export class DataPipelineWorkflow extends WorkflowEntrypoint<Env, Params> {
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);
if (!res.ok) throw new Error('Fetch failed');
return res.json();
});
const transformed = await step.do('transform', async () =>
rawData.map(item => ({ id: item.id, normalized: normalizeData(item) }))
);
const dataRef = await step.do('store', async () => {
const key = `processed/${Date.now()}.json`;
await this.env.BUCKET.put(key, JSON.stringify(transformed));
return { key };
});
await step.do('load', async () => {
const data = await (await this.env.BUCKET.get(dataRef.key)).json();
for (let i = 0; i < data.length; i += 100) {
await this.env.DB.batch(data.slice(i, i + 100).map(item =>
this.env.DB.prepare('INSERT INTO records VALUES (?, ?)').bind(item.id, item.normalized)
));
}
});
}
}
```
## Human-in-the-Loop Approval
```typescript
export class ApprovalWorkflow extends WorkflowEntrypoint<Env, Params> {
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());
try {
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) {
await step.do('auto reject', async () => await this.env.DB.prepare('UPDATE approvals SET status = ? WHERE id = ?').bind('auto-rejected', event.instanceId).run());
}
}
}
```
## Testing Workflows
### Setup
```typescript
// vitest.config.ts
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' }
}
}
}
});
```
### Introspection API
```typescript
import { introspectWorkflowInstance } from 'cloudflare:test';
const instance = await env.MY_WORKFLOW.create({ params: { userId: '123' } });
const introspector = await introspectWorkflowInstance(env.MY_WORKFLOW, instance.id);
// Wait for step completion
const result = await introspector.waitForStepResult({ name: 'fetch user', index: 0 });
// Mock step behavior
await introspector.modify(async (m) => {
await m.mockStepResult({ name: 'api call' }, { mocked: true });
});
```
## Best Practices
### ✅ DO
1. **Granular steps**: One API call per step (unless proving idempotency)
2. **Idempotency**: Check-then-execute; use idempotency keys
3. **Deterministic names**: Use static or step-output-based names
4. **Return state**: Persist via step returns, not variables
5. **Always await**: `await step.do()`, avoid dangling promises
6. **Deterministic conditionals**: Base on `event.payload` or step outputs
7. **Store large data externally**: R2/KV for >1 MiB, return refs
8. **Batch creation**: `createBatch()` for multiple instances
### ❌ DON'T
1. **One giant step**: Breaks durability & retry control
2. **State outside steps**: Lost on hibernation
3. **Mutate events**: Events immutable, return new state
4. **Non-deterministic logic outside steps**: `Math.random()`, `Date.now()` must be in steps
5. **Side effects outside steps**: May duplicate on restart
6. **Non-deterministic step names**: Prevents caching
7. **Ignore timeouts**: `waitForEvent` throws, use try-catch
8. **Reuse instance IDs**: Must be unique within retention
## Orchestration Patterns
### Fan-Out (Parallel Processing)
```typescript
const files = await step.do('list', async () => this.env.BUCKET.list());
await Promise.all(files.objects.map((file, i) => step.do(`process ${i}`, async () => processFile(await (await this.env.BUCKET.get(file.key)).arrayBuffer()))));
```
### Parent-Child Workflows
```typescript
const child = await step.do('start child', async () => await this.env.CHILD_WORKFLOW.create({id: `child-${event.instanceId}`, params: { data: result.data }}));
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())
]);
```
### Scheduled Workflow Chain
```typescript
export default { async scheduled(event, env) { await env.DAILY_WORKFLOW.create({id: `daily-${event.scheduledTime}`, params: { timestamp: event.scheduledTime }}); }};
export class DailyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event, step) {
await step.do('daily task', async () => {});
await step.sleep('wait 7 days', '7 days');
await step.do('weekly followup', async () => {});
}
}
```
See: [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)