Schedules
You declare a Schedule in wawesome-function.json and a deploy applies it. No command creates one,
and the dashboard does not either. A Schedule runs a Function on a timer with nobody waiting for the
answer.
{
"app": "scheduled-job",
"function": "health-check",
"visibility": "private",
"schedules": [
{ "name": "health-check", "expression": "*/5 * * * *" },
{ "name": "nightly-reconcile", "expression": "0 3 * * *" }
]
}
The expression is five fields, read in UTC. A sixth field is seconds, and it is turned down. You
cannot ask for a per-second timer here. Two runs may be no closer together than five minutes, and one
Function may carry five Schedules. An expression naming a date that never occurs, like 0 0 30 2 *,
is turned down at the deploy rather than left on the Function firing never.
The deploy prints every Schedule it applied, including the ones that are not running, so putting recurring compute on the bill is never silent:
Schedules:
health-check */5 * * * * next run 2026-09-08 09:15 UTC
nightly-reconcile 0 3 * * * paused. Resume it to run again
Schedules need a paid plan
Section titled “Schedules need a paid plan”The free plan grants none. Declaring one on it is turned down at the deploy, before a byte is compiled or stored. You meet it at your keyboard rather than at three in the morning:
[wawesome] Error: Schedule 'health-check' needs the paid plan. A schedule runs on the platform's own time with nobody waiting for the answer, which is why the paid plan grants it and the free plan does not. Move to the paid plan to keep it, or take it out of the configuration file to deploy without it.
[wawesome] Where to resolve it: https://dashboard.wawesome.io/billing
A file carrying no Schedule is not asking for one, so a free workspace deploys as it always did.
A workspace that holds a Schedule and then drops off a paid plan has it suspended within a few
minutes. It enqueues nothing. Payment that stopped arriving gets it there. So does a subscription
cancelled while paid up, and so does spending far past what the plan allows. We do not record which
of the three it was. The way out is the same either way, and a workspace that owes nothing should
not be sent to its card details.
Suspension is the one state neither hand can undo. Pause and resume both turn it down:
[wawesome] Error: Schedule 'health-check' is suspended by the platform: the workspace owes payment, its subscription ended, or it has spent far past what its plan allows. Neither pausing nor resuming reaches it — settling up, subscribing again or upgrading ends the suspension, as does the start of the next usage period, and it starts again on its own within a few minutes.
Settling up or subscribing again starts the job on its own, with no redeploy. The next run counts from the moment the workspace was back on a paid plan, so nothing from the suspension is made up. A Schedule a person paused stays paused through the suspension and through the settlement that ends it.
What the next deploy does
Section titled “What the next deploy does”A file with no schedules key says nothing about them, so the deploy leaves what the Function has
alone. An empty list says the Function declares none, which disables the ones it had. Dropping one
entry disables that one. Nothing is deleted either way. The run history stays, and cron list goes
on showing the Schedule as not in this config file, so it is disabled.
A deploy never starts a paused Schedule. Whether a job is running and when it runs are two facts on the row, and the deploy writes only the second. Delete a paused Schedule’s line, deploy, put the line back, deploy again, and it is still paused. A job somebody stopped at three in the morning does not come back because a file moved.
The name is what makes one Schedule the same Schedule across deploys. Edit the expression under a name and you have changed when a job runs. Change the name and you have deleted one job and created another. The history stays with the old name.
A deploy whose code and files are unchanged still applies the schedule block. It prints
CONFIGURATION APPLIED rather than promoting a version.
Listing them
Section titled “Listing them”npx wawesome cron list
🗓 Schedules for 'scheduled-job/health-check'
health-check */5 * * * * next run 2026-09-08 09:15 UTC
hourly-sync 0 * * * * suspended. The workspace owes payment, its subscription ended, or it has spent past its plan's allowance
nightly-reconcile 0 3 * * * paused. Resume it to run again
weekly-digest 0 6 * * 1 not in this config file, so it is disabled. Its history is kept
npx wawesome cron list <function> reads another Function, and --app <slug> lists every Function in
an App. npx wawesome schedules is the same command under a second name.
Each line says why a job is not running rather than naming a state. A Schedule can be off twice
over, and the two halves are undone by different hands. Pause one, then delete its line, and it
reads paused, and not in this config file. Declare it again, then resume. Read only the pause
there and you get a resume that does nothing.
Every time the CLI prints is UTC, including the next run. There is no time zone setting.
Pausing and resuming
Section titled “Pausing and resuming”npx wawesome cron pause nightly-reconcile -r "upstream maintenance"
[wawesome] ✔ Paused schedule 'nightly-reconcile' on 'scheduled-job/health-check'.
State: paused. Resume it to run again (survives future deploys)
Reason: upstream maintenance
Cancelled 1 queued run (never started).
The reason is optional and is stored on the Schedule, where the next person to run cron list finds
it. A pause cancels the ticks that were queued and never claimed. A run already going is left to
finish.
npx wawesome cron resume nightly-reconcile
[wawesome] ✔ Resumed schedule 'nightly-reconcile' on 'scheduled-job/health-check'.
next run 2026-09-09 03:00 UTC (no backfill)
Resume counts the next run from now. An hourly job paused for eight hours runs once when you resume it, not eight times. Nothing that was due during the pause is made up.
The dashboard does these two on the Function’s Schedules tab, and nothing else about a Schedule. A running Schedule’s row offers Run now, and Pause sits behind the arrow beside it. A paused row offers Resume. Creating one and changing its expression stay in the configuration file.
Resume turns down a Schedule the configuration file no longer declares. Starting it would run something in production that is not in the repository:
[wawesome] Error: Schedule 'weekly-digest' is no longer declared in the function's configuration file, so there is nothing to resume. Declare it again and deploy — nothing runs in production that is not in the repository.
Firing a run by hand
Section titled “Firing a run by hand”npx wawesome invoke
[wawesome] ⏳ Run 0f6f2b1a-3c7d-4e58-9a10-6b2c8d4e1f03 queued for 'scheduled-job/health-check'...
Checked https://api.example.com/health: 200 in 412ms
[wawesome] ✔ Run completed: success (412ms)
That is the same kind of run a tick fires. It reaches a private Function, which has no address on
the web for anyone else to reach. -m and -d send a method and a body with it, and --no-follow
fires the run and exits instead of tailing it.
Your handler is told what started it. We set x-wawesome-trigger to schedule for a tick, manual
for invoke, and caller for an inbound HTTP request. Every header beginning x-wawesome- is
stripped off an inbound request before your code runs. A caller cannot claim to be the scheduler, so
a job that skips its own authorization on that header has not opened a hole.
One background run per Function is in flight at a time. A second is turned down rather than queued behind the first:
[wawesome] Error: A run for this Function is already in flight. Background runs are dispatched at most once, so a second is refused rather than queued behind it.
Which version runs is resolved when the run starts and not when the tick was enqueued, so a rollback made a minute before a tick takes effect on that tick.
What the history says
Section titled “What the history says”npx wawesome cron history
📜 Run history for 'scheduled-job/health-check' (Showing 5 of 5 records)
SCHEDULE | STATE | DUE AT (UTC) | STARTED AT (UTC) | DELAY | INVOCATION ID
-------------|------------|-------------------------|-------------------------|-------|-------------------------------------
health-check | dispatched | 2026-09-08 09:15:00 UTC | 2026-09-08 09:15:00 UTC | 412ms | 0f6f2b1a-3c7d-4e58-9a10-6b2c8d4e1f03
manual | dispatched | 2026-09-08 09:12:41 UTC | 2026-09-08 09:12:41 UTC | 38ms | b41c9d02-7e55-4a13-8f6b-2d09c7e5a418
health-check | skipped | 2026-09-08 09:10:00 UTC | - | - | c72e5f18-9b04-4d6a-b3e7-1a58f0c9d264
health-check | missed | 2026-09-08 09:05:00 UTC | - | - | d5a80c36-2f41-4b97-8e0d-73c6b1e94f52
health-check | dispatched | 2026-09-08 09:00:00 UTC | 2026-09-08 09:00:00 UTC | 96ms | e93b7a45-6c28-41df-95a3-0847bd2ce6f1
manual in the first column is a run you fired with invoke. Everything else carries the
Schedule’s name. DELAY is how long after the tick we picked the run up.
Every state here is about the dispatch. None of them says what your code returned.
| State | What it means |
|---|---|
dispatched |
An invocation exists. What it did is in the logs, not here |
skipped |
The previous run was still going when this tick arrived. Ticks are never queued behind each other, because a job slower than its own interval would pile up forever |
missed |
The tick was found more than two minutes late, so it was recorded instead of fired, and the Schedule moved to the next tick in the future rather than walking the ones it slept through |
cancelled |
A pause stopped it before it was claimed |
lost |
An instance claimed it and never finished. It is not retried, because the side effects may already have landed |
failed |
The dispatch produced no invocation at all, and the sentence beside it says why |
pending, running |
Queued, or going right now |
--state <state> filters the table. --limit changes how many rows come back, and the default is 50.
A failed row is a run that never reached your code. The Function has nothing promoted to run, or
we stopped it. npx wawesome cron history --state failed finds those and finds nothing else. On
the Function’s Runs tab in the console, Failed shows the same rows.
A run that answers non-2xx has failed
Section titled “A run that answers non-2xx has failed”A background run is a success only if it answers 2xx. Anything else is recorded as an error, whatever
your handler meant by it. A 404 from a nightly job is a failed run.
A request somebody made over HTTP is recorded differently. That one is a success once it answered at all, and its status code sits in a field beside the outcome. The two rules differ because nobody is on the other end of a background run. The status code is the only thing left to read the outcome off.
So treat the status code as the job’s exit code. Answer 204 when the work is done and something
outside the 2xx range when it is not.
The response body goes nowhere. Nobody receives it, and the run is recorded as having sent zero
bytes to a caller however large the body was. console.log is the output a background run has. npx wawesome logs is where to read it.
Failed runs are on the logs side rather than the history side:
npx wawesome logs health-check --error
cron history will not show them as failures. That table reads the dispatch, and a run that reached
your code and answered 500 was dispatched.
A scheduled run also gets the longest invocation limit instead of the tighter one that holds a waiting caller. A job that takes a while is not cut off for being slow.
Start from the template
Section titled “Start from the template”scheduled-job ships a working job with the Schedule, the private visibility and the trigger check
already in place:
npx wawesome init --template scheduled-job
The scheduled-job template walks through what it ships. The CLI reference lists every command on this page beside the rest.