Scheduled jobs
Run a published handler on a schedule, as its own principal, with the roles and org reach you give it.
A scheduled job runs one of your app's published handlers on an interval or a cron expression.
It is the background half of your app: the nightly dunning email, the hourly sync, the weekly
clean-up. Create jobs in the console under Code › Scheduled jobs, from a handler with
sdk.crons, or over the API.
A job is three things:
- a schedule: when it fires (Schedules);
- a principal: who it runs as, and so what it may do (this page);
- a firing that is carried to completion, however long it takes and whatever fails on the way (Jobs that loop, Runs, failures and Resume).
A job runs as itself
Every job gets its own identity, a principal of the app, and every run is that principal. Never the person who created the job, and never the person who presses Run now.
The obvious alternative, running a job as whoever set it up, is wrong for this shape of thing. A scheduled job is infrastructure with an indefinite lifetime; a person's access is a fluid claim about their current role. Tie the first to the second and someone changing teams silently disarms your nightly invoice sweep, and it fails as a stream of denials nobody is watching. So the two questions are kept apart:
| Question | Answered by | Changes when |
|---|---|---|
| What may this job do? | The roles bound to its principal | You deliberately bind or remove one |
| Who set this up? | The job's creator, kept for audit | Never: it is a record, not a permission |
In the handler, ctx.user is the job's principal and the organization it runs as.
Born paused, holding nothing
A new job is created paused, and its principal holds no roles, so it is born unable to do anything. Giving it power is a separate act:
Create it
Name it, choose the published handler it runs, its payload (the data the handler receives on
every run), its schedule, and its timeout (per turn, 1 to 3,600 seconds, 300 by default).
Give it roles
On the job's page, under Roles, bind the roles it needs. Binding a role to a job is its own
permission (Arm scheduled jobs, crons:assign_cron_role), declared high risk, so an agent
asked to do it halts for a person. Effective permissions on the same page shows exactly what
the roles add up to.
Choose where it can act
For one of your own jobs, set its org reach. It starts at None.
Activate it
Activate starts the schedule. Activating a job with no roles asks you to confirm, because every governed thing it tries will be refused.
Creating a schedule and giving it power are two decisions, and nothing collapses them into one.
That is also why sdk.crons can create a job but can never bind
it a role.
How each call is authorized
A job's run is an ordinary run of the handler, with one difference: there is no endpoint in front of it, so there is no endpoint allow-list either. Each SDK call the handler makes is decided by the job principal's own roles. On top of that, the published version's frozen capability manifest still gates the elevated powers: raw SQL, email, storage, cross-org reads.
So a job with no roles still runs: a handler that touches nothing governed is legitimate. It is refused the moment it reaches for something its roles do not allow, and the run history shows that refusal next to the empty role list.
Reviewing a job's roles
A job keeps its roles until you change them. The job list shows each job's bound-role count, and every run records the roles the job actually held at the moment it ran, so you can compare what a job used against what it may do.
Where a job can act
An app's data belongs to the app's organizations. Which of them a job may touch depends on whose job it is.
A job created inside the app (by one of the app's own users, through a handler) belongs to that user's organization. It acts in that organization only, like everything else inside the app.
One of your own jobs, created by you as the app's owner, has an org reach, set on its page under Where it can act:
| Org reach | The job reaches |
|---|---|
| None (the default) | No organization's data: only your own records, and anything outside the app (email, outside APIs). |
| All organizations | Every organization in the app, including ones that sign up later. |
| Only these | The organizations you tick. A new organization is never added on its own. |
Org reach decides which organizations; roles decide what the job may do there. A job needs both. Two consequences in the handler:
- A job whose reach is exactly one organization runs as that organization. Its rows carry no
org column and its new rows are that organization's, exactly as if a member of it were running
the handler.
ctx.audience.modeisorg. - Any other reach acts for you, the owner. Rows say which org they belong to (
org_uid), and a new row in an organization's table must name one.ctx.audiencetells the handler which mode it is in; see The handler context.
Only the app's owner, in the console, can change a job's org reach (ORG_REACH_OWNER_ONLY). A job
that belongs to one of the app's organizations has no reach to set (ORG_REACH_NOT_APPLICABLE).
What a job can run
A job runs a published handler: the version that is live when it fires, with that version's frozen manifest. If the handler has no published version, the run is recorded as failed and says so. Publish first, then activate.
A handler written only to be run by its jobs (a bulk mailing, a sweep over every organization) should be job-only: then no endpoint, agent, MCP tool or key can run it, only its scheduler and you testing it in the console.
Deleting a job
Deleting a job retires its principal and removes that principal's role bindings, so nothing is left holding access. The run history is kept: "what did this do before we removed it" is exactly the question asked afterwards.
Next
Schedules
Intervals, cron expressions, timezones and missed windows.
Jobs that loop
ctx.job.each: one page a turn, resumable, never twice.
Runs, failures and Resume
Run history, retries, alerts, Run now.
Managing jobs from code
sdk.crons, and the two grants that govern it.
Roles & policies
Deciding what a job's principal may do.