#20492: feat(cron): gate script evaluated before agent turn
docs
cli
size: M
Cluster:
Cron Job Stability Fixes
## Summary
Adds an optional `gate` field to cron jobs that runs a shell script before the agent turn. The agent fires only when the gate exits with `triggerExitCode` (default 0). Any other exit code or timeout silently skips the job at **zero LLM cost**.
## Motivation
A common pattern in cron-driven agents is "check if there is anything to do, then do it". Today, even when there is nothing to do, the job still spins up an isolated agent session or wakes the heartbeat — burning tokens and time. A gate lets you express the condition in a short shell script and skip the expensive part entirely.
Real-world examples:
```bash
# Only run PR-review agent when open PRs exist
openclaw cron add \
--name "PR review" \
--cron "0 */2 * * *" \
--session isolated \
--message "Review open PRs and summarise findings." \
--gate "gh pr list --repo myorg/myrepo --state open --json number --jq 'length > 0'" \
--announce
# Only alert when disk usage is above 80 %
openclaw cron add \
--name "Disk alert" \
--cron "0 * * * *" \
--session main \
--system-event "Disk usage is critical — investigate and report." \
--gate "bash -c '[ \ -ge 80 ]'"
# Custom exit code: trigger only when script finds work (exits 1)
openclaw cron add \
--name "Queue drain" \
--cron "*/5 * * * *" \
--session isolated \
--message "Process the pending task queue." \
--gate "~/scripts/has-pending-tasks.sh" \
--gate-exit-code 1
```
## Schema changes
```ts
// New type
type CronGate = {
command: string; // shell command (full shell syntax via /bin/sh -c)
triggerExitCode?: number; // exit code that allows the agent; defaults to 0
timeoutMs?: number; // gate killed after this many ms; defaults to 30 000
};
// New optional field on CronJob
gate?: CronGate;
```
`CronJobPatch.gate` accepts `CronGate | null` — null removes an existing gate.
## New CLI flags
```
cron add / cron edit:
--gate <cmd> Shell command to run as the gate
--gate-exit-code <n> Exit code that lets the agent proceed (default 0)
--gate-timeout-ms <n> Max ms before the gate is killed (default 30000)
cron edit only:
--clear-gate Remove the gate from an existing job
```
## Implementation
| File | Change |
|---|---|
| `src/cron/types.ts` | `CronGate` type; `CronJob.gate` field; `CronJobPatch.gate` |
| `src/cron/gate.ts` | New gate runner — `child_process.execFile` via `/bin/sh -c` / `cmd /c`, resolves immediately on timeout (SIGKILL sent async) |
| `src/cron/service/timer.ts` | Gate check at the top of `executeJobCore`, before any agent or heartbeat work |
| `src/cron/service/jobs.ts` | Gate normalisation and validation in `createJob` and `applyJobPatch` |
| `src/cli/cron-cli/register.cron-add.ts` | `--gate`, `--gate-exit-code`, `--gate-timeout-ms` |
| `src/cli/cron-cli/register.cron-edit.ts` | Same flags + `--clear-gate` |
## Tests
- **8 unit tests** in `src/cron/gate.test.ts`: exit code pass/fail, custom `triggerExitCode`, timeout, shell pipeline syntax, empty command
- **7 tests** appended to `src/cron/service.jobs.test.ts`: `applyJobPatch` gate set, replace, null-remove, and validation errors
All 25 new + existing tests pass.
## Backward compatibility
Fully additive — existing jobs without a `gate` field are unaffected. The `gate` field is optional throughout the type hierarchy and stored only when explicitly set.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Adds an optional `gate` field to cron jobs — a shell script that runs before the agent turn to conditionally skip job execution at zero LLM cost. The gate passes when the script exits with the configured `triggerExitCode` (default 0).
**Changes:**
- Added `CronGate` type with `command`, `triggerExitCode`, and `timeoutMs` fields
- Implemented gate runner in `src/cron/gate.ts` using `execFile` via `/bin/sh -c` (POSIX) or `cmd /c` (Windows)
- Integrated gate check at the top of `executeJobCore` in timer service
- Added CLI flags: `--gate`, `--gate-exit-code`, `--gate-timeout-ms`, `--clear-gate`
- Gate validation and normalization in job creation/patching
- 8 unit tests for gate runner, 7 tests for service integration
**Issues found:**
- Timer cleanup logic in `src/cron/gate.ts:65-94` needs refinement to prevent potential memory leak
<h3>Confidence Score: 4/5</h3>
- Safe to merge with minor timer cleanup issue that should be addressed
- The implementation is well-designed with proper validation, error handling, platform compatibility, and comprehensive test coverage. However, there's a timer cleanup issue in the gate runner that could cause a memory leak in edge cases. The feature is additive and backward compatible, with no impact on existing jobs.
- src/cron/gate.ts needs timer cleanup fix in the execFile callback
<sub>Last reviewed commit: aa43b8f</sub>
<!-- greptile_other_comments_section -->
<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#17529: feat(cron): add preCheck gate to skip jobs when nothing changed
by scottgl9 · 2026-02-15
81.4%
#18856: fix(cron): reject past timestamps in createJob (defense-in-depth)
by kevinodell · 2026-02-17
76.0%
#13065: fix(cron): Fix "every" schedule not re-arming after gateway restart
by trevorgordon981 · 2026-02-10
76.0%
#8034: fix(cron): run past-due one-shot jobs immediately on startup
by FelixFoster · 2026-02-03
75.5%
#6515: fix: in-process IPC for cron tool to avoid WS self-contention timeout
by amco3008 · 2026-02-01
75.4%
#14430: Cron: anti-zombie scheduler recovery and in-flight job persistence
by philga7 · 2026-02-12
75.4%
#8698: fix(cron): default enabled to true for new jobs
by emmick4 · 2026-02-04
75.3%
#10829: fix: prevent cron scheduler permanent death on transient startup/ru...
by meaadore1221-afk · 2026-02-07
74.9%
#12303: fix(cron): correct nextRunAtMs calculation and prevent timer stall
by colddonkey · 2026-02-09
74.8%
#23290: fix(cron): use lastRunAtMs for next schedule of interval jobs after...
by SidQin-cyber · 2026-02-22
74.8%