Background jobs console
The four background jobs, what their status columns really mean, how they are actually scheduled, and what "Run due now" does and does not do.
Last updated 2026-08-06
Summary#
The Job Console shows the four recurring jobs that run without a user pressing anything: scheduled reports, rank alerts, the competitor monitor and tier-2 AI visibility tracking. For each it reports the last run, the next run, how many items it manages and whether any are due. An owner can trigger a job immediately. That trigger is not a forced re-run, and understanding why is most of what this page is for.
Purpose#
When a customer says their weekly report never arrived, there are only three possible causes: the schedule is not enabled, the job has not run, or the job ran and the send failed. This screen settles the first two in a glance, without a database query, and its Run due now button settles the third by making the job run while you watch.
Requirements#
- An active admin identity to read the screen. See Getting admin access.
- The
ownerrole to trigger a job. MONITOR_DBbound. Every figure comes from D1.- For the tier-2 job to do anything,
OPENAI_API_KEYset on the environment.
Permissions#
| Action | Role needed | Endpoint |
|---|---|---|
| Read job status | admin | /api/admin/jobs |
| Trigger a job now | owner | /api/admin/jobs/run |
A non-owner gets 403 {"error":"Only an owner can run jobs."} and does not see the button.
Navigation Path#
Admin console → Operations → Jobs
The screen's own title is Job Console.
How scheduling actually works#
This matters more than the screen does, because the console cannot tell you whether the scheduler is alive.
The worker has a scheduled() handler, but the site runs on Cloudflare Pages, and Pages never calls scheduled(). Cron triggers are a Workers feature. The jobs are therefore driven from outside, by an HTTP call:
POST /api/cron/runruns whatever is due. Every job self-throttles in the database, so calling it often is safe and idempotent.- A small standalone Cloudflare Worker in
cron-worker/holds a real cron trigger and calls that endpoint every 15 minutes. It is the primary scheduler and is deployed separately from the site. - A GitHub Actions workflow calls the same endpoint hourly as a backup.
Both callers must present MV_INTERNAL_SECRET. The check fails closed: if the secret is unset on the Pages environment, /api/cron/run returns 503 and nothing runs at all. The same secret must match in three places: the Pages project, the cron worker's secrets, and the GitHub repository secrets.
Important: If every job on this screen shows a stale last run and a growing due count, suspect the scheduler or the secret before suspecting the jobs. Nothing on this screen reports scheduler health.
Reading the table#
The table Scheduled & background jobs has six columns: Job, Status, Last run, Next run, Items, Actions.
| Column | Meaning |
|---|---|
Status | Grey Disabled when the job is switched off by its environment, amber Due now when at least one item is due, green Scheduled otherwise |
Last run | A relative time, or the literal unknown when there is nothing recorded. It is never faked |
Next run | in Xm, in Xh, in Xd, or — when the job has no computable next time |
Items | How many enabled things the job manages, not how many are due |
Two of those columns need a caveat, and the screen states it in its own explainer: only tier-2 AI visibility records a true job-level last run. For the other three, Last run is derived from the most recently processed item, so an empty job with no items shows unknown even though the scheduler is calling it on time. Rank alerts and the competitor monitor have no computable next run at all, because each item carries its own interval, so their Next run is always —.
The four jobs#
| Job | Label on screen | What it does | Cadence | Last run comes from | Enabled when |
|---|---|---|---|---|---|
schedules | Scheduled reports | Sends each customer's scheduled report when its next-run time passes | Per schedule | The most recent report that fired | Always |
rank_alerts | Rank alerts | Re-checks each enabled rank alert and notifies on movement | Every 24 hours per alert | The most recently checked alert | Always |
monitor | Competitor monitor | Re-fetches each monitored URL on its own interval and reports changes | Per URL, from its own interval setting | The most recently checked URL | Always |
tier2 | AI visibility (tier-2) | Measures tracked brands across AI models, up to 5 per run | Roughly every 22 hours | A real job-level timestamp | OPENAI_API_KEY is set |
Each row carries a note under the label. For rank alerts and the monitor it reads either <n> alert(s) due now. and <n> URL(s) due now., or the steady state Enabled alerts; each re-checked every 24h. and Enabled URLs; each on its own interval. For scheduled reports it reads Enabled schedules; last run = the last one that fired. For tier-2 it reads Tracked brands (up to 5 measured per run, ~every 22h)., or OPENAI_API_KEY not set — this job is a no-op. when the key is missing.
Step-by-Step Guide#
Check whether a job is running#
- Select Jobs in the Operations group.
- Read
Last runfor the job in question. A time within the expected cadence means the scheduler is reaching the worker. - Read
Status.Due nowfor a sustained period means items are queuing up and the job is not being called, or is failing before it processes them. - Read
Items. A count of zero explains anunknownlast run without implying a fault.
Run a job now#
- Press Run due now on the job's row. The button label becomes
Starting…. - The page shows
Triggered <job> — it processes only items that are due. Refreshing status…and reloads the table after four seconds. - On failure the message is
Error: <message>.
The endpoint returns immediately with {ok:true, job, triggered:true} and the job itself runs in the background, so a successful trigger tells you the job started, not that it finished. Re-read Last run after the refresh, and check the Run Log or the error log for what it did.
Warning: Run due now is not a forced re-run. It calls exactly the same function the scheduler calls, and every job still processes only items that are due. Tier-2 keeps its 22-hour throttle, so pressing the button an hour after a tier-2 run does nothing measurable. If you need a specific item processed ahead of schedule, change that item's own next-run or last-checked time; the console cannot do that for you.
Every trigger is audited as jobs.run with the job key as the target. See Audit log.
What the endpoint accepts#
POST /api/admin/jobs/run takes { token, job } where job is one of monitor, schedules, rank_alerts or tier2. Anything else returns 400 {"error":"Unknown job."}. There is no rate limit, and no lock: pressing the button twice starts the job twice, though the due-item throttling means the second pass usually finds nothing left to do.
Troubleshooting#
| Symptom | Likely cause | Fix |
|---|---|---|
Every job shows an old Last run and a rising due count | The external scheduler is not reaching the worker, or MV_INTERNAL_SECRET is unset or mismatched | Check the cron worker's deploy and the secret in all three places; /api/cron/run returns 503 when the secret is unset |
Last run reads unknown on a healthy job | The job has no processed items yet, so there is nothing to derive a time from | Check Items. Zero items is not a fault |
Next run is — for rank alerts and the monitor | Neither job has a single next time; each item carries its own interval | Expected. Use Due now and the item count instead |
Tier-2 shows Disabled | OPENAI_API_KEY is not set on the environment | Set the key and redeploy. Until then the job is a no-op |
| Run due now succeeded but nothing changed | No items were due, or tier-2's 22-hour throttle has not elapsed | Confirm with the item note in the row |
| No Run due now button anywhere | Your role is admin | An owner triggers the job |
| A customer's scheduled report never arrived, but the job ran | The schedule may be disabled, or the send failed after the job processed it | Check the schedule itself and the error log. See Error log and resolution |
| The whole screen fails to load | MONITOR_DB is not bound | Restore the binding and redeploy |
FAQs#
Can I pause a job? Not from this screen. Three of the four are always enabled and tier-2 is enabled by the presence of an environment variable. Pausing individual work means disabling the individual schedules, alerts or monitored URLs.
Can I see what a job did on its last pass? Not here. The Job Console reports state, not output. Per-run detail lives in the Run Log, and failures group into the error log.
Does triggering a job cost anything? It costs whatever the due work costs in provider and model calls. Tier-2 measures up to five brands per run. Because only due items are processed, pressing the button does not multiply that cost.
Why is the scheduler a separate deploy? Because Pages cannot run cron. The cron worker is a real Cloudflare Worker with a cron trigger whose only job is to call /api/cron/run. Deploying the site does not deploy it.
Do jobs notify anyone when they fail? Not directly. Failures surface as grouped issues in the error log and, for failed automation steps, on the Workflow Failures screen. Nothing pages a human.
See also
Was this article helpful?
Thanks — feedback noted for the docs team.