Work that happens while you sleep.
The nightly digest, the reminder before the booking, the slow job nobody should have to watch a spinner for. Declared in your project, run by the platform, with no queue service and no cron server of your own.
You ask once. It runs forever.
A schedule is two lines of config pointed at one handler. Your agent writes both, deploys, and the job is live. Nothing is clicked into a dashboard and nothing lives outside your project.
# two keys, and only these two.
# the parser ignores anything else.
[[cron]]
path = "/api/jobs/daily-digest"
schedule = "0 13 * * *" # 13:00 UTC
import { db, email } from 'hatchable';
// 404 to the public internet. only the
// scheduler can reach this route.
export const access = 'scheduler';
export default async function (req, res) {
const isCron =
req.headers['x-hatchable-trigger'] === 'cron';
const { rows } = await db.query(
`SELECT email FROM subscribers
WHERE digest_enabled = true
AND (last_digest_at IS NULL
OR last_digest_at < now() - interval '23 hours')
ORDER BY id
LIMIT 8`
);
for (const sub of rows) {
await email.send({
to: sub.email,
subject: 'Your daily digest',
html: renderDigest(sub),
});
await db.query(
'UPDATE subscribers SET last_digest_at = now() WHERE email = $1',
[sub.email]
);
}
res.json({ sent: rows.length, cron: isCron });
}
The background half of a real app.
Most apps are only half request and response. The other half runs on a clock, or after the visitor has already been given their answer. Both halves are here, in every project.
Schedules that live in your project
A recurring job is a [[cron]] block in your config file: one route, one schedule, standard five-field cron. It ships with the deploy, and the schedule is right there in the file list where anyone can read it.
Change the hour, redeploy, done. No console to hunt through.
Answer now, work later
A handler can hand a slow job off and return immediately, so the person on the screen gets a response in milliseconds instead of watching a progress bar. The work runs out of band, seconds later, with a much longer budget.
Upload finishes instantly. The transcoding happens behind it.
Nobody can call it but the platform
A job handler declares itself scheduler-only and then returns 404 to the entire internet, signed in or not. There is no secret URL to leak and no token to rotate, because there is no door.
The nightly billing job is not a URL anyone can find.
One clock, for everybody
Every schedule is UTC. No project time zone to set, no daylight saving to reason about, no argument about whose morning it is. Repeat firings are spread within the hour so that thousands of jobs on the same schedule do not all land on the same second.
Think of a schedule as the hour it runs in, not the second.
Big work, one bite at a time
Each fire has about 65 seconds, which is short on purpose: it keeps a stuck job from pinning anything. Long work is written to take a fixed batch, save its progress, and queue itself again for the next run.
Summarising forty thousand old posts finishes overnight, fifty at a time.
The digest that sends itself
Query, then send to each person who is due. It is the shape behind every weekly summary and monthly report. Sending is rate limited per minute for deliverability, so a large list is meant to drain across several runs rather than one burst.
See how email works for the sending side.
Ask what is scheduled
The connection that built your app can list every job in the project, with the schedule, the next firing time, and whether the last run failed. You get the answer in the chat, without going to look for it.
"Is the Monday report still running?" comes back as a real answer.
A tab that shows the truth
Projects with schedules get a Schedule tab: every job, when it fires next, when it last fired, how many times it has run, and the last error if there was one. Apps with no background work never see it.
One glance tells you whether last night's job actually ran.
The spec sheet, in plain terms.
[[cron]] block in hatchable.toml with two keys, path and schedule. A route file can also carry its own export const schedule.0 9 * * * is every day at 09:00, 0 13 * * 1 is Monday at 13:00.access = 'scheduler' returns 404 to every request from the public internet. The scheduler invokes it from inside the platform.x-hatchable-trigger: cron header, so one handler can serve both the clock and a "run it now" button.scheduler.at for a moment or a repeating expression, scheduler.now to run something immediately off the request, scheduler.cancel to call one off.Numbers current as of August 2026 and enforced by the same deploy validator your project is checked against.
Say it like this.
You never write a cron expression. These are ordinary sentences that turn into a schedule, a handler, and a deploy.
Asked and answered.
How often can a scheduled job run?
Once an hour is the fastest recurring schedule, and that is true on every plan, paid ones included. A schedule like */15 * * * * is rejected when you deploy, with an error that says exactly why. If you need something tighter, do it from your own code: a handler can hand work off to run in seconds, and can re-arm itself as often as it likes.
What time zone do schedules use?
UTC, always. A job set for 13:00 fires at 13:00 UTC no matter where you or your visitors are. There is no per-project time zone setting. If a local morning matters, pick the UTC hour that lands closest, or fan out by time zone inside the handler.
My cron block is in the config but nothing fires.
Check the key name. The parser reads path and schedule, and nothing else. A block written with route = instead of path = parses without complaint, produces no error, and never fires. It is the first thing to check, and the easiest to miss.
Can someone on the internet trigger my scheduled job?
No. A handler marked scheduler-only returns 404 to every request that arrives over the public internet, signed in or not. The scheduler reaches it from inside the platform, on a path that never touches the front door. That 404 is the security model, not a bug.
What if the job takes longer than a minute?
A scheduled fire gets roughly 65 seconds. That is deliberate. The answer is to process a fixed batch each time and let the handler queue the remainder for the next run, which is a pattern your AI already knows. Work that genuinely needs one long stretch can be deferred from a live request instead, where the budget is several minutes.
Can I see what is scheduled?
Yes, two ways. A project with schedules gets a Schedule tab in the console listing every job, when it fires next, when it last fired, how many times it has run, and the last error if there was one. Your AI can pull the same list over the project connection and read it back to you in the chat.
More detail lives in the config reference and the SDK reference.
Nothing to run, nothing to babysit.
Connect the AI you already use, describe the thing that should happen every morning, and it happens every morning.
Want the technical surface? The config reference covers the [[cron]] block and everything else in hatchable.toml.