cloudflare-cron-triggers — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cloudflare-cron-triggers (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Status: Production Ready ✅ Last Updated: 2025-10-23 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: [email protected], @cloudflare/[email protected]
src/index.ts:
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Cron job executed at:', new Date(controller.scheduledTime));
console.log('Triggered by cron:', controller.cron);
// Your scheduled task logic here
await doPeriodicTask(env);
},
};Why this matters:
scheduled (not scheduledHandler or onScheduled)wrangler.jsonc:
{
"name": "my-scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-23",
"triggers": {
"crons": [
"0 * * * *" // Every hour at minute 0
]
}
}CRITICAL:
minute hour day-of-month month day-of-week# Enable scheduled testing
npx wrangler dev --test-scheduled
# In another terminal, trigger the scheduled handler
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# View output in wrangler dev terminalTesting tips:
/__scheduled endpoint is only available with --test-scheduled flag/cdn-cgi/handler/scheduled insteadnpm run deploy
# or
npx wrangler deployAfter deployment:
* * * * *
│ │ │ │ │
│ │ │ │ └─── Day of Week (0-6, Sunday=0)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of Month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)| Character | Meaning | Example |
|---|---|---|
* | Every | * * * * * = every minute |
, | List | 0,30 * * * * = every hour at :00 and :30 |
- | Range | 0 9-17 * * * = every hour from 9am-5pm |
/ | Step | */15 * * * * = every 15 minutes |
# Every minute
* * * * *
# Every 5 minutes
*/5 * * * *
# Every 15 minutes
*/15 * * * *
# Every hour at minute 0
0 * * * *
# Every hour at minute 30
30 * * * *
# Every 6 hours
0 */6 * * *
# Every day at midnight (00:00 UTC)
0 0 * * *
# Every day at noon (12:00 UTC)
0 12 * * *
# Every day at 3:30am UTC
30 3 * * *
# Every Monday at 9am UTC
0 9 * * 1
# Every weekday at 9am UTC
0 9 * * 1-5
# Every Sunday at midnight UTC
0 0 * * 0
# First day of every month at midnight UTC
0 0 1 * *
# Twice a day (6am and 6pm UTC)
0 6,18 * * *
# Every 30 minutes during business hours (9am-5pm UTC, weekdays)
*/30 9-17 * * 1-5CRITICAL: UTC Timezone Only
interface ScheduledController {
readonly cron: string; // The cron expression that triggered this execution
readonly type: string; // Always "scheduled"
readonly scheduledTime: number; // Unix timestamp (ms) when scheduled
}#### controller.cron (string)
The cron expression that triggered this execution.
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
console.log(`Triggered by: ${controller.cron}`);
// Output: "Triggered by: 0 * * * *"
},
};Use case: Differentiate between multiple cron schedules (see Multiple Cron Triggers pattern).
#### controller.type (string)
Always returns "scheduled" for cron-triggered executions.
if (controller.type === 'scheduled') {
// This is a cron-triggered execution
}#### controller.scheduledTime (number)
Unix timestamp (milliseconds since epoch) when this execution was scheduled to run.
export default {
async scheduled(controller: ScheduledController): Promise<void> {
const scheduledDate = new Date(controller.scheduledTime);
console.log(`Scheduled for: ${scheduledDate.toISOString()}`);
// Output: "Scheduled for: 2025-10-23T15:00:00.000Z"
},
};Note: This is the scheduled time, not the actual execution time. Due to system load, actual execution may be slightly delayed (usually <1 second).
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext // ← Execution context
): Promise<void> {
// Use ctx.waitUntil() for async operations that should complete
ctx.waitUntil(logToAnalytics(env));
},
};ctx.waitUntil(promise: Promise<any>)Extends the execution context to wait for async operations to complete after the handler returns.
Use cases:
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
// Critical task - must complete before handler exits
await processData(env);
// Non-critical tasks - can complete in background
ctx.waitUntil(sendMetrics(env));
ctx.waitUntil(cleanupOldData(env));
ctx.waitUntil(notifySlack({ message: 'Cron completed' }));
},
};Important: First waitUntil() that fails will be reported as the status in dashboard logs.
Best for: Workers that only run on schedule (no HTTP requests)
// src/index.ts
interface Env {
DB: D1Database;
MY_BUCKET: R2Bucket;
}
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Running scheduled maintenance...');
// Database cleanup
await env.DB.prepare('DELETE FROM sessions WHERE expires_at < ?')
.bind(Date.now())
.run();
// Generate daily report
const report = await generateDailyReport(env.DB);
// Upload to R2
await env.MY_BUCKET.put(
`reports/${new Date().toISOString().split('T')[0]}.json`,
JSON.stringify(report)
);
console.log('Maintenance complete');
},
};Best for: Workers that handle both HTTP requests and scheduled tasks
// src/index.ts
import { Hono } from 'hono';
interface Env {
DB: D1Database;
}
const app = new Hono<{ Bindings: Env }>();
// Regular HTTP routes
app.get('/', (c) => c.text('Worker is running'));
app.get('/api/stats', async (c) => {
const stats = await c.env.DB.prepare('SELECT COUNT(*) as count FROM users').first();
return c.json(stats);
});
// Export both fetch handler and scheduled handler
export default {
// Handle HTTP requests
fetch: app.fetch,
// Handle cron triggers
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Cron triggered:', controller.cron);
// Run scheduled task
await updateCache(env.DB);
// Log completion
ctx.waitUntil(logExecution(controller.scheduledTime));
},
};Why this pattern:
Best for: Different schedules for different tasks
wrangler.jsonc:
{
"triggers": {
"crons": [
"*/5 * * * *", // Every 5 minutes
"0 */6 * * *", // Every 6 hours
"0 0 * * *" // Daily at midnight UTC
]
}
}src/index.ts:
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
// Route based on which cron triggered this execution
switch (controller.cron) {
case '*/5 * * * *':
// Every 5 minutes: Check system health
await checkSystemHealth(env);
break;
case '0 */6 * * *':
// Every 6 hours: Sync data from external API
await syncExternalData(env);
break;
case '0 0 * * *':
// Daily at midnight: Generate reports and cleanup
await generateDailyReports(env);
await cleanupOldData(env);
break;
default:
console.warn(`Unknown cron trigger: ${controller.cron}`);
}
},
};CRITICAL:
All Worker bindings available in scheduled handler:
interface Env {
// Databases
DB: D1Database;
// Storage
MY_BUCKET: R2Bucket;
KV_NAMESPACE: KVNamespace;
// AI & Vectors
AI: Ai;
VECTOR_INDEX: VectorizeIndex;
// Queues & Workflows
MY_QUEUE: Queue;
MY_WORKFLOW: Workflow;
// Durable Objects
RATE_LIMITER: DurableObjectNamespace;
// Secrets
API_KEY: string;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// D1 Database
const users = await env.DB.prepare('SELECT * FROM users WHERE active = 1').all();
// R2 Storage
const file = await env.MY_BUCKET.get('data.json');
// KV Storage
const config = await env.KV_NAMESPACE.get('config', 'json');
// Workers AI
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Summarize today\'s data',
});
// Send to Queue
await env.MY_QUEUE.send({ type: 'process', data: users.results });
// Trigger Workflow
await env.MY_WORKFLOW.create({ input: { timestamp: Date.now() } });
// Use secrets
await fetch('https://api.example.com/webhook', {
headers: { Authorization: `Bearer ${env.API_KEY}` },
});
},
};Best for: Multi-step, long-running tasks triggered on schedule
wrangler.jsonc:
{
"triggers": {
"crons": ["0 2 * * *"] // Daily at 2am UTC
},
"workflows": [
{
"name": "daily-report-workflow",
"binding": "DAILY_REPORT"
}
]
}src/index.ts:
interface Env {
DAILY_REPORT: Workflow;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
console.log('Triggering daily report workflow...');
// Trigger workflow with initial state
const instance = await env.DAILY_REPORT.create({
params: {
date: new Date().toISOString().split('T')[0],
reportType: 'daily-summary',
},
});
console.log(`Workflow started: ${instance.id}`);
},
};Why use Workflows:
Reference: Cloudflare Workflows Docs
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
try {
// Main task
await performScheduledTask(env);
} catch (error) {
// Log error
console.error('Scheduled task failed:', error);
// Send alert
await sendAlert({
worker: 'my-scheduled-worker',
cron: controller.cron,
error: error.message,
timestamp: new Date(controller.scheduledTime).toISOString(),
});
// Store failure in database
ctx.waitUntil(
env.DB.prepare(
'INSERT INTO cron_failures (cron, error, timestamp) VALUES (?, ?, ?)'
)
.bind(controller.cron, error.message, Date.now())
.run()
);
// Re-throw to mark execution as failed
throw error;
}
},
};
async function sendAlert(details: any): Promise<void> {
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 Cron job failed: ${details.worker}`,
blocks: [
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Worker:*\n${details.worker}` },
{ type: 'mrkdwn', text: `*Cron:*\n${details.cron}` },
{ type: 'mrkdwn', text: `*Error:*\n${details.error}` },
{ type: 'mrkdwn', text: `*Time:*\n${details.timestamp}` },
],
},
],
}),
});
}{
"name": "my-scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-23",
"triggers": {
"crons": ["0 * * * *"]
}
}{
"triggers": {
"crons": [
"*/5 * * * *", // Every 5 minutes
"0 */6 * * *", // Every 6 hours
"0 2 * * *", // Daily at 2am UTC
"0 0 * * 1" // Weekly on Monday at midnight UTC
]
}
}Limits:
{
"name": "my-worker",
"main": "src/index.ts",
"env": {
"dev": {
"triggers": {
"crons": ["*/5 * * * *"] // Dev: every 5 minutes for testing
}
},
"staging": {
"triggers": {
"crons": ["*/30 * * * *"] // Staging: every 30 minutes
}
},
"production": {
"triggers": {
"crons": ["0 * * * *"] // Production: hourly
}
}
}
}Deploy specific environment:
# Deploy to dev
npx wrangler deploy --env dev
# Deploy to production
npx wrangler deploy --env production{
"triggers": {
"crons": [] // Empty array removes all crons
}
}After deploy, Worker will no longer execute on schedule.
# Start dev server with scheduled testing enabled
npx wrangler dev --test-scheduledThis exposes /__scheduled endpoint for triggering scheduled handlers.
# Trigger with default cron (if only one configured)
curl "http://localhost:8787/__scheduled"
# Trigger with specific cron expression
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# Trigger with URL-encoded cron
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"Note: Use + instead of spaces in URL, or URL-encode properly.
# Start dev server
npx wrangler dev --test-scheduled
# In another terminal, trigger and watch output
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"Output appears in wrangler dev terminal:
[wrangler:inf] GET /__scheduled?cron=0+*+*+*+* 200 OK (45ms)
Cron job executed at: 2025-10-23T15:00:00.000Z
Triggered by cron: 0 * * * *
Scheduled task completed successfully# Test hourly cron
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# Test daily cron
curl "http://localhost:8787/__scheduled?cron=0+0+*+*+*"
# Test weekly cron
curl "http://localhost:8787/__scheduled?cron=0+0+*+*+1"# Python Workers use different endpoint
curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*"Run cron triggers only in data centers powered by renewable energy.
Via Dashboard:
Applies to:
How it works:
This skill prevents 6 documented issues:
Error: Cron triggers updated in wrangler.jsonc but not executing
Source: Cloudflare Docs - Cron Triggers
Why It Happens:
Prevention:
wrangler triggers deploy for trigger-only changes# If you only changed triggers (not code), use:
npx wrangler triggers deploy
# Wait 15 minutes, then verify in dashboardError: Handler does not export a 'scheduled' method
Source: Common deployment error
Why It Happens:
scheduledPrevention:
// ❌ Wrong: Incorrect handler name
export default {
async scheduledHandler(controller, env, ctx) { }
};
// ❌ Wrong: Not in default export
export async function scheduled(controller, env, ctx) { }
// ✅ Correct: Named 'scheduled' in default export
export default {
async scheduled(controller, env, ctx) { }
};Error: Cron runs at wrong time
Source: User expectation vs. reality
Why It Happens:
Prevention:
Convert your local time to UTC manually:
// Want to run at 9am PST (UTC-8)?
// 9am PST = 5pm UTC (17:00)
{
"triggers": {
"crons": ["0 17 * * *"] // 9am PST = 5pm UTC
}
}
// Want to run at 6pm EST (UTC-5)?
// 6pm EST = 11pm UTC (23:00)
{
"triggers": {
"crons": ["0 23 * * *"] // 6pm EST = 11pm UTC
}
}
// Remember: DST changes affect conversion!
// PST is UTC-8, PDT is UTC-7Tools:
Error: Cron doesn't execute, no error shown
Source: Silent validation failure
Why It Happens:
Prevention:
# ❌ Wrong: Too many fields (6 fields instead of 5)
"crons": ["0 0 * * * *"] # Has seconds field - not supported
# ❌ Wrong: Invalid minute range
"crons": ["65 * * * *"] # Minute must be 0-59
# ❌ Wrong: Invalid day of week
"crons": ["0 0 * * 7"] # Day of week is 0-6 (use 0 for Sunday)
# ✅ Correct: 5 fields, valid ranges
"crons": ["0 0 * * 0"] # Sunday at midnight UTCValidation:
--test-scheduledError: Worker must use ES modules format
Source: Legacy Service Worker format
Why It Happens:
Prevention:
// ❌ Wrong: Service Worker format
addEventListener('scheduled', (event) => {
event.waitUntil(handleScheduled(event));
});
// ✅ Correct: ES modules format
export default {
async scheduled(controller, env, ctx) {
await handleScheduled(controller, env, ctx);
},
};Error: CPU time limit exceeded
Source: Long-running scheduled tasks
Why It Happens:
Prevention:
Option 1: Increase CPU limit in wrangler.jsonc
{
"limits": {
"cpu_ms": 300000 // 5 minutes (max for Standard plan)
}
}Option 2: Use Workflows for long-running tasks
// Instead of long task in cron:
export default {
async scheduled(controller, env, ctx) {
// Trigger Workflow that can run for hours
await env.MY_WORKFLOW.create({
params: { task: 'long-running-job' },
});
},
};Option 3: Break into smaller chunks
export default {
async scheduled(controller, env, ctx) {
// Process in batches
const batch = await getNextBatch(env.DB);
for (const item of batch) {
await processItem(item);
}
// If more work, send to Queue for next batch
const hasMore = await hasMoreWork(env.DB);
if (hasMore) {
await env.MY_QUEUE.send({ type: 'continue-processing' });
}
},
};scheduled, not scheduledHandler or variantswrangler dev --test-scheduled--test-scheduled firstEvery day at 2am UTC: Delete old records
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// Delete sessions older than 30 days
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
await env.DB.prepare('DELETE FROM sessions WHERE created_at < ?')
.bind(thirtyDaysAgo)
.run();
// Delete soft-deleted users older than 90 days
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
await env.DB.prepare('DELETE FROM users WHERE deleted_at < ?')
.bind(ninetyDaysAgo)
.run();
console.log('Database cleanup completed');
},
};wrangler.jsonc:
{
"triggers": {
"crons": ["0 2 * * *"] // Daily at 2am UTC
}
}Every 15 minutes: Fetch data from external API
interface Env {
DB: D1Database;
API_KEY: string;
}
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
try {
// Fetch from external API
const response = await fetch('https://api.example.com/v1/data', {
headers: {
Authorization: `Bearer ${env.API_KEY}`,
},
});
const data = await response.json();
// Store in D1
for (const item of data.items) {
await env.DB.prepare(
'INSERT INTO collected_data (id, value, timestamp) VALUES (?, ?, ?)'
)
.bind(item.id, item.value, Date.now())
.run();
}
console.log(`Collected ${data.items.length} items`);
} catch (error) {
console.error('Failed to collect data:', error);
throw error; // Mark execution as failed
}
},
};wrangler.jsonc:
{
"triggers": {
"crons": ["*/15 * * * *"] // Every 15 minutes
}
}Every day at 8am UTC: Generate and email report
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
// Generate report from database
const report = await generateDailyReport(env.DB);
// Store in R2
const fileName = `reports/${new Date().toISOString().split('T')[0]}.json`;
await env.MY_BUCKET.put(fileName, JSON.stringify(report));
// Send via email
ctx.waitUntil(sendReportEmail(report, env.RESEND_API_KEY));
console.log('Daily report generated and sent');
},
};
async function generateDailyReport(db: D1Database) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const startOfDay = yesterday.setHours(0, 0, 0, 0);
const endOfDay = yesterday.setHours(23, 59, 59, 999);
const stats = await db
.prepare(`
SELECT
COUNT(*) as total_users,
COUNT(DISTINCT user_id) as active_users,
SUM(revenue) as total_revenue
FROM events
WHERE timestamp BETWEEN ? AND ?
`)
.bind(startOfDay, endOfDay)
.first();
return {
date: yesterday.toISOString().split('T')[0],
stats,
};
}wrangler.jsonc:
{
"triggers": {
"crons": ["0 8 * * *"] // Daily at 8am UTC
}
}Every hour: Pre-warm cache with popular content
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
// Get most popular pages from analytics
const popularPages = await env.DB
.prepare('SELECT url FROM pages ORDER BY views DESC LIMIT 100')
.all();
// Fetch each page to warm cache
const requests = popularPages.results.map((page) =>
fetch(`https://example.com${page.url}`, {
cf: {
cacheTtl: 3600, // Cache for 1 hour
},
})
);
await Promise.all(requests);
console.log(`Warmed cache for ${popularPages.results.length} pages`);
},
};wrangler.jsonc:
{
"triggers": {
"crons": ["0 * * * *"] // Every hour
}
}Every 5 minutes: Check system health
export default {
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
const checks = await Promise.allSettled([
checkDatabaseHealth(env.DB),
checkAPIHealth(),
checkStorageHealth(env.MY_BUCKET),
]);
const failures = checks.filter((check) => check.status === 'rejected');
if (failures.length > 0) {
// Send alert
await sendAlert({
service: 'health-check',
failures: failures.map((f) => f.reason),
timestamp: new Date().toISOString(),
});
}
},
};
async function checkDatabaseHealth(db: D1Database): Promise<void> {
const result = await db.prepare('SELECT 1 as health').first();
if (!result || result.health !== 1) {
throw new Error('Database health check failed');
}
}
async function checkAPIHealth(): Promise<void> {
const response = await fetch('https://api.example.com/health');
if (!response.ok) {
throw new Error(`API health check failed: ${response.status}`);
}
}
async function checkStorageHealth(bucket: R2Bucket): Promise<void> {
const testObject = await bucket.get('health-check.txt');
if (!testObject) {
throw new Error('Storage health check failed');
}
}wrangler.jsonc:
{
"triggers": {
"crons": ["*/5 * * * *"] // Every 5 minutes
}
}// Scheduled event controller
interface ScheduledController {
readonly cron: string;
readonly type: string;
readonly scheduledTime: number;
}
// Execution context
interface ExecutionContext {
waitUntil(promise: Promise<any>): void;
passThroughOnException(): void;
}
// Scheduled handler
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void>;
}| Feature | Free Plan | Paid Plan |
|---|---|---|
| Cron triggers per Worker | 3 | Higher (check docs) |
| CPU time per execution | 10 ms (avg) | 30 seconds (default), 5 min (max) |
| Wall clock time | 30 seconds | 15 minutes |
| Memory | 128 MB | 128 MB |
Cron triggers use Standard Workers pricing:
Cron execution = 1 request
Example:
Cost:
High frequency example:
Possible causes:
Solution:
# Re-deploy
npx wrangler deploy
# Wait 15 minutes
# Check dashboard
# Workers & Pages > [Worker] > Cron Triggers
# Check logs
# Workers & Pages > [Worker] > Logs > Real-time LogsPossible causes:
Solution:
export default {
async scheduled(controller, env, ctx) {
try {
await yourTask(env);
} catch (error) {
// Log detailed error
console.error('Handler failed:', {
error: error.message,
stack: error.stack,
cron: controller.cron,
time: new Date(controller.scheduledTime),
});
// Send alert
ctx.waitUntil(sendAlert(error));
// Re-throw to mark as failed
throw error;
}
},
};Check logs in dashboard for error details.
Cause: UTC vs. local timezone confusion
Solution:
Convert your desired local time to UTC:
// Want 9am PST (UTC-8)?
// 9am PST = 5pm UTC (17:00)
{
"triggers": {
"crons": ["0 17 * * *"]
}
}Tools:
Possible causes:
--test-scheduled flag/__scheduled)/cdn-cgi/handler/scheduled)Solution:
# Correct: Start with flag
npx wrangler dev --test-scheduled
# In another terminal
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"Before deploying cron triggers to production:
scheduled in default exportlimits.cpu_ms)--test-scheduledLast Updated: 2025-10-23 Version: 1.0.0 Maintainer: Jeremy Dawes | [email protected]
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.