Skip to content

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

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.

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.

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.

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.
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.

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 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.

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.