Managing jobs from code
sdk.crons creates, reads and triggers scheduled jobs from a handler, and can never give one power.
sdk.crons is the handler-facing door to scheduled jobs. Reach for it when a schedule belongs to
your data rather than to your configuration: giving each new customer their own digest job, or
reporting which jobs are failing.
Two grants
| Capability | Methods | Default |
|---|---|---|
sdk:crons_read | list, get, listRuns, getRun, listRoles, preview | granted |
sdk:crons_write | create, update, pause, resume, remove, runNow (implies sdk:crons_read) | deny, high risk |
sdk:crons_write is high risk, not medium, and the reason is easy to miss. Creating a job is
harmless: a new job holds no roles and can do nothing. Changing one is not. An existing job may
already be armed with roles a person deliberately bound, and its handler decides what code runs
under them. A handler that could repoint someone else's job would run arbitrary code with roles
its own author was never given. runNow is here for the same reason: it fires a job as that job's
principal, not as the caller.
Binding roles is deliberately not in the SDK. Arming a job stays in the console and the
governed API (crons:assign_cron_role), so handler code can create a schedule but can never grant
it power. That is what keeps a handler from minting an unattended principal more privileged than
itself.
What a caller sees
Inside the app, a caller lists and manages only its own organization's jobs and runs. The app's
owner, reaching the app from outside (the console, the owner's agents), sees every organization's,
and each job and run names its organization (orgUid, orgName; null for the owner's own). A
job created inside the app belongs to the caller's organization.
Create a job
export default async function onboardCustomer(data, ctx) {
const { sdk } = ctx;
// Check the schedule first: real fire times, in the customer's timezone.
const nextRuns = await sdk.crons.preview({
scheduleKind: "cron",
cronExpression: "0 7 * * mon-fri",
timezone: data.timezone,
count: 3,
});
// Created PAUSED, holding no roles: it can do nothing until someone arms it.
const job = await sdk.crons.create({
name: `Morning digest: ${data.customerName}`,
targetHandlerUid: data.digestHandlerUid, // the handler's uid; it runs the published version
payload: { customerUid: data.customerUid },
scheduleKind: "cron",
cronExpression: "0 7 * * mon-fri",
timezone: data.timezone,
timeoutSeconds: 120,
});
return { jobUid: job.uid, status: job.status, nextRuns };
}A schedule is either { scheduleKind: "interval", intervalSeconds } (at least 60) or
{ scheduleKind: "cron", cronExpression }, with a timezone. The same validator serves the
console, the API and the SDK, so a schedule cannot be accepted by one and refused by another. See
Schedules.
Read jobs and runs
// Which jobs failed recently, and what they were allowed to do when they did.
const failures = await sdk.crons.listRuns({ status: "failed", limit: 20 });
return failures.map((run) => ({
job: run.cronName,
when: run.startedAt,
why: run.errorText,
couldDo: run.principalRoles, // the roles held AT THE TIME it ran
}));list({ status, limit, offset })returns jobs, newest first. A job'sfiringis its unfinished occurrence, if any (running,retryingorstopped, with its turns and counts).listRuns({ cronUid, status, limit, offset })returns runs, newest first; omitcronUidfor every job you can see.getRun(runUid)adds the result and the failed rows.listRoles(cronUid)returns the roles bound to the job's principal: exactly what it may do.
Change, pause and trigger
| Method | Does |
|---|---|
update(cronUid, patch) | Changes name, description, handler, payload, timeout or schedule. The next run time is recomputed only when the schedule changes or the job is activated, so a rename never pushes a job's next fire. |
resume(cronUid) | Activates the job: it starts firing on its schedule. |
pause(cronUid) | Holds the job, keeping its schedule and history. |
remove(cronUid) | Deletes the job and retires its principal, removing its role bindings. Run history is kept. |
runNow(cronUid) | Fires the job now, for real, as its own principal. Carries on an unfinished firing instead of starting over. |
Two meanings of 'resume'
sdk.crons.resume activates a paused job. The console's Resume button is something else:
it carries on an unfinished firing, and in code that is sdk.crons.runNow.
runNow answers { status, resumed, continues, run }. resumed is true when it carried on an
unfinished firing; continues is "background" when work remains on an active job,
"resume" when a paused job stopped with work left, and null when the firing finished. It is
refused with CRON_BUSY while a turn of the job is running.
Errors
| Code | When |
|---|---|
SDK_CAP_NOT_GRANTED (403) | At save: the code uses a write method and you lack sdk:crons_write. |
SDK_CAP_NOT_IN_MANIFEST (403) | At run time: the published version was not frozen with the capability it needs. |
CRON_BUSY (409) | runNow while a turn of that job is running. |
404 | An unknown or malformed job or run uid, or one in another organization. |
Signatures and every field of CronJob and CronRun are in the
Handler SDK reference.