Skip to main content
A deferred run is an invocation you book to happen once in the future: a reminder 24 hours out, an abandoned-cart follow-up, a survey 2 days after purchase.
Cron or deferred?
  • Cron — repeats on a fixed schedule (every day at 9:00). Defined in code.
  • Deferred — happens once, at a time computed at runtime (24 hours after this order). Booked from the handler or from the HTTP request.

Book from the handler

ctx.jelou.schedule() books a run relative to “now”:
index.ts

Parameters

Durations accept compound units from largest to smallest: "1h30m" is valid, "30m1h" is not.
For very short follow-ups use 5 seconds or more. Below that, network latency can push the booked time into the past and the platform rejects it.

Absolute time

ctx.jelou.scheduleAt() takes a date instead of a duration:
It throws if the date is in the past or unparseable. Every other field behaves like in schedule().

Avoid duplicates

If the service calling your function retries, idempotencyKey guarantees a single booking:
Reusing the same idempotencyKey with a different payload returns a 409 error.

Book from an HTTP request

Any caller can defer a call by adding a header to the normal POST. Without schedule headers, the function runs immediately as always.
The response is 202 Accepted when the booking is created and 200 OK when a retry reuses an existing booking. The body cannot exceed 64 KB.

Receive the fire

When the time comes, your function receives the original payload at the path you specified. Use ctx.isScheduledFire to tell the fire apart from a normal request:

Check before acting

Between booking and firing, reality can change: the customer already paid, the template got paused, the user opted out. ctx.guard chains those checks and skips the send if any of them fails:
If every check passes, .run() executes and its result is the response. If any fails, the chain short-circuits and returns { skipped: "<name>" } without running the send.
The chain terminates with .run(handler), not .then(). ctx.guard is a fresh chain on every request.
If the bot does not travel in the payload, ctx.bot is not resolved on a deferred fire and ctx.templateRegistry has no channel bound. Include the botId in the payload when booking and bind it with ctx.templateRegistry.for(botId). See WhatsApp templates.

Look up what is booked

ctx.jelou.findDeferred() lists your function’s pending runs:

Cancel

ctx.jelou.cancelDefer() cancels in bulk by audience or tag. Exactly one of subject, key or keyPrefix is required:
raced holds the runs that were already firing when the cancel arrived — for those, the ctx.guard check inside the handler is the last line of defence. Write your handlers to be idempotent. Before a broad cancellation, preview the scope with dryRun:
There is no “reschedule”: a booking is immutable. To change the time, cancel and book again.

Local testing

schedule, scheduleAt, findDeferred and cancelDefer only work in a deployed function. Locally they throw because there are no platform credentials. To exercise the flow with jelou functions dev, turn on simulation:
In that mode arguments are validated, the booking is logged, and a synthetic result is returned — your handler keeps running, but nothing real is booked. For unit tests use createMockContext, whose ctx.jelou records the calls with no network or setup.

Inspect from the CLI

See the CLI reference.

Limits

Bookings that already fired, were cancelled or failed do not count against the active limit.

Common problems

Check the booking’s status and last error:
If the status is failed, the error field says why delivery failed. If it is cancelled, something cancelled it first — review your cancelDefer calls.

Cron

Recurring tasks on a fixed schedule.

Messaging

Send WhatsApp and validate templates.

Webhooks

Verify signatures from external services.

CLI

The defer list and defer get commands.