Code-defined schedules and timers
Define schedules in TypeScript or Python with platform.cron, create one-off timers, and understand what happens at deploy and in local development.
You can define schedules in your code instead of the settings file. You can also create timers: one-off jobs that run once at a time you pick, such as "remind this user in 13 days".
Turn it on first:
{ "bindings": { "cron": { "code": true }, "data": { "managed": true } } }
Generate the typed object once (the platform object):
cohostly types --project my-app
A schedule in code
import express from 'express' import platform from './platform.gen.js' const app = express() platform.cron.schedule('every 15 minutes', async (run) => { const seen = await platform.data.$app.sync_runs.findMany({ where: { run_id: run.id }, take: 1 }) if (seen.length) return // a repeat: ignore it await syncInbox() await platform.data.$app.sync_runs.create({ data: { run_id: run.id, ran_at: new Date().toISOString() } }) }, { name: 'sync-inbox', timeZone: 'UTC', retries: 1, overlap: 'skip', timeout: '5m' }) app.use(platform.cron.express()) // serves POST /__platform/jobs/<name>, scheduler only app.listen(Number(process.env.PORT) || 3000)
from platform_gen import platform @platform.cron("0 9 * * 1-5", name="daily-report", time_zone="Asia/Kolkata", retries=2) def daily_report(run): # run is a dict: id, name, scheduled_for, attempt, payload print("report for", run["id"], "attempt", run["attempt"])
Options in TypeScript are name, timeZone (node-cron's timezone also works), timeout, overlap, retries, misfire, catchUp ('latest'), freshSandbox and exact. In Python they are name, time_zone, timeout, overlap, retries, misfire, catch_up, fresh_sandbox and exact. Their meaning is the same as in schedules and syntax.
Rules for the name:
- It must match the settings-file pattern: lower-case letters, digits,
_and-, up to 63 characters, no dots. The SDK itself is a little looser and accepts a dot, but the platform refuses it when the schedule is sent, so do not use one. - It must not also appear in the settings file.
When schedules are sent
Define schedules while the app starts, at the top level of your file. About a quarter of a second after the first schedule() or on() call, the SDK sends everything to the platform. It tries up to 6 times with growing waits, and it does not retry a 4xx answer except 429 (too many requests).
The throwaway start at deploy
When you deploy a project with "code": true, the platform has to learn your schedules. It does so by starting your app once in a throwaway sandbox, waiting for the SDK to send them, and then closing that sandbox.
- The deploy waits up to 90 seconds for the schedules to arrive.
- If none arrive, or the app crashes first, the deploy fails and the previous version stays live.
- Your start-up code therefore runs once at every deploy, in a sandbox nobody uses. Do not do anything in start-up code that must happen only once, such as sending an email or charging a card.
If you do not define schedules in code, remove bindings.cron or set up the schedules in the settings file instead.
Timers
A timer is a one-off job. Register a handler with on, then create the timer with once.
platform.cron.on('trial-ending', async (run) => { await sendReminder(run.payload.userId) }) app.post('/start-trial', express.json(), async (req, res) => { const when = new Date(Date.now() + 13 * 86_400_000) const { id } = await platform.cron.once(when, 'trial-ending', { userId: req.body.userId }) res.json({ timer: id }) // keep the id if you may need to cancel it })
from datetime import datetime, timedelta, timezone from platform_gen import platform @platform.cron.on("trial-ending") def trial_ending(run): print("remind", run["payload"]["userId"]) def start_trial(user_id): when = datetime.now(timezone.utc) + timedelta(days=13) # a time-zone-aware datetime timer = platform.cron.once(when, "trial-ending", {"userId": user_id}) return timer["id"]
The calls are platform.cron.once(at, name, payload?), platform.cron.timers() to list the pending ones, and platform.cron.cancel(id).
Timer rules:
- The
namefollows the same pattern as a schedule name, and a handler with that name must exist. - The time must be a real date, no more than 1 minute in the past and no more than 366 days ahead.
- The payload is at most 8 KB.
- You can have at most 1000 waiting timers per project.
- A timer run uses the default 10-minute timeout, allows overlap, and gets one attempt with no retries.
- Calling
oncetwice creates two timers. Nothing removes duplicates by name. - Timers survive deploys and rollbacks. A handler with that name must keep existing in later versions, or the timer will find nothing to run.
- Pass a time-zone-aware time in Python. A time with no zone is sent as it is, with no zone.
From the command line:
cohostly schedules timers --project my-app cohostly schedules cancel-timer <id> --project my-app
Local development
cohostly dev runs your app on your machine with a temporary development token. That token is read-only for the clock:
- Schedules are not registered. The SDK logs the failure and your app keeps running.
once,timersandcancelanswer 403read_only_token.
So you cannot test a schedule end to end on your computer. To test one handler by hand, run the app in run mode, which makes the SDK run only that handler and exit:
PLATFORM_RUN=daily-report PLATFORM_RUN_ID=test-1 cohostly dev --project digest -- node server.js
This combination uses the documented variables, but treat it as a convenience to try, not a promise. To test the real thing, deploy and use cohostly run <name> (failures and history).
Python in a web app
In a web app, mount your own route and hand the call to the helper after you have verified the context:
@app.post("/__platform/jobs/{name}") async def job(name: str, request: Request): ctx = platform_context(request) # verify X-Platform-Context, as on the previous page status, body = platform.cron.handle(name, await request.json(), ctx["caller"]) return JSONResponse(body, status_code=status)
platform_context is the verifying function from what your app receives. handle returns the status code and body to send back.