How OpenHelm's Scheduler Works
A look under the hood at OpenHelm's scheduling engine — how jobs are queued, prioritised, and executed.
OpenHelm's promise — that jobs run on schedule without manual intervention — depends on a reliable scheduling engine. This post explains how that engine actually works.
The Scheduler Loop
At its core, OpenHelm's scheduler is simple: once every 60 seconds, it checks the database for any jobs whose nextFireAt timestamp has arrived.
When a due job is found:
- A run record is created with status
queued - The run is added to a priority queue
- The job's
nextFireAtis updated to its next occurrence
That 60-second tick is intentional. Sub-minute scheduling isn't needed for the kind of work OpenHelm is designed for — daily code audits, weekly dependency updates, nightly test runs. The tick keeps the scheduler lightweight and the database activity minimal.
The Priority Queue
Runs don't execute immediately when created — they sit in a priority queue. This queue has three priority levels:
| Priority | Source | Description |
|---|---|---|
| 0 (highest) | Manual | Triggered by Run now — always jumps the queue |
| 1 | Scheduled | Regular scheduled fires |
| 2 (lowest) | Corrective | Auto-generated self-correction retries |
Manual triggers get the highest priority because when you click "run now", you want it to actually run now — not wait behind a queue of scheduled jobs. Corrective retries get the lowest priority so that normal scheduled work always proceeds even when previous runs are being retried.
Within each priority level, runs execute first-in, first-out.
Concurrency
By default, only one run executes at a time. The concurrency limit is configurable in Settings — you can increase it to 2 or 3 if you want jobs to run in parallel across different projects.
The executor pulls from the priority queue whenever a slot is free. If you're running three concurrent jobs, it fills all three slots simultaneously, always picking from the highest-priority available run.
Schedule Types
Each schedule type has a different rule for calculating the next fire time:
Once: After firing, nextFireAt is set to null — the job never fires again.
Interval: nextFireAt is set to the run's completion time plus the interval. This means runs don't pile up if Claude Code takes longer than expected — the next run starts a full interval after the previous one finishes.
Cron: The next matching cron expression occurrence is calculated from the current time. Standard 5-field cron syntax is supported.
Calendar: A human-friendly alternative to cron. Pick daily, weekly, or monthly with a specific time, and OpenHelm calculates the next occurrence.
Manual: nextFireAt is always null — the scheduler never auto-fires this job.
What Happens on Startup
When OpenHelm launches, it checks for runs that were left in an inconsistent state — for example, if the app was force-quit while a job was running. Any run stuck in running status is automatically marked as failed with a note explaining the reason. Any run stuck in queued status is re-enqueued correctly.
This crash-recovery pass runs before the scheduler starts, so you never start with phantom running jobs.
Job Execution
When the executor picks a run from the queue:
- Pre-flight checks run: the job still exists, the project directory exists, the Claude Code binary exists
- If any check fails, the run is marked
permanent_failureimmediately (no retry) - If all checks pass, the run is marked
runningand Claude Code is launched as a child process - stdout and stderr from Claude Code are streamed in real time to the run log
- When the process exits, the run is marked
succeededorfailedbased on the exit code
Silence Detection
If Claude Code stops producing output for 10 minutes while a run is active, OpenHelm detects the silence and flags it — this usually indicates Claude Code is waiting for interactive input, which it can't receive in headless mode. The run is logged accordingly.
The Result
This architecture keeps things simple and reliable. There's no external scheduler, no separate daemon, no network dependency — just a tick loop, a priority queue, and direct process invocation. When it works, it fades into the background entirely.
More from the blog
Wake Up to Finished Work
How OpenHelm turns your overnight hours into productive development time — without you lifting a finger.
Goal-Driven Automation: Why Intentions Beat Instructions
The philosophical shift from scripting every step to specifying the outcome you want — and why it unlocks a completely different class of automation.