Skip to content

feat(cron): conditional triggers for cron jobs#2654

Closed
iankar8 wants to merge 1 commit into
NousResearch:mainfrom
iankar8:feat/cron-triggers
Closed

feat(cron): conditional triggers for cron jobs#2654
iankar8 wants to merge 1 commit into
NousResearch:mainfrom
iankar8:feat/cron-triggers

Conversation

@iankar8

@iankar8 iankar8 commented Mar 23, 2026

Copy link
Copy Markdown

Summary

Adds a lightweight trigger system for cron jobs — a $0 pre-run gate that evaluates a condition before spawning an AIAgent. If the trigger returns false, the job is skipped and rescheduled. No LLM call, no tokens burned.

Problem: Cron jobs run on fixed schedules regardless of whether there's new data to process. A job scheduled every 30m fires 48 times/day even if only 2 of those runs have meaningful work. The other 46 runs waste inference costs.

Solution: An optional trigger field on cron jobs that supports three evaluation types:

Type Gate Cost
sql Query state.db, skip if result < threshold $0 (local SQLite)
file_changed Check file mtime vs last run $0 (stat call)
command Shell command, skip if exit ≠ 0 $0 (subprocess)

Examples

# Only summarize when there are new conversations
cronjob(action="create",
  prompt="Summarize new conversations",
  schedule="every 2h",
  trigger={"type": "sql",
           "query": "SELECT COUNT(*) FROM messages WHERE timestamp > strftime('%s','now','-2 hours')",
           "threshold": 1})

# Only process when feed file has been updated
cronjob(action="create",
  prompt="Process updated data feed",
  schedule="every 30m",
  trigger={"type": "file_changed", "path": "~/data/feed.json"})

# Only run when an external flag is set
cronjob(action="create",
  prompt="Run nightly analysis",
  schedule="0 9 * * *",
  trigger={"type": "command", "command": "test -f /tmp/new-data-ready"})

Design decisions

  • Fail-closed: Trigger errors (SQL exceptions, missing files, timeouts) skip the job rather than running it. You never accidentally burn tokens on a broken trigger.
  • Read-only SQL: Mutation queries (INSERT/UPDATE/DELETE/DROP/ALTER/CREATE) are blocked.
  • 10s timeout: Command triggers have a hard 10-second timeout to prevent blocking the scheduler.
  • Backward compatible: Jobs without a trigger field run exactly as before. Existing jobs, configs, and the [SILENT] mechanism are untouched.
  • Minimal surface: 3 files changed + 1 test file. The trigger check is entirely contained in the cron subsystem — AIAgent, model_tools, gateway, and config are untouched.

Files changed

  • cron/scheduler.pycheck_trigger() function + 3 type-specific evaluators, wired into tick() before run_job()
  • cron/jobs.pytrigger param on create_job(), persisted in job dict
  • tools/cronjob_tools.pytrigger param on cronjob() tool + schema update
  • tests/cron/test_triggers.py — 18 tests covering all trigger types, edge cases, and failure modes

Test plan

  • 18 unit tests passing (pytest tests/cron/test_triggers.py)
  • Manual test: create a cron job with SQL trigger, verify it skips when query returns 0
  • Manual test: create a cron job with file_changed trigger, verify it skips when file is stale
  • Manual test: verify existing cron jobs without triggers still run normally

Add a $0 pre-run gate for cron jobs. Before spawning an AIAgent (which
costs real money), evaluate a lightweight trigger condition. If the
trigger returns false, the job is skipped and rescheduled — no LLM
call, no tokens burned.

Three trigger types:

- **sql**: Run a read-only query against state.db. Skip if result is
  below a configurable threshold (default: 1). Useful for "only run if
  there are new messages/sessions since last check."

- **file_changed**: Check file mtime against the job's last_run_at.
  Skip if the file hasn't been modified. Useful for "only summarize
  the feed when it has new data."

- **command**: Run a shell command with 10s timeout. Skip if exit code
  is non-zero. Useful for arbitrary external conditions like flag files
  or API health checks.

Mutations (INSERT/UPDATE/DELETE/DROP) are blocked in SQL triggers.
Trigger failures are fail-closed (skip the job, don't waste money).

Examples:

  cronjob(action="create",
    prompt="Summarize new conversations",
    schedule="every 2h",
    trigger={"type": "sql",
             "query": "SELECT COUNT(*) FROM messages WHERE timestamp > strftime('%s','now','-2 hours')",
             "threshold": 1})

  cronjob(action="create",
    prompt="Process updated data",
    schedule="every 30m",
    trigger={"type": "file_changed",
             "path": "~/data/feed.json"})

  cronjob(action="create",
    prompt="Run analysis",
    schedule="0 9 * * *",
    trigger={"type": "command",
             "command": "test -f /tmp/new-data-ready"})
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management labels May 3, 2026
teknium1 added a commit that referenced this pull request May 15, 2026
Adds three pre-run gate recipes to the cron docs:
- file-change gate (stat + mtime + state file)
- external-flag gate (file presence)
- SQL-count gate (user's own database, not state.db)

These are the use cases @iankar8 proposed adding as a parallel
'trigger' subsystem in #2654. The existing `script` + `wakeAgent`
gate already covers all three at $0 — this lands the patterns as
documentation so users can find them, instead of adding a second
gating mechanism to the cron subsystem.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the careful writeup @iankar8 — the trigger use cases here are real, but Hermes-Agent already ships a $0 pre-run gate that covers all three: a cron job's script parameter runs before the LLM, and emitting {"wakeAgent": false} on its last stdout line skips the agent run entirely.

So rather than landing a parallel trigger subsystem, the work from this PR landed as documentation — three worked recipes (file-change, external-flag, SQL-count) in the existing wakeAgent section of the cron docs, with credit to you:

#26229

Two extra notes from the review:

  • The sql trigger's example queries Hermes's own ~/.hermes/state.db. That schema is internal and changes between releases, so the recipes point at the user's own database instead.
  • Fail-closed on script errors / non-zero exit is already how the existing gate behaves (empty stdout / non-zero exit → silent tick).

Closing in favor of #26229. Appreciate the contribution.

@teknium1 teknium1 closed this May 15, 2026
@iankar8

iankar8 commented May 15, 2026 via email

Copy link
Copy Markdown
Author

DIZ-admin pushed a commit to DIZ-admin/hermes-agent that referenced this pull request May 16, 2026
…ch#26229)

Adds three pre-run gate recipes to the cron docs:
- file-change gate (stat + mtime + state file)
- external-flag gate (file presence)
- SQL-count gate (user's own database, not state.db)

These are the use cases @iankar8 proposed adding as a parallel
'trigger' subsystem in NousResearch#2654. The existing `script` + `wakeAgent`
gate already covers all three at $0 — this lands the patterns as
documentation so users can find them, instead of adding a second
gating mechanism to the cron subsystem.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…ch#26229)

Adds three pre-run gate recipes to the cron docs:
- file-change gate (stat + mtime + state file)
- external-flag gate (file presence)
- SQL-count gate (user's own database, not state.db)

These are the use cases @iankar8 proposed adding as a parallel
'trigger' subsystem in NousResearch#2654. The existing `script` + `wakeAgent`
gate already covers all three at $0 — this lands the patterns as
documentation so users can find them, instead of adding a second
gating mechanism to the cron subsystem.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants