Initial commit
This commit is contained in:
136
templates/basic-workflow.ts
Normal file
136
templates/basic-workflow.ts
Normal 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}`
|
||||
});
|
||||
}
|
||||
};
|
||||
252
templates/scheduled-workflow.ts
Normal file
252
templates/scheduled-workflow.ts
Normal 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
287
templates/worker-trigger.ts
Normal 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;
|
||||
335
templates/workflow-with-events.ts
Normal file
335
templates/workflow-with-events.ts
Normal 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'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
235
templates/workflow-with-retries.ts
Normal file
235
templates/workflow-with-retries.ts
Normal 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()
|
||||
});
|
||||
}
|
||||
};
|
||||
171
templates/wrangler-workflows-config.jsonc
Normal file
171
templates/wrangler-workflows-config.jsonc
Normal 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>
|
||||
*/
|
||||
Reference in New Issue
Block a user