Ploy
Ploy
Features

Cron Triggers

Schedule your workers to run on a recurring basis using cron expressions.

Cron Triggers

Ploy Cron Triggers let you schedule your workers to run automatically on a recurring basis. Define cron expressions in your ploy.yaml and implement a scheduled handler to process them.

Configuration

Add cron triggers in your ploy.yaml:

ploy.yaml
kind: worker
build: pnpm build
out: dist
cron:
  EVERY_10_SECONDS: "*/10 * * * * *"
  EVERY_MINUTE: "* * * * *"
  HOURLY_CLEANUP: "0 * * * *"
  DAILY_REPORT: "0 9 * * *"

Each key is a trigger name (uppercase with underscores) and the value is a cron expression. The standard form has 5 fields:

# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday)
# │ │ │ │ │
# * * * * *

For sub-minute schedules, use the extended 6-field form with a leading seconds field:

# ┌───────────── second (0-59)
# │ ┌───────────── minute (0-59)
# │ │ ┌───────────── hour (0-23)
# │ │ │ ┌───────────── day of month (1-31)
# │ │ │ │ ┌───────────── month (1-12)
# │ │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday)
# │ │ │ │ │ │
# * * * * * *

A 5-field expression fires at second 0 of every matching minute, so existing schedules behave exactly as before.

Basic Example

src/index.ts
export default {
	async fetch(request, env) {
		return new Response("Hello from cron worker!");
	},

	async scheduled(event) {
		console.log(`Cron triggered: ${event.cron}`);
		console.log(
			`Scheduled time: ${new Date(event.scheduledTime).toISOString()}`,
		);

		// Your scheduled logic here
		await performCleanup();
	},
} satisfies Ploy;

ScheduledEvent

The scheduled handler receives a ScheduledEvent with the following properties:

interface ScheduledEvent {
	/** The cron expression that triggered this execution */
	cron: string;
	/** Timestamp when the event was scheduled (ms since epoch) */
	scheduledTime: number;
	/** Call to signal this invocation should not be retried */
	noRetry: () => void;
}

Cron Expression Syntax

Standard 5-field cron expressions and extended 6-field expressions (with a leading seconds field) are supported:

ExpressionDescription
*/10 * * * * *Every 10 seconds
*/30 * * * * *Every 30 seconds
0 * * * * *Every minute at second 0
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Every day at midnight
0 9 * * 1-5Weekdays at 9 AM
0 0 1 * *First day of every month
30 2 * * 0Sundays at 2:30 AM

Supported Syntax

  • Wildcards: * matches all values
  • Specific values: 5 matches exactly 5
  • Ranges: 1-5 matches 1 through 5
  • Steps: */15 matches every 15th value
  • Lists: 1,3,5 matches 1, 3, and 5
  • Combined: 1-10/2 matches 1, 3, 5, 7, 9
  • Seconds: prefix with a sixth leading field for sub-minute schedules (e.g. */10 * * * * *)

Multiple Triggers

You can define multiple cron triggers, each identified by a unique name:

ploy.yaml
cron:
  CLEANUP: "0 * * * *"
  DAILY_DIGEST: "0 9 * * *"
  WEEKLY_REPORT: "0 10 * * 1"

Use the event.cron property to distinguish which trigger fired:

async scheduled(event) {
  switch (event.cron) {
    case "0 * * * *":
      await runCleanup();
      break;
    case "0 9 * * *":
      await sendDailyDigest();
      break;
    case "0 10 * * 1":
      await generateWeeklyReport();
      break;
  }
}

Using with Next.js

Next.js apps deploy the framework server as the worker module, so the scheduled handler comes from your Ploy handlers file instead. Export it from ploy.ts (project root, src/, or app/):

app/ploy.ts
import type { Ploy } from "@meetploy/types";

export default {
	async scheduled(event, env, ctx) {
		console.log(`Cron triggered: ${event.cron}`);
		// Your scheduled logic here
	},
} satisfies Ploy;

If your project pairs the Next.js frontend with a worker entry at worker/index.ts (the same convention the Vite integration uses), its default export is picked up automatically when no ploy.ts exists — a scheduled handler exported there receives cron triggers in production too.

Using with the Start SDK

If you're using @meetploy/start, register a scheduled handler with .scheduled():

src/index.ts
import { ploy } from "@meetploy/start";

const app = ploy<PloyEnv>()
	.get(
		"/",
		{
			response: z.object({ status: z.string() }),
		},
		() => ({ status: "ok" }),
	)
	.scheduled(async (event, env, ctx) => {
		console.log(`Cron: ${event.cron}`);
		// Your scheduled logic
	})
	.build();

export default app;

Combining with Other Bindings

Cron triggers work alongside other Ploy bindings. Use queues, databases, and caches from your scheduled handler:

ploy.yaml
kind: worker
build: pnpm build
out: dist
db:
  DB: default
queue:
  TASKS: tasks
cron:
  HOURLY_SYNC: "0 * * * *"
async scheduled(event, env) {
  // Query your database
  const staleRecords = await env.DB.prepare(
    "SELECT id FROM records WHERE updated_at < ?"
  ).bind(Date.now() - 86400000).all();

  // Queue work for processing
  for (const record of staleRecords.results) {
    await env.TASKS.send({ action: "refresh", recordId: record.id });
  }
}

The scheduled handler has access to the same env bindings as your fetch handler.

Local Development

When running the emulator with ploy dev, cron triggers are automatically scheduled based on your ploy.yaml configuration. The emulator checks cron expressions every second, so sub-minute (6-field) schedules fire on time, and invokes your scheduled handler when a match occurs.

Cron execution history is visible in the dev dashboard.

Next Steps

  • Queues - Process background jobs
  • Workflows - Orchestrate multi-step processes
  • Workers - Learn about Ploy workers

How is this guide?

Last updated on