**Status**: Production Ready ✅
/plugin marketplace add secondsky/claude-skills/plugin install cloudflare-cron-triggers@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/common-patterns.mdreferences/cron-expressions-reference.mdreferences/integration-patterns.mdreferences/testing-guide.mdreferences/wrangler-config.mdtemplates/basic-scheduled.tstemplates/hono-with-scheduled.tstemplates/multiple-crons.tstemplates/scheduled-with-bindings.tstemplates/wrangler-cron-config.jsoncStatus: Production Ready ✅ Last Updated: 2025-11-25 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: wrangler@4.50.0, @cloudflare/workers-types@4.20251125.0
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
bunx wrangler dev --test-scheduled
# In another terminal, trigger the scheduled handler
curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*"
# View output in wrangler dev terminal
Testing tips:
/__scheduled endpoint is only available with --test-scheduled flag/cdn-cgi/handler/scheduled insteadnpm run deploy
# or
bunx wrangler deploy
After deployment:
Load immediately when user mentions:
cron-expressions-reference.md → "cron syntax", "schedule format", "expression", "minute hour day", "every X minutes"common-patterns.md → "examples", "use cases", "patterns", "real-world", "database cleanup", "report generation", "how to"integration-patterns.md → "implement", "Hono", "multiple triggers", "bindings", "workflows", "error handling"wrangler-config.md → "configuration", "wrangler.jsonc", "multiple crons", "environment-specific", "dev staging production"testing-guide.md → "test", "local development", "__scheduled", "unit test", "curl", "debugging"Load proactively when:
integration-patterns.mdwrangler-config.mdcron-expressions-reference.mdtesting-guide.mdcommon-patterns.md* * * * *
│ │ │ │ │
│ │ │ │ └─── 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-5
CRITICAL: 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.
6 production-ready cron patterns:
controller.cron to route executionLoad references/integration-patterns.md for complete implementations with code examples, configuration details, and best practices.
Add cron triggers to wrangler.jsonc in the triggers.crons array. Each trigger requires a cron expression. Supports multiple crons (Free: 3 max, Paid: higher limits) and environment-specific configurations for dev/staging/production deployments.
Load references/wrangler-config.md for complete configuration examples including multiple triggers, environment-specific schedules, timezone handling, and removal procedures.
Test scheduled functions locally using the /__scheduled endpoint by running bunx wrangler dev --test-scheduled, then triggering handlers with curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*" (use + instead of spaces in cron expressions).
Load references/testing-guide.md for complete testing strategies, local development setup, unit testing examples, integration testing patterns, and production monitoring techniques.
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:
bunx wrangler triggers deploy
# Wait 15 minutes, then verify in dashboard
Error: 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-7
Tools:
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 UTC
Validation:
--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 firstLoad references/common-patterns.md for 10 real-world cron patterns including database cleanup, API data collection, daily reports generation, cache warming, monitoring & health checks, data synchronization, backup automation, sitemap generation, webhook processing, and scheduled notifications.
// 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
bunx wrangler deploy
# Wait 15 minutes
# Check dashboard
# Workers & Pages > [Worker] > Cron Triggers
# Check logs
# Workers & Pages > [Worker] > Logs > Real-time Logs
Possible 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
bunx 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: Claude Skills Maintainers | maintainers@example.com