Schedules
What runs on a timer, when, and what you see when it works or when it fails.
On this page
DispatchSEO's automation runs across three different schedulers, not one - and the split isn't arbitrary. Vercel's Hobby plan (what the hosted dispatchseo.com and most Vercel self-hosts run on) caps scheduled jobs at once a day and two jobs total, so anything that needs to run more often than that lives in GitHub Actions instead - either this repo's own workflows (for the backend's own data crons) or the 11 workflows installed into your connected site's repo (for the content pipeline). A self-hosted Docker install has neither constraint, but it has a different one: a home machine or a fresh VPS usually has no public URL for GitHub Actions to call back into, so it runs its own cron container plus an in-stack builder instead, both of which only ever make outbound connections.
Backend crons
These are the jobs that keep your numbers current - rank tracking, Search Console stats, and the post-deploy smoke test. They loop over every project on the instance; nothing here is specific to one site.
Runs
Vercel: daily at 04:00 UTC (vercel.json). Docker: the cron container, same schedule.
Needs
Nothing to be turned on - it loops every project and skips per-project gates on its own.
If it fails
Red banner on Home, alert email if configured, shown in get_cron_health as daily-ranks.
daily-ranks checks where you rank for every tracked keyword, using whichever source your project is set to: DataForSEO mode posts cheap standard-queue tasks here and the actual rank rows land later via serp-collect below; SerpApi mode checks live, but only on Mondays, to stay inside the free 250-search monthly quota; GSC mode reads average position from Search Console, for free, on every run. The same pass also snapshots yesterday's Search Console clicks and impressions, and refreshes your domain rating once it's aged past a week.
Runs
Cloud: GitHub Actions every 3 hours at :07. Docker: the cron container, same cadence.
Needs
Search Console connected - skips as an informational no-op otherwise.
If it fails
Same rails as daily-ranks, job name hourly-gsc.
hourly-gsc re-snapshots the last few days of Search Console data ( today's numbers are provisional and firm up as the day goes on), checks up to 10 newly-published pages to see if Google has indexed them yet, and piggybacks two maintenance sweeps: recovering any build that got stuck mid-way, and firing fresh research for a project whose guide queue has run completely dry.
Runs
Cloud: GitHub Actions hourly at :37. Docker: the cron container, same cadence.
Needs
DataForSEO connected with tasks actually pending - most projects on GSC or SerpApi have nothing to collect and don't appear in the report at all.
If it fails
Same rails, job name serp-collect.
serp-collect is daily-ranks' companion: it fetches the DataForSEO
standard-queue SERP tasks daily-ranks posted and writes the actual
rank_checks rows, usually 30-60 minutes later. Without this job,
DataForSEO-mode rank tracking would queue tasks that simply never land.
Runs
After every push to main (cloud) - the workflow polls until the pushed commit is actually live, then runs the check.
Needs
Nothing - runs automatically on every deploy.
If it fails
Same rails, job name deploy-check.
deploy-check is the post-deploy smoke test: it confirms the core tables are reachable, projects resolve correctly, GSC credentials still parse, and (in cloud mode) that billing and the GitHub App are configured. It also doubles as the generic outcome-reporting door for everything else on this page - every workflow below, on both schedulers, reports its own success or failure back through this same endpoint under its own job name, which is how a broken workflow in your site's repo shows up on your dashboard instead of dying quietly in the GitHub Actions tab.
Runs
Cloud: GitHub Actions every 3 hours at :41. Docker: the cron container, same cadence - and it stands down entirely when the in-stack builder is running, so the two never both claim the same job.
Needs
A connected repo with the pipeline installed. Mid-setup projects are skipped quietly.
If it fails
Same rails, job name seo-dispatch. This is the one to watch: it wakes every connected repo, so if it stops, nothing builds anywhere.
seo-dispatch is the scheduler for the content pipeline. Every few hours it asks, per project, whether any of the four scheduled jobs below are due - and only then wakes the matching workflow in your repo.
It exists because of how GitHub bills. Those workflows used to carry their own timers and fire three times a day, twice as insurance against GitHub's scheduler dropping a run. GitHub charges the repo owner a full minute minimum for every run that starts, so those extra attempts cost you real money to discover something your dashboard already knew - that the queue was empty, or a PR was still open. Roughly a quarter of a GitHub Free account's monthly allowance, per site, spent finding nothing to do.
Waking a workflow does not prove it ran: GitHub accepts the call even for a repo with Actions switched off or one that's out of minutes. So each wake-up is logged as a claim and stays open until the workflow reports its own outcome. One that never reports shows up on your dashboard rather than disappearing.
weekly-opportunities is retired - it's not on any schedule anymore (the
Monday research workflow described below replaced it), but the route still
answers if you call it by hand. It only ever covered the instance's default
project and has no toggle, so nothing else on your dashboard depends on it.
Workflows in your site's repo
When you connect a site, DispatchSEO installs 11 GitHub Actions workflows
into that repo's .github/workflows/ folder. These are what actually do
the work of the pipeline - researching keywords, writing guides, building
tools, validating them, and merging the result - while the backend crons
above only track your numbers. Because they run on GitHub's own runners,
they need a public URL to call back into your dashboard; that's exactly
the constraint the self-hosted Docker builder (below) exists to route
around when you don't have one.
Runs
When your dashboard says there's a guide to build - it checks every few hours. A daily 19:13 UTC run is a fallback in case your dashboard can't reach the repo.
Needs
An approved guide or tool suggestion waiting, and no SEO PR already open.
If it fails
Reports job=seo-daily to your dashboard - banner, email, and get_cron_health.
SEO daily builder (seo-daily.yml) builds the oldest approved
suggestion into a PR: get_instructions → template → live SERP gate →
humanizer voice pass → PR. Ships at most one guide a day.
Your dashboard decides when it runs, rather than the workflow waking up on a timer to check for itself. It already knows whether anything is approved, whether a PR is still open and whether today's guide is built, so it can answer all of that without starting a GitHub runner - and GitHub charges your account a full minute for every run that starts, even one that exits immediately. An agent usage limit is still a quiet deferral rather than a lost day - a Claude subscription limit, or a transient OpenAI rate limit on Codex - the job simply stays due and gets woken again a few hours later. A Codex account that genuinely can't run builds (out of credit, spend cap) is the opposite on purpose: that's a loud failure naming the billing fix, because retrying can't help it.
Runs
Event-driven - the moment any check reports, plus a backstop sweep every 6 hours at :17.
Needs
Hands-off publishing turned on, an open PR labeled seo, every check green, and (for guides) every changed file under your repo's own .dispatchseo/publish-paths.
If it fails
Reports job=seo-auto-merge.
SEO auto-merge (seo-auto-merge.yml) is the hands-off publish gate. A
tool PR additionally needs the tool-validated label from SEO tool
validation below before this workflow will touch it - structural changes
outside the publish paths are never auto-merged, regardless of this
setting.
Runs
Weekly, when your dashboard says it's due. A Monday 20:23 UTC run is a fallback in case your dashboard can't reach the repo.
Needs
Nothing you have to enable - this is what fills the queue in the first place.
If it fails
Reports job=seo-weekly-research.
SEO weekly research (seo-weekly-research.yml) reads your product as
it exists that week, derives keyword ideas from it, validates them through
DataForSEO, tracks the winners, and queues suggestions - guides
pre-approved, tool ideas pending your review. It also fires on demand the
moment your guide queue runs completely dry, so a missed Monday run never
leaves the daily builder with nothing to publish.
Runs
Weekly, when your dashboard says it's due. A Wednesday 20:27 UTC run is a fallback in case your dashboard can't reach the repo.
Needs
Nothing you have to enable.
If it fails
Reports job=seo-geo-scan.
SEO geo scan (seo-geo-scan.yml) measures AI visibility: it builds the
questions your customers would ask an AI assistant, answers each one with
real web search on your own Claude subscription, and records which answers
cite your site. No PR - results land directly on the dashboard's
AI-visibility section.
Runs
Instantly when you approve a tool suggestion, and weekly when your dashboard says it's due. A Wednesday 19:42 UTC run is a fallback in case your dashboard can't reach the repo.
Needs
An approved tool suggestion waiting.
If it fails
Reports job=seo-tools.
SEO tool builder (seo-tools.yml) builds the oldest approved tool idea
into a PR - template, live SERP gate, theme discovery, an execution plan
held against the pipeline's value bar (a reskinned template or a
canned-output widget fails this check), then the build itself, labeled
seo and seo-tool.
Runs
On every tool PR (opened, updated, reopened, or labeled).
Needs
A PR labeled seo-tool - guide PRs never trigger this.
If it fails
Labels the PR tool-validation-failed with the concrete failures as a comment - no dashboard alert, since a failing tool PR is a normal, actionable outcome, not a broken schedule. Only the merge job reports to the dashboard (job=seo-tool-validate); the validation job itself runs LLM-authored PR code and deliberately holds no secrets.
SEO tool validation (seo-tool-validate.yml) is the functional
reviewer for tool PRs: it builds the branch for production, exercises the
tool in a real browser, and only a pass earns the tool-validated label
that auto-merge requires.
Runs
Daily at 04:18 UTC, ahead of the day's build.
Needs
Nothing you have to enable.
If it fails
Reports two job names: seo-token-check (a dead agent credential) and seo-pipeline-version (an outdated pipeline pack - shown as an update notice, not a failure).
SEO health check (seo-token-check.yml) proves your agent credential
still works before the day's build attempts depend on it - one minimal
model call on Claude Code, a direct API probe on Codex (cheaper and more
precise there: OpenAI's own error code says whether a key is throttled,
broke, or revoked), or a cursor-agent models liveness call on Cursor -
and checks whether your installed pipeline is a version behind.
Because it runs every day without fail, its own silence over time is a
useful signal too - see the pipeline heartbeat note below.
Runs
Once, dispatched by the backend right after it installs the pipeline (cloud zero-touch onboarding), or by hand.
Needs
Nothing - this workflow is the setup step.
If it fails
Reports job=seo-setup.
SEO setup (seo-setup.yml) is the one-time personalization run: it
reads your repo, finds or scaffolds the content home, writes
.dispatchseo/conventions.md, fills in the site profile, then confirms
itself back to the backend. Safe to re-run any time - it reconciles rather
than duplicating.
Runs
Once, dispatched by setup.sh at the end of an install.
Needs
Nothing.
If it fails
Fails visibly in the GitHub Actions tab during install - this is a one-time proof, not an ongoing schedule, so it isn't something you'll see fail months later.
SEO install canary (seo-canary.yml) opens a real, throwaway PR from
inside a workflow - the exact permission GitHub's default settings block on
brand-new repos - then closes it and deletes the branch. Nothing is ever
merged. If any repo permission is wrong, this is where the install fails,
in front of you, instead of three days later at 05:00.
Runs
Manual only - fires when you click "Scan now" on the Trends page.
Needs
Nothing - always available, no schedule on purpose.
If it fails
Reports job=seo-trend-scan.
SEO trend scan (seo-trend-scan.yml) is stage one of the trend radar:
it reads your niche for subjects being talked about right now - launches,
Reddit and Hacker News buzz, Google Trends - and puts them on the radar
with evidence. It proposes no guide ideas and spends no SERP budget; that's
stage two, below.
Runs
Fires when you click "Get takes" on a trend-radar subject.
Needs
A subject on the radar for you to pick.
If it fails
Reports job=seo-trend-expand.
SEO trend expand (seo-trend-expand.yml) is stage two: it turns the
one subject you picked into 3-5 validated guide angles, queued as pending
suggestions - this is where SERP validation spends money, which is exactly
why it only ever runs on a subject you chose.
Self-hosted Docker
A Docker install runs neither the Vercel cron nor the GitHub Actions
workflows above by default - it has its own cron container and its own
builder container, both defined in docker-compose.yml, that only ever
make outbound connections. That's what makes a fully-automatic install
possible on a machine with no public URL at all.
Runs
BusyBox crond, hitting four endpoints on the same cadences as the cloud schedule above: daily-ranks at 04:00 UTC, hourly-gsc every 3 hours at :07, serp-collect hourly at :37, seo-dispatch every 3 hours at :41. A fifth line, the heartbeat, is covered below.
Needs
Nothing - always on.
If it fails
Same banner/email/get_cron_health rails as every other job on this page. docker compose logs cron shows every hit as it fires.
seo-dispatch is on that list even on a stack that runs the builder
container, and costs nothing there: the route stands down for as long as the
builder's heartbeat is fresh, so the two schedulers can never both claim the
same job. It's there for the self-host that connected a repo but doesn't run
the builder - without it, that install's only scheduler would be GitHub's own.
The crontab fires one more job the cloud schedule has no equivalent for:
a heartbeat at 05:23 UTC, the anonymous install ping covered in
Security and your data.
Unlike everything else on this page, it deliberately does not call
reportCronRun() - a failed ping costs this project one data point and
costs you nothing, so it never raises a banner, an email, or a
get_cron_health entry. DISPATCHSEO_TELEMETRY=off in your .env
turns it off for good.
The cron container (docker/cron/crontab) deliberately does not
schedule weekly-opportunities, even though it's part of the same file:
that route is retired, hard-scoped to a single project, duplicates
research the builder below already runs on its own cadence, and spends
DataForSEO balance with no toggle to turn it off. It stays callable by
hand; it just no longer runs behind your back.
Runs
Polls /api/builder/jobs every BUILDER_POLL_SECONDS (default 600s / 10 minutes) and executes whatever comes back.
Needs
Your project's agent credential - a Claude Code OAuth token, an OpenAI key, or a Cursor API key (the credential box on Settings, or CLAUDE_CODE_OAUTH_TOKEN / OPENAI_API_KEY / CURSOR_API_KEY in .env) - without one it idles and says exactly what it's waiting for.
If it fails
Each job reports under builder-<job>--<slug>, same rails as everything above.
The builder container is the in-stack replacement for the four schedule-driven workflows above (research, build-guide, build-tool, geo-scan) - the scheduling logic lives entirely on the backend, so cadence fixes ship as ordinary backend deploys, never as "rebuild your builder image." It runs each job with your project's own agent - headless Claude Code, Codex, or Cursor, taken per job off its poll feed - inside a clone of your site's repo: research and geo-scan roughly every 6.5 days, build-guide and build-tool roughly daily (20-hour windows, so a run that fired a bit early yesterday is still due today). If the builder claims a job and then dies before reporting back - a crash, a bad token - the claim expires after 3 hours and the job becomes due again, rather than sitting falsely "done" for the rest of its whole cadence window.
It also runs its own merge sweep for auto-merge projects: it checks
every open, green seo-labeled PR against your repo's own
.dispatchseo/publish-paths, and merges the ones that qualify -
functionally the same gate GitHub's seo-auto-merge.yml enforces on cloud,
just running from inside the stack instead.
What happens when a job fails
Every job on this page - backend cron, workflow, or builder job - reports
its outcome through the same function, reportCronRun(). A run always
gets logged to the cron_runs table; a failure that's worth interrupting
you for additionally triggers, in order:
- A red banner on the dashboard Home page.
- An email through Resend, if you've set up failure alerts - debounced so a job that stays broken doesn't spam you: 24 hours for the backend data crons, 6 hours by default for everything reported from GitHub Actions or the builder, and every single time for deploy-check, since each of its failures is a distinct human push.
- Visibility in the
get_cron_healthMCP tool, so your agent can see exactly what's broken and, once you've actually fixed and verified it, clear the alert itself withmark_cron_fixed.
"Worth interrupting you for" has a precise meaning: the same job failed
twice in a row, a job missed a whole schedule window, or the error is one
you must act on now (a broken deploy you just pushed, dead credentials,
an empty balance). A single failed run - a GitHub hiccup, a vendor
timeout - stays in the run log and in get_cron_health, where your agent
can still see it; scheduled jobs retry on their own, so it either cures
itself on the next run or gets promoted to the banner and email then.
See Troubleshooting for the fixes to the specific failures you're most likely to hit.
Setup-incomplete vs. a real failure
A cron or workflow that hits a prerequisite you haven't finished setting up yet - no Search Console connected, no DataForSEO credentials, a project still mid-wizard - returns an informational "skipped: setup incomplete" result and stops there. That is never treated as a failure: no red banner, no email, no HTTP 500. Being half set up is a completely normal, expected state while you're onboarding - the dashboard's Home page has its own "Initial setup" cards for exactly this, and a cron that panicked every time a project wasn't fully configured yet would make every fresh install look broken from minute one.
Loudness is reserved for regressions instead: a capability that has demonstrably worked before - data rows already exist, credentials were validated when you saved them - and then fails is treated as broken and alerts every time, because that's the difference between "you haven't gotten here yet" and "something that used to work just stopped."
Running on a machine that sleeps
If you're self-hosting on a laptop rather than an always-on VPS, a missed schedule is a delay, not a failure. Docker's staleness checks measure how long the app container has actually been running, not wall-clock time - so a machine that was asleep for ten hours wakes up owing ten hours of schedules, not flagged as broken for having missed them. Content builds catch up on their own once the machine wakes (the builder checks what's due within its next poll), and only a job that misses its window while the stack is actually up counts as a real failure. You'll never get an alert for a job that was simply powered off.
DispatchSEO