Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:41 +08:00
commit f6fce7220b
12 changed files with 3095 additions and 0 deletions

136
templates/basic-workflow.ts Normal file
View File

@@ -0,0 +1,136 @@
/**
* Basic Cloudflare Workflow Example
*
* Demonstrates:
* - WorkflowEntrypoint class
* - step.do() for executing work
* - step.sleep() for delays
* - Accessing environment bindings
* - Returning state from workflow
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
// Define environment bindings
type Env = {
MY_WORKFLOW: Workflow;
// Add your bindings here:
// MY_KV: KVNamespace;
// DB: D1Database;
// MY_BUCKET: R2Bucket;
};
// Define workflow parameters
type Params = {
userId: string;
email: string;
};
/**
* Basic Workflow
*
* Three-step workflow that:
* 1. Fetches user data
* 2. Processes user data
* 3. Sends notification
*/
export class BasicWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// Access parameters from event.payload
const { userId, email } = event.payload;
console.log(`Starting workflow for user ${userId}`);
// Step 1: Fetch user data
const userData = await step.do('fetch user data', async () => {
// Example: Fetch from external API
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
return {
id: data.id,
name: data.name,
email: data.email,
preferences: data.preferences
};
});
console.log(`Fetched user: ${userData.name}`);
// Step 2: Process user data
const processedData = await step.do('process user data', async () => {
// Example: Perform some computation
return {
userId: userData.id,
processedAt: new Date().toISOString(),
status: 'processed'
};
});
// Step 3: Wait before sending notification
await step.sleep('wait before notification', '5 minutes');
// Step 4: Send notification
await step.do('send notification', async () => {
// Example: Send email or push notification
await fetch('https://api.example.com/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: email,
subject: 'Processing Complete',
body: `Your data has been processed at ${processedData.processedAt}`
})
});
return { sent: true, timestamp: Date.now() };
});
// Return final state (must be serializable)
return {
userId,
status: 'complete',
processedAt: processedData.processedAt
};
}
}
/**
* Worker that triggers the workflow
*/
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
// Handle favicon
if (url.pathname.startsWith('/favicon')) {
return Response.json({}, { status: 404 });
}
// Get instance status if ID provided
const instanceId = url.searchParams.get('instanceId');
if (instanceId) {
const instance = await env.MY_WORKFLOW.get(instanceId);
const status = await instance.status();
return Response.json({
id: instanceId,
status
});
}
// Create new workflow instance
const instance = await env.MY_WORKFLOW.create({
params: {
userId: '123',
email: 'user@example.com'
}
});
return Response.json({
id: instance.id,
details: await instance.status(),
statusUrl: `${url.origin}?instanceId=${instance.id}`
});
}
};

View File

@@ -0,0 +1,252 @@
/**
* Scheduled Workflow Example
*
* Demonstrates:
* - step.sleep() for relative delays
* - step.sleepUntil() for absolute times
* - Scheduling daily/weekly tasks
* - Long-running processes
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
type Env = {
MY_WORKFLOW: Workflow;
DB: D1Database;
};
type ReportParams = {
reportType: 'daily' | 'weekly' | 'monthly';
recipients: string[];
};
/**
* Scheduled Reporting Workflow
*
* Generates and sends reports on a schedule:
* - Daily reports at 9am UTC
* - Weekly reports on Monday 9am UTC
* - Monthly reports on 1st of month 9am UTC
*/
export class ScheduledReportWorkflow extends WorkflowEntrypoint<Env, ReportParams> {
async run(event: WorkflowEvent<ReportParams>, step: WorkflowStep) {
const { reportType, recipients } = event.payload;
if (reportType === 'daily') {
await this.runDailyReport(step, recipients);
} else if (reportType === 'weekly') {
await this.runWeeklyReport(step, recipients);
} else if (reportType === 'monthly') {
await this.runMonthlyReport(step, recipients);
}
return { reportType, status: 'complete' };
}
/**
* Daily report - runs every day at 9am UTC
*/
private async runDailyReport(step: WorkflowStep, recipients: string[]) {
// Calculate next 9am UTC
const now = new Date();
const next9am = new Date();
next9am.setUTCDate(next9am.getUTCDate() + 1);
next9am.setUTCHours(9, 0, 0, 0);
// Sleep until tomorrow 9am
await step.sleepUntil('wait until 9am tomorrow', next9am);
// Generate report
const report = await step.do('generate daily report', async () => {
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T')[0];
const results = await this.env.DB.prepare(
'SELECT * FROM daily_metrics WHERE date = ?'
).bind(dateStr).all();
return {
date: dateStr,
type: 'daily',
metrics: results.results
};
});
// Send report
await step.do('send daily report', async () => {
await this.sendReport(report, recipients);
return { sent: true };
});
}
/**
* Weekly report - runs every Monday at 9am UTC
*/
private async runWeeklyReport(step: WorkflowStep, recipients: string[]) {
// Calculate next Monday 9am UTC
const nextMonday = new Date();
const daysUntilMonday = (1 + 7 - nextMonday.getDay()) % 7 || 7;
nextMonday.setDate(nextMonday.getDate() + daysUntilMonday);
nextMonday.setUTCHours(9, 0, 0, 0);
await step.sleepUntil('wait until Monday 9am', nextMonday);
// Generate report
const report = await step.do('generate weekly report', async () => {
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const results = await this.env.DB.prepare(
'SELECT * FROM daily_metrics WHERE date >= ? ORDER BY date DESC'
).bind(lastWeek.toISOString().split('T')[0]).all();
return {
weekStart: lastWeek.toISOString().split('T')[0],
type: 'weekly',
metrics: results.results
};
});
// Send report
await step.do('send weekly report', async () => {
await this.sendReport(report, recipients);
return { sent: true };
});
}
/**
* Monthly report - runs on 1st of each month at 9am UTC
*/
private async runMonthlyReport(step: WorkflowStep, recipients: string[]) {
// Calculate first day of next month at 9am UTC
const firstOfNextMonth = new Date();
firstOfNextMonth.setUTCMonth(firstOfNextMonth.getUTCMonth() + 1, 1);
firstOfNextMonth.setUTCHours(9, 0, 0, 0);
await step.sleepUntil('wait until 1st of month 9am', firstOfNextMonth);
// Generate report
const report = await step.do('generate monthly report', async () => {
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
const monthStart = new Date(lastMonth.getFullYear(), lastMonth.getMonth(), 1);
const monthEnd = new Date(lastMonth.getFullYear(), lastMonth.getMonth() + 1, 0);
const results = await this.env.DB.prepare(
'SELECT * FROM daily_metrics WHERE date >= ? AND date <= ? ORDER BY date DESC'
).bind(
monthStart.toISOString().split('T')[0],
monthEnd.toISOString().split('T')[0]
).all();
return {
month: lastMonth.toISOString().substring(0, 7), // YYYY-MM
type: 'monthly',
metrics: results.results
};
});
// Send report
await step.do('send monthly report', async () => {
await this.sendReport(report, recipients);
return { sent: true };
});
}
/**
* Send report via email
*/
private async sendReport(report: any, recipients: string[]) {
const subject = `${report.type.charAt(0).toUpperCase() + report.type.slice(1)} Report`;
await fetch('https://api.example.com/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: recipients,
subject,
body: this.formatReport(report)
})
});
}
/**
* Format report data as HTML
*/
private formatReport(report: any): string {
// Format report metrics as HTML
return `
<h1>${report.type} Report</h1>
<p>Period: ${report.date || report.weekStart || report.month}</p>
<pre>${JSON.stringify(report.metrics, null, 2)}</pre>
`;
}
}
/**
* Example: Reminder Workflow with Multiple Delays
*/
export class ReminderWorkflow extends WorkflowEntrypoint<Env, { userId: string; message: string }> {
async run(event: WorkflowEvent<{ userId: string; message: string }>, step: WorkflowStep) {
const { userId, message } = event.payload;
// Send initial reminder
await step.do('send initial reminder', async () => {
await this.sendReminder(userId, message);
return { sent: true };
});
// Wait 1 hour
await step.sleep('wait 1 hour', '1 hour');
// Send second reminder
await step.do('send second reminder', async () => {
await this.sendReminder(userId, `Reminder: ${message}`);
return { sent: true };
});
// Wait 1 day
await step.sleep('wait 1 day', '1 day');
// Send final reminder
await step.do('send final reminder', async () => {
await this.sendReminder(userId, `Final reminder: ${message}`);
return { sent: true };
});
return { userId, remindersSent: 3 };
}
private async sendReminder(userId: string, message: string) {
await fetch(`https://api.example.com/send-notification`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, message })
});
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
if (url.pathname.startsWith('/favicon')) {
return Response.json({}, { status: 404 });
}
// Create daily report workflow
const instance = await env.MY_WORKFLOW.create({
params: {
reportType: 'daily',
recipients: ['admin@example.com', 'team@example.com']
}
});
return Response.json({
id: instance.id,
status: await instance.status(),
message: 'Daily report workflow scheduled'
});
}
};

287
templates/worker-trigger.ts Normal file
View File

@@ -0,0 +1,287 @@
/**
* Worker that Triggers and Manages Workflows
*
* Demonstrates:
* - Creating workflow instances
* - Querying workflow status
* - Sending events to workflows
* - Pausing/resuming workflows
* - Terminating workflows
*/
import { Hono } from 'hono';
type Bindings = {
MY_WORKFLOW: Workflow;
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
/**
* Create new workflow instance
*/
app.post('/workflows/create', async (c) => {
const body = await c.req.json<{
userId: string;
email: string;
[key: string]: any;
}>();
try {
// Create workflow instance with parameters
const instance = await c.env.MY_WORKFLOW.create({
params: body
});
// Optionally store instance ID for later reference
await c.env.DB.prepare(`
INSERT INTO workflow_instances (id, user_id, status, created_at)
VALUES (?, ?, ?, ?)
`).bind(
instance.id,
body.userId,
'queued',
new Date().toISOString()
).run();
return c.json({
id: instance.id,
status: await instance.status(),
createdAt: new Date().toISOString()
}, 201);
} catch (error) {
return c.json({
error: 'Failed to create workflow',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* Get workflow instance status
*/
app.get('/workflows/:id', async (c) => {
const instanceId = c.req.param('id');
try {
const instance = await c.env.MY_WORKFLOW.get(instanceId);
const status = await instance.status();
return c.json({
id: instanceId,
...status
});
} catch (error) {
return c.json({
error: 'Workflow not found',
message: error instanceof Error ? error.message : 'Unknown error'
}, 404);
}
});
/**
* Send event to waiting workflow
*/
app.post('/workflows/:id/events', async (c) => {
const instanceId = c.req.param('id');
const body = await c.req.json<{
type: string;
payload: any;
}>();
try {
const instance = await c.env.MY_WORKFLOW.get(instanceId);
await instance.sendEvent({
type: body.type,
payload: body.payload
});
return c.json({
success: true,
message: 'Event sent to workflow'
});
} catch (error) {
return c.json({
error: 'Failed to send event',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* Pause workflow instance
*/
app.post('/workflows/:id/pause', async (c) => {
const instanceId = c.req.param('id');
try {
const instance = await c.env.MY_WORKFLOW.get(instanceId);
await instance.pause();
// Update database
await c.env.DB.prepare(`
UPDATE workflow_instances SET status = ? WHERE id = ?
`).bind('paused', instanceId).run();
return c.json({
success: true,
message: 'Workflow paused'
});
} catch (error) {
return c.json({
error: 'Failed to pause workflow',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* Resume paused workflow instance
*/
app.post('/workflows/:id/resume', async (c) => {
const instanceId = c.req.param('id');
try {
const instance = await c.env.MY_WORKFLOW.get(instanceId);
await instance.resume();
// Update database
await c.env.DB.prepare(`
UPDATE workflow_instances SET status = ? WHERE id = ?
`).bind('running', instanceId).run();
return c.json({
success: true,
message: 'Workflow resumed'
});
} catch (error) {
return c.json({
error: 'Failed to resume workflow',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* Terminate workflow instance
*/
app.post('/workflows/:id/terminate', async (c) => {
const instanceId = c.req.param('id');
try {
const instance = await c.env.MY_WORKFLOW.get(instanceId);
await instance.terminate();
// Update database
await c.env.DB.prepare(`
UPDATE workflow_instances SET status = ? WHERE id = ?
`).bind('terminated', instanceId).run();
return c.json({
success: true,
message: 'Workflow terminated'
});
} catch (error) {
return c.json({
error: 'Failed to terminate workflow',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* List all workflow instances (with filtering)
*/
app.get('/workflows', async (c) => {
const status = c.req.query('status');
const userId = c.req.query('userId');
const limit = parseInt(c.req.query('limit') || '20');
const offset = parseInt(c.req.query('offset') || '0');
let query = 'SELECT * FROM workflow_instances WHERE 1=1';
const params: any[] = [];
if (status) {
query += ' AND status = ?';
params.push(status);
}
if (userId) {
query += ' AND user_id = ?';
params.push(userId);
}
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
try {
const results = await c.env.DB.prepare(query).bind(...params).all();
return c.json({
workflows: results.results,
limit,
offset,
total: results.results.length
});
} catch (error) {
return c.json({
error: 'Failed to list workflows',
message: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
/**
* Health check
*/
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString()
});
});
/**
* API documentation
*/
app.get('/', (c) => {
return c.json({
name: 'Workflow Management API',
version: '1.0.0',
endpoints: {
'POST /workflows/create': {
description: 'Create new workflow instance',
body: { userId: 'string', email: 'string', ...params: 'any' }
},
'GET /workflows/:id': {
description: 'Get workflow status',
params: { id: 'workflow instance ID' }
},
'POST /workflows/:id/events': {
description: 'Send event to workflow',
params: { id: 'workflow instance ID' },
body: { type: 'string', payload: 'any' }
},
'POST /workflows/:id/pause': {
description: 'Pause workflow',
params: { id: 'workflow instance ID' }
},
'POST /workflows/:id/resume': {
description: 'Resume paused workflow',
params: { id: 'workflow instance ID' }
},
'POST /workflows/:id/terminate': {
description: 'Terminate workflow',
params: { id: 'workflow instance ID' }
},
'GET /workflows': {
description: 'List workflows',
query: { status: 'string (optional)', userId: 'string (optional)', limit: 'number', offset: 'number' }
}
}
});
});
export default app;

View File

@@ -0,0 +1,335 @@
/**
* Event-Driven Workflow Example
*
* Demonstrates:
* - step.waitForEvent() for external events
* - instance.sendEvent() to trigger waiting workflows
* - Timeout handling
* - Human-in-the-loop patterns
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
type Env = {
APPROVAL_WORKFLOW: Workflow;
DB: D1Database;
};
type ApprovalParams = {
requestId: string;
requesterId: string;
amount: number;
description: string;
};
type ApprovalEvent = {
approved: boolean;
approverId: string;
comments?: string;
};
/**
* Approval Workflow with Event Waiting
*
* Flow:
* 1. Create approval request
* 2. Notify approvers
* 3. Wait for approval decision (max 7 days)
* 4. Process decision
* 5. Execute approved action or reject
*/
export class ApprovalWorkflow extends WorkflowEntrypoint<Env, ApprovalParams> {
async run(event: WorkflowEvent<ApprovalParams>, step: WorkflowStep) {
const { requestId, requesterId, amount, description } = event.payload;
// Step 1: Create approval request in database
await step.do('create approval request', async () => {
await this.env.DB.prepare(`
INSERT INTO approval_requests
(id, requester_id, amount, description, status, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).bind(
requestId,
requesterId,
amount,
description,
'pending',
new Date().toISOString()
).run();
return { created: true };
});
// Step 2: Send notification to approvers
await step.do('notify approvers', async () => {
// Get list of approvers based on amount
const approvers = amount > 10000
? ['senior-manager@example.com', 'finance@example.com']
: ['manager@example.com'];
// Send notification to each approver
await fetch('https://api.example.com/send-notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipients: approvers,
subject: `Approval Required: ${description}`,
body: `
Request ID: ${requestId}
Amount: $${amount}
Description: ${description}
Requester: ${requesterId}
Please review and approve/reject at:
https://app.example.com/approvals/${requestId}
`,
data: {
requestId,
workflowInstanceId: event.instanceId // Store for sending event later
}
})
});
return { notified: true, approvers };
});
// Step 3: Wait for approval decision (max 7 days)
let approvalEvent: ApprovalEvent;
try {
approvalEvent = await step.waitForEvent<ApprovalEvent>(
'wait for approval decision',
{
type: 'approval-decision',
timeout: '7 days' // Auto-reject after 7 days
}
);
console.log('Approval decision received:', approvalEvent);
} catch (error) {
// Timeout occurred - auto-reject
console.log('Approval timeout - auto-rejecting');
await step.do('auto-reject due to timeout', async () => {
await this.env.DB.prepare(`
UPDATE approval_requests
SET status = ?, updated_at = ?, rejection_reason = ?
WHERE id = ?
`).bind(
'rejected',
new Date().toISOString(),
'Approval timeout - no response within 7 days',
requestId
).run();
// Notify requester
await this.notifyRequester(requesterId, requestId, false, 'Approval timeout');
return { rejected: true, reason: 'timeout' };
});
return {
requestId,
status: 'rejected',
reason: 'timeout'
};
}
// Step 4: Process approval decision
await step.do('process approval decision', async () => {
await this.env.DB.prepare(`
UPDATE approval_requests
SET status = ?, approver_id = ?, comments = ?, updated_at = ?
WHERE id = ?
`).bind(
approvalEvent.approved ? 'approved' : 'rejected',
approvalEvent.approverId,
approvalEvent.comments || null,
new Date().toISOString(),
requestId
).run();
return { processed: true };
});
// Step 5: Notify requester
await step.do('notify requester', async () => {
await this.notifyRequester(
requesterId,
requestId,
approvalEvent.approved,
approvalEvent.comments
);
return { notified: true };
});
// Step 6: Execute approved action if approved
if (approvalEvent.approved) {
await step.do('execute approved action', async () => {
// Execute the action that was approved
console.log(`Executing approved action for request ${requestId}`);
// Example: Process payment, create resource, etc.
await fetch('https://api.example.com/execute-action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId,
amount,
description
})
});
return { executed: true };
});
}
return {
requestId,
status: approvalEvent.approved ? 'approved' : 'rejected',
approver: approvalEvent.approverId
};
}
/**
* Send notification to requester
*/
private async notifyRequester(
requesterId: string,
requestId: string,
approved: boolean,
comments?: string
) {
await fetch('https://api.example.com/send-notification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient: requesterId,
subject: `Request ${requestId} ${approved ? 'Approved' : 'Rejected'}`,
body: `
Your request ${requestId} has been ${approved ? 'approved' : 'rejected'}.
${comments ? `\n\nComments: ${comments}` : ''}
`
})
});
}
}
/**
* Worker that handles:
* 1. Creating approval workflows
* 2. Receiving approval decisions via webhook
* 3. Sending events to waiting workflows
*/
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
if (url.pathname.startsWith('/favicon')) {
return Response.json({}, { status: 404 });
}
// Endpoint: Create new approval request
if (url.pathname === '/approvals/create' && req.method === 'POST') {
const body = await req.json<ApprovalParams>();
// Create workflow instance
const instance = await env.APPROVAL_WORKFLOW.create({
params: body
});
// Store instance ID for later (when approval decision comes in)
// In production, store this in DB/KV
await env.DB.prepare(`
UPDATE approval_requests
SET workflow_instance_id = ?
WHERE id = ?
`).bind(instance.id, body.requestId).run();
return Response.json({
id: instance.id,
requestId: body.requestId,
status: await instance.status()
});
}
// Endpoint: Submit approval decision (webhook from approval UI)
if (url.pathname === '/approvals/decide' && req.method === 'POST') {
const body = await req.json<{
requestId: string;
approved: boolean;
approverId: string;
comments?: string;
}>();
// Get workflow instance ID from database
const result = await env.DB.prepare(`
SELECT workflow_instance_id
FROM approval_requests
WHERE id = ?
`).bind(body.requestId).first<{ workflow_instance_id: string }>();
if (!result) {
return Response.json(
{ error: 'Request not found' },
{ status: 404 }
);
}
// Get workflow instance
const instance = await env.APPROVAL_WORKFLOW.get(result.workflow_instance_id);
// Send event to waiting workflow
await instance.sendEvent({
type: 'approval-decision',
payload: {
approved: body.approved,
approverId: body.approverId,
comments: body.comments
}
});
return Response.json({
success: true,
message: 'Approval decision sent to workflow'
});
}
// Endpoint: Get approval status
if (url.pathname.startsWith('/approvals/') && req.method === 'GET') {
const requestId = url.pathname.split('/')[2];
const result = await env.DB.prepare(`
SELECT workflow_instance_id, status
FROM approval_requests
WHERE id = ?
`).bind(requestId).first<{ workflow_instance_id: string; status: string }>();
if (!result) {
return Response.json(
{ error: 'Request not found' },
{ status: 404 }
);
}
const instance = await env.APPROVAL_WORKFLOW.get(result.workflow_instance_id);
const workflowStatus = await instance.status();
return Response.json({
requestId,
dbStatus: result.status,
workflowStatus
});
}
// Default: Show usage
return Response.json({
endpoints: {
'POST /approvals/create': 'Create approval request',
'POST /approvals/decide': 'Submit approval decision',
'GET /approvals/:id': 'Get approval status'
}
});
}
};

View File

@@ -0,0 +1,235 @@
/**
* Workflow with Advanced Retry Configuration
*
* Demonstrates:
* - Custom retry limits
* - Exponential, linear, and constant backoff
* - Step timeouts
* - NonRetryableError for terminal failures
* - Error handling with try-catch
*/
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
import { NonRetryableError } from 'cloudflare:workflows';
type Env = {
MY_WORKFLOW: Workflow;
};
type PaymentParams = {
orderId: string;
amount: number;
customerId: string;
};
/**
* Payment Processing Workflow with Retries
*
* Handles payment processing with:
* - Validation with NonRetryableError
* - Retry logic for payment gateway
* - Fallback to backup gateway
* - Graceful error handling
*/
export class PaymentWorkflow extends WorkflowEntrypoint<Env, PaymentParams> {
async run(event: WorkflowEvent<PaymentParams>, step: WorkflowStep) {
const { orderId, amount, customerId } = event.payload;
// Step 1: Validate input (no retries - fail fast)
await step.do(
'validate payment request',
{
retries: {
limit: 0 // No retries for validation
}
},
async () => {
if (!orderId || !customerId) {
throw new NonRetryableError('Missing required fields: orderId or customerId');
}
if (amount <= 0) {
throw new NonRetryableError(`Invalid amount: ${amount}`);
}
if (amount > 100000) {
throw new NonRetryableError(`Amount exceeds limit: ${amount}`);
}
return { valid: true };
}
);
// Step 2: Call primary payment gateway (exponential backoff)
let paymentResult;
try {
paymentResult = await step.do(
'charge primary payment gateway',
{
retries: {
limit: 5, // Max 5 retry attempts
delay: '10 seconds', // Start at 10 seconds
backoff: 'exponential' // 10s, 20s, 40s, 80s, 160s
},
timeout: '2 minutes' // Each attempt times out after 2 minutes
},
async () => {
const response = await fetch('https://primary-payment-gateway.example.com/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId,
amount,
customerId
})
});
if (!response.ok) {
// Check if error is retryable
if (response.status === 401 || response.status === 403) {
throw new NonRetryableError('Authentication failed with payment gateway');
}
throw new Error(`Payment gateway error: ${response.status}`);
}
const data = await response.json();
return {
transactionId: data.transactionId,
status: data.status,
gateway: 'primary'
};
}
);
} catch (error) {
console.error('Primary gateway failed:', error);
// Step 3: Fallback to backup gateway (linear backoff)
paymentResult = await step.do(
'charge backup payment gateway',
{
retries: {
limit: 3,
delay: '30 seconds',
backoff: 'linear' // 30s, 60s, 90s
},
timeout: '3 minutes'
},
async () => {
const response = await fetch('https://backup-payment-gateway.example.com/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId,
amount,
customerId
})
});
if (!response.ok) {
throw new Error(`Backup gateway error: ${response.status}`);
}
const data = await response.json();
return {
transactionId: data.transactionId,
status: data.status,
gateway: 'backup'
};
}
);
}
// Step 4: Update order status (constant backoff)
await step.do(
'update order status',
{
retries: {
limit: 10,
delay: '5 seconds',
backoff: 'constant' // Always 5 seconds between retries
},
timeout: '30 seconds'
},
async () => {
const response = await fetch(`https://api.example.com/orders/${orderId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'paid',
transactionId: paymentResult.transactionId,
gateway: paymentResult.gateway
})
});
if (!response.ok) {
throw new Error('Failed to update order status');
}
return { updated: true };
}
);
// Step 5: Send confirmation (optional - don't fail workflow if this fails)
try {
await step.do(
'send payment confirmation',
{
retries: {
limit: 3,
delay: '10 seconds',
backoff: 'exponential'
}
},
async () => {
await fetch('https://api.example.com/notifications/payment-confirmed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId,
customerId,
amount
})
});
return { sent: true };
}
);
} catch (error) {
// Log but don't fail workflow
console.error('Failed to send confirmation:', error);
}
return {
orderId,
transactionId: paymentResult.transactionId,
gateway: paymentResult.gateway,
status: 'complete'
};
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
if (url.pathname.startsWith('/favicon')) {
return Response.json({}, { status: 404 });
}
// Create payment workflow
const instance = await env.MY_WORKFLOW.create({
params: {
orderId: 'ORD-' + Date.now(),
amount: 99.99,
customerId: 'CUST-123'
}
});
return Response.json({
id: instance.id,
status: await instance.status()
});
}
};

View File

@@ -0,0 +1,171 @@
/**
* Complete Wrangler Configuration for Workflows
*
* This file shows all configuration options for Cloudflare Workflows.
* Copy and adapt to your project's needs.
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-workflow-project",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-22",
/**
* Workflows Configuration
*
* Define workflows that can be triggered from Workers.
* Each workflow binding makes a workflow class available via env.BINDING_NAME
*/
"workflows": [
{
/**
* name: The name of the workflow (used in dashboard, CLI commands)
* This is the workflow identifier for wrangler commands like:
* - npx wrangler workflows instances list my-workflow
* - npx wrangler workflows instances describe my-workflow <id>
*/
"name": "my-workflow",
/**
* binding: The name used in your code to access this workflow
* Available in Workers as: env.MY_WORKFLOW
*/
"binding": "MY_WORKFLOW",
/**
* class_name: The exported class name that extends WorkflowEntrypoint
* Must match your TypeScript class:
* export class MyWorkflow extends WorkflowEntrypoint { ... }
*/
"class_name": "MyWorkflow"
},
/**
* Example: Multiple workflows in one project
*/
{
"name": "payment-workflow",
"binding": "PAYMENT_WORKFLOW",
"class_name": "PaymentWorkflow"
},
{
"name": "approval-workflow",
"binding": "APPROVAL_WORKFLOW",
"class_name": "ApprovalWorkflow"
}
/**
* Example: Workflow in different Worker script
*
* If your workflow is defined in a separate Worker script,
* use script_name to reference it
*/
// {
// "name": "external-workflow",
// "binding": "EXTERNAL_WORKFLOW",
// "class_name": "ExternalWorkflow",
// "script_name": "workflow-worker" // Name of Worker that contains the workflow
// }
],
/**
* Optional: Other Cloudflare bindings
*/
// KV Namespace
// "kv_namespaces": [
// {
// "binding": "MY_KV",
// "id": "YOUR_KV_ID"
// }
// ],
// D1 Database
// "d1_databases": [
// {
// "binding": "DB",
// "database_name": "my-database",
// "database_id": "YOUR_DB_ID"
// }
// ],
// R2 Bucket
// "r2_buckets": [
// {
// "binding": "MY_BUCKET",
// "bucket_name": "my-bucket"
// }
// ],
// Queues
// "queues": {
// "producers": [
// {
// "binding": "MY_QUEUE",
// "queue": "my-queue"
// }
// ]
// },
/**
* Optional: Environment variables
*/
// "vars": {
// "ENVIRONMENT": "production",
// "API_URL": "https://api.example.com"
// },
/**
* Optional: Observability
*/
"observability": {
"enabled": true
},
/**
* Optional: Multiple environments
*/
"env": {
"staging": {
"vars": {
"ENVIRONMENT": "staging"
},
"workflows": [
{
"name": "my-workflow-staging",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}
]
},
"production": {
"vars": {
"ENVIRONMENT": "production"
},
"workflows": [
{
"name": "my-workflow-production",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}
]
}
}
}
/**
* Deployment Commands:
*
* # Deploy to default (production)
* npx wrangler deploy
*
* # Deploy to staging environment
* npx wrangler deploy --env staging
*
* # List workflow instances
* npx wrangler workflows instances list my-workflow
*
* # Get instance status
* npx wrangler workflows instances describe my-workflow <instance-id>
*/