Scheduled tasks run your code on a cron schedule without a request coming in. How you write them depends on the app's delivery mode: apps that run on the edge network call a scheduled() handler in your Worker code, and container apps run the Laravel scheduler or a command you name.
Schedules and where they come from
Schedules come from two places, merged on each deploy:
- Overview → Add resource → Scheduled task. Add as many as you need (up to the limit below); rows you add there are stored on the app.
- A
cronslist indply.yaml, committed with your code. These show on Overview as read-only, marked Repo.
crons:
- schedule: "*/5 * * * *"
- schedule: "0 3 * * *"
handler: "reports:send --daily"
A schedule is a standard 5-field cron expression in UTC. In the form, pick Minutes, Hourly, Daily, Weekly or Monthly and a time to build one, or choose Custom and type any expression. Changes take effect on the next deploy. Once an app has a task, a Scheduled tasks box appears on the Overview map under the app. Select it to see every schedule, Edit or Run now one, or Add another. Scheduled tasks are available once the app has a Worker to schedule: a container app, a Worker SSR app, or a static or hybrid app with edge middleware. Static apps with middleware have no Add resource button, so they get an Add a scheduled task link under the app box instead.
How many you can have
- Container apps: up to 50 scheduled tasks. Every task shares one trigger that fires each minute. The app's Worker checks which tasks are due and wakes the app only for those, so a task that runs at 06:00 wakes the app at 06:00 and not before. The Laravel scheduler doesn't take a slot.
- Worker SSR, hybrid, and static apps with edge middleware: up to 5 distinct schedules. Each schedule is its own Cloudflare Cron Trigger, and Cloudflare allows 5 per Worker. Your
scheduled()handler tells them apart bycontroller.cron. The sheet shows N of 5 schedules used. Schedules past the fifth are dropped at deploy and marked Won't run. Several commands can share one schedule.
Note
On container apps a schedule can use numbers, *, ranges (1-5), lists (1,15), steps (*/10), and month and weekday names (JAN, MON). Cloudflare's extras L, W, # and ? aren't supported: the form refuses them, and one in dply.yaml is marked Won't run.
Preview deployments never run scheduled tasks; only production does.
Static and hybrid apps
A static or hybrid app runs schedules through its edge middleware. Export a scheduled handler from src/middleware.ts (or middleware.ts at the repository root):
// src/middleware.ts
export default {
async fetch(request, env) {
// Let the request continue to your static files or origin.
return new Response(null, {
status: 204,
headers: { "X-Dply-Middleware": "continue" },
});
},
async scheduled(controller, env, ctx) {
// controller.cron is the expression that fired
console.log("cron", controller.cron);
},
};
Every schedule calls the same scheduled export, so there is no command field for these apps; branch on controller.cron. Your environment variables are available on env. Without middleware there is nothing to schedule.
Worker SSR apps
Each schedule calls the scheduled export of your app's Worker entry. Next.js (OpenNext), Astro, SvelteKit and Remix builds do not export one by default, so a schedule does nothing until your Worker entry exports scheduled alongside fetch.
Container apps
The Laravel scheduler
To run schedule:run every minute, open Overview, select Add resource → Scheduled task, choose Run Laravel’s scheduler at the top, and redeploy. You can also turn on Run the Laravel scheduler every minute in Sleep, scaling, region. dply adds the dply/laravel package to the image if your app does not require it (add it yourself if you ship your own Dockerfile).
Where the scheduler runs depends on whether the app has queue workers:
| App has | The scheduler runs | Effect on sleep |
|---|---|---|
| At least one always-on queue worker | php artisan schedule:work inside worker-0 |
The web instances can sleep. Pausing the workers pauses the scheduler. |
| No always-on worker | A trigger every minute calls schedule:run in the app |
The app is woken only when one of your tasks is due. |
After each run, the app reports its tasks' cron expressions and time zones, and dply wakes the app only in minutes when one of them is due. An app with only a nightly task sleeps the rest of the day. dply wakes the app every minute when it cannot tell: right after a deploy, when the report is more than a day old, or when the app has sub-minute tasks.
Your tasks use the time zones set in your schedule (->timezone() or schedule_timezone), not UTC.
Select the Scheduler card on Overview (or the Queue workers card when the app has workers) to see which mode is active. Run now runs schedule:run once in the live app and shows its output. Remove turns the scheduler off on the next deploy.
Commands on a schedule
For anything other than the Laravel scheduler, add a Scheduled task on Overview (or in dply.yaml) with a command. Each run POSTs the command to /_dply/schedule in the live app.
On a Laravel or Rails container app, choose Pick from the app's artisan commands (or rake tasks) under Command to list what the live app defines. Your app's own commands are listed first; typing in the field suggests the rest. Asking wakes the app if it is asleep.
How the command runs:
- Laravel: runs through
dply/laravel. An artisan command such asreports:send --dailyruns through Artisan (a leadingphp artisanis optional). Anything else runs as a shell command from the app root, for examplephp scripts/cleanup.phpornode bin/sync.js. A blank handler runsschedule:run. - Rails: runs through
dply-rails. A rake task such asreports:dailyruns through Rake. Anything else runs as a shell command from the app root, for examplebin/rails runner Cleanup.call. - Node: your app handles
POST /_dply/scheduleitself. The body is JSON{cron, handler}; check that thex-dply-queue-tokenheader equalsDPLY_QUEUE_TOKENfrom the app's environment, and answer JSON{output}:
app.post("/_dply/schedule", express.json(), async (req, res) => {
if (req.get("x-dply-queue-token") !== process.env.DPLY_QUEUE_TOKEN) {
return res.status(403).json({ error: "Forbidden" });
}
const { handler, cron } = req.body; // e.g. "reports:daily", "0 6 * * *"
// run the task for `handler` here
res.json({ output: `ran ${handler}` });
});
Run now next to a listed command runs it once in the live app and shows the output, for Laravel, Rails and Node apps alike. Only commands already on the list can run, and you need edit rights. Rails tasks print no output; a Node app without the route gets a message saying so.
Each run wakes the app if it is asleep. With Run queued jobs and scheduled tasks on their own instance on in Overview → App card → Sleep, scaling, region, scheduled runs go to a separate jobs instance instead of a web instance.
When a run does not happen
- Scheduled tasks stop while the organization is paused. See Paused accounts.
- A schedule added in the dashboard does nothing until the next deploy.
- A static or hybrid app needs middleware that exports
scheduled. - Check that the schedule is within the first five.