Hermes cron agent scheduler
Skill kjuhwa/skills-hub/skills/llm-agents/hermes-cron-agent-scheduler
Run an LLM agent on a cron schedule with inactivity timeout, silent marker, and cross-platform delivery.From its SKILL.md
npx -y skills add kjuhwa/skills-hub --skill hermes-cron-agent-schedulerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.9 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Cron-Driven LLM Agent with Inactivity Timeout
Context
Scheduled LLM agents (daily reports, nightly audits) have two failure modes that naive timeout() wrappers miss: (1) a long tool call is legitimate and should not count as "stuck", and (2) an empty model response should not look like a successful run.
Hermes' cron path solves both, plus adds platform-agnostic delivery and a [SILENT] marker the agent can emit to suppress delivery when there's nothing to report.
When to use
- Building a cron/scheduler layer that invokes an LLM agent.
- Agent runs can take minutes (browser work, long test suites).
- Delivery happens across multiple channels (Telegram, Slack, Discord, email, SMS, webhook).
- You want the agent itself to decide "nothing to say today".
Procedure
1. Advance next_run BEFORE executing
To avoid crash-loop re-fire, update the job's next_run_at before you start the agent, not after. See cron/scheduler.py:1028-1034:
for job in due_jobs:
advance_next_run(job["id"]) # future run already scheduled
success, output, final_response, error = run_job(job)
Keep one-shot jobs one-shot by only advancing recurring jobs.
2. File-lock the tick so overlapping instances don't double-fire
tick() takes an exclusive file lock (fcntl.flock on Unix, msvcrt.locking on Windows) so gateway + daemon + systemd-timer overlap is safe. Skipped ticks return 0 silently (cron/scheduler.py:1001-1082).
3. Drive the agent with an inactivity timeout, not a wall-clock one
A 2-hour compilation is not "stuck". Hermes polls the agent's get_activity_summary() and only aborts when seconds_since_activity exceeds the limit:
_cron_inactivity_limit = float(os.getenv("HERMES_CRON_TIMEOUT", 600))
_cron_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
_cron_context = contextvars.copy_context() # preserve ContextVars
_cron_future = _cron_pool.submit(_cron_context.run, agent.run_conversation, prompt)
while True:
done, _ = concurrent.futures.wait({_cron_future}, timeout=5.0)
if done:
result = _cron_future.result()
break
idle_secs = agent.get_activity_summary().get("seconds_since_activity", 0.0)
if idle_secs >= _cron_inactivity_limit:
agent.interrupt("Cron job timed out (inactivity)")
raise TimeoutError(...)
contextvars.copy_context() is essential — without it, any skill-declared env passthrough or session-context propagation set in the scheduler thread gets lost in the worker. See cron/scheduler.py:850-914.
4. Signal "nothing to report" via a sentinel
The scheduler prepends a system hint to every cron prompt:
"SILENT: If there is genuinely nothing new to report, respond with exactly
[SILENT](nothing else) to suppress delivery. Never combine [SILENT] with content."
After the run, the scheduler checks for the marker and suppresses delivery while still saving the output locally for audit (cron/scheduler.py:600-612, 1045-1049).
5. Validate script paths for pre-run data collection
Jobs can declare script: to run a data-collection script before the LLM call. Hermes forces the script inside HERMES_HOME/scripts/ via Path.resolve() + relative_to() to block path traversal, and also redacts secrets from stdout/stderr with redact_sensitive_text() before the output enters the prompt (cron/scheduler.py:486-564).
6. Delivery: live adapter first, standalone fallback
If the gateway is running, prefer its live adapter (supports E2EE rooms like Matrix). Otherwise spin up a fresh asyncio.run() in a worker thread:
runtime_adapter = (adapters or {}).get(platform)
if runtime_adapter is not None and loop is not None and loop.is_running():
future = asyncio.run_coroutine_threadsafe(runtime_adapter.send(...), loop)
future.result(timeout=60)
else:
# asyncio.run() + close-coro-on-RuntimeError fallback
try:
asyncio.run(coro)
except RuntimeError:
coro.close()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(asyncio.run, _send_to_platform(...))
future.result(timeout=30)
See cron/scheduler.py:370-445.
7. Treat empty response as soft failure
If the model completes but returns nothing, don't mark the job "ok" — that hides silent misconfiguration:
if success and not final_response:
success = False
error = "Agent completed but produced empty response..."
(cron/scheduler.py:1062-1065)
8. Disable toolsets the agent shouldn't have in cron context
agent = AIAgent(
...,
disabled_toolsets=["cronjob", "messaging", "clarify"],
skip_context_files=True, # no SOUL.md/AGENTS.md injection
skip_memory=True, # don't let cron corrupt user memory
platform="cron",
)
skip_memory=True prevents the scheduled prompt from being written back into persistent user-memory files.
Pitfalls
- Don't use a wall-clock timeout. A 20-minute pip install or 2-hour dataset download will be killed even though the agent is making progress.
- Don't forget
contextvars.copy_context()around the worker submit — ContextVars don't cross thread boundaries automatically. - Clean up injected env vars in a
finallyblock soHERMES_CRON_AUTO_DELIVER_*etc. don't leak into the next job. - Validate platform names against an allowlist (
_KNOWN_DELIVERY_PLATFORMS) to prevent env-var enumeration via crafteddeliver:values.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most context ai engineering skills give in ~1.3k tokens
Counted across 1,193 of the 1,976 authors here whose files we hold, read 2026-08-07
- Dispatch a fresh implementer subagent per taskin 48 of 1193, across 19 files
- Dispatch a final code reviewer after all tasksin 33 of 1193, across 8 files
- Provide full task text to the subagentin 30 of 1193, across 9 files
- Review spec compliance before code qualityin 27 of 1193, across 10 files
- Make the hook script executablein 26 of 1193, across 8 files
- Re-snapshot after navigation or DOM changesin 25 of 1193, across 19 files
- Read files before editing themin 22 of 1193, across 11 files
- Answer subagent questions before proceedingin 22 of 1193, across 7 files
- Mark task complete in TodoWrite after approvalin 22 of 1193, across 6 files
- Merge hook into existing settingsin 21 of 1193, across 3 files
- Ask if installation is global or projectin 20 of 1193, across 2 files
- Copy the hook script to target locationin 20 of 1193, across 2 files
Said here and by no other author read
- advance next run before executing job
- file-lock the tick to prevent overlap
- abort on inactivity timeout not wall-clock
- copy context before submitting worker thread
- suppress delivery on silent marker
- redact secrets from pre-run script output
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.