# How the Cron Scheduler Integrates with the Messaging Gateway in Hermes Agent

> Discover how Hermes Agent's cron scheduler integrates with the messaging gateway to automate task delivery. Learn how it runs jobs and sends messages to chat platforms.

- Repository: [Nous Research/hermes-agent](https://github.com/NousResearch/hermes-agent)
- Tags: how-to-guide
- Published: 2026-03-09

---

**The Hermes Agent cron scheduler runs as a background thread inside the gateway process, executing due jobs every 60 seconds and routing their output to chat platforms via the unified `send_message` tool.**

The NousResearch/hermes-agent repository combines a Python-based cron scheduler with a multi-platform messaging gateway to enable fully automated, agent-driven workflows. When a scheduled job completes, the system seamlessly hands the result to the messaging gateway for delivery to Telegram, Discord, Slack, or other configured channels without requiring external cron daemons or manual intervention.

## The Gateway's Background Cron Ticker

When you launch the gateway with `hermes gateway start`, the main process spawns a daemon thread that keeps the cron scheduler alive. In [`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py), the `_start_cron_ticker` function creates this persistent loop:

```python

# gateway/run.py – lines 2855-2866

def _start_cron_ticker(stop_event: threading.Event, adapters=None, interval: int = 60):
    """
    Background thread that ticks the cron scheduler at a regular interval.
    Runs inside the gateway process so cronjobs fire automatically without
    needing a separate `hermes cron daemon` or system cron entry.
    """
    from cron.scheduler import tick as cron_tick
    ...
    while not stop_event.is_set():
        cron_tick(verbose=False)          # invokes the scheduler

        stop_event.wait(timeout=interval)

```

This thread calls `cron.scheduler.tick()` every 60 seconds, ensuring jobs fire on time even when no user is actively chatting with the agent. The implementation uses a `threading.Event` for clean shutdown and runs entirely in-process, eliminating the need for separate cron services or system-level cron entries.

## Job Execution Pipeline

The core scheduling logic resides in [`cron/scheduler.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/scheduler.py). The `tick()` function checks for due jobs, executes them, and triggers delivery.

### Tick Loop and Due Job Detection

Each tick acquires a file lock to prevent overlapping executions between the gateway ticker and any standalone `hermes cron tick` commands:

```python

# cron/scheduler.py

def tick(verbose: bool = True) -> int:
    """
    Check and run all due jobs.
    Uses a file lock so only one tick runs at a time.
    """
    # ... acquire lock ...

    due_jobs = get_due_jobs()              # from cron/jobs.py

    for job in due_jobs:
        success, output, final_response, error = run_job(job)
        # ... store output, mark run, deliver result ...

```

The `get_due_jobs()` helper (from [`cron/jobs.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/jobs.py)) queries the persistent [`jobs.json`](https://github.com/NousResearch/hermes-agent/blob/main/jobs.json) store to find jobs whose next run time has passed.

### Creating the AIAgent Instance

For each due job, `run_job()` constructs a temporary **AIAgent** instance that executes the user's prompt using the same inference stack as interactive chats:

```python

# cron/scheduler.py – run_job()

agent = AIAgent(
    model=model,
    api_key=runtime.get("api_key"),
    ...,
    session_id=f"cron_{job_id}_{_hermes_now().strftime('%Y%m%d_%H%M%S')}"
)
result = agent.run_conversation(prompt)
final_response = result.get("final_response", "")

```

The agent receives a unique `session_id` formatted as `cron_{job_id}_{timestamp}`, isolating cron runs from user conversations while maintaining full access to tools and configuration. This design allows scheduled jobs to perform web searches, file operations, or API calls exactly like human-driven sessions.

### Persisting Output to Disk

After execution, the scheduler saves a markdown transcript to `~/.hermes/cron/output/{job_id}/{timestamp}.md` via `save_job_output()`:

```python
output_file = save_job_output(job["id"], output)

```

Simultaneously, `mark_job_run()` (from [`cron/jobs.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/jobs.py)) updates the job metadata with the next scheduled time, success status, and any error text.

## Delivering Results Through the Messaging Gateway

The final integration point is `_deliver_result()`, which bridges the scheduler and the messaging gateway. This function determines the destination platform and chat, then hands the content to the cross-platform send-message infrastructure.

### Platform and Channel Resolution

The scheduler supports three delivery modes configured per job:

- **`deliver="local"`**: Write output to disk only; no message sent
- **`deliver="origin"`**: Send back to the chat where the job was created
- **`deliver="platform"`** or **`deliver="platform:chat_id"`**: Send to a specific platform (Telegram, Discord, Slack) or explicit channel

If the target platform is specified without a chat ID, the system falls back to the **home channel** defined in environment variables (e.g., `TELEGRAM_HOME_CHANNEL`), emitting a warning when using defaults.

### The Unified Send-Message Tool

The actual API transmission happens through [`tools/send_message_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/send_message_tool.py). The `_deliver_result()` function invokes `_send_to_platform()`, which abstracts platform-specific protocols:

```python

# cron/scheduler.py – _deliver_result()

def _deliver_result(job: dict, content: str) -> None:
    deliver = job.get("deliver", "local")
    ...
    from tools.send_message_tool import _send_to_platform
    result = asyncio.run(_send_to_platform(platform, pconfig, chat_id, content))

```

The send-message tool handles authentication, rate limiting, and API formatting for each backend:

```python

# tools/send_message_tool.py

async def _send_to_platform(platform, pconfig, chat_id, message):
    if platform == Platform.TELEGRAM:
        return await _send_telegram(pconfig.token, chat_id, message)
    elif platform == Platform.DISCORD:
        return await _send_discord(pconfig.token, chat_id, message)
    elif platform == Platform.SLACK:
        return await _send_slack(pconfig.token, chat_id, message)
    ...

```

Because the scheduler runs inside the gateway process, `asyncio.run()` executes safely even from the background ticker thread. If an event loop is already running, the tool automatically falls back to a temporary executor thread (lines 119-128 of [`send_message_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/send_message_tool.py)).

## Complete Workflow Example

Consider scheduling a daily morning briefing:

```bash
hermes cron add "0 8 * * *" \
    "Search the web for AI news from the last 24h and summarise. Deliver to telegram." \
    --name "daily-briefing" --deliver telegram

```

The internal execution flow follows this path:

1. **08:00 UTC**: [`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py) → `_start_cron_ticker` invokes `cron.scheduler.tick()`
2. **Job execution**: [`cron/scheduler.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/scheduler.py) → `run_job()` creates an `AIAgent`, fetches news, and generates `final_response`
3. **Persistence**: Output written to `~/.hermes/cron/output/daily-briefing/20240115_080000.md`
4. **Delivery**: `_deliver_result()` resolves the Telegram home channel from [`gateway/config.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/config.py) and calls `_send_to_platform()`
5. **Receipt**: The message appears in the Telegram channel; [`gateway/mirror.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/mirror.py) reflects it back into the session history for UI consistency

## Summary

- The **cron scheduler** runs as a daemon thread inside the Hermes Agent gateway, ticking every 60 seconds via `_start_cron_ticker` in [`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py).
- Job execution creates ephemeral **AIAgent** instances with isolated session IDs, ensuring cron tasks have full tool access without polluting user conversation history.
- Results are persisted to disk in `~/.hermes/cron/output/` and then routed through the **messaging gateway** using the unified `send_message` tool.
- The **send-message tool** ([`tools/send_message_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/send_message_tool.py)) abstracts platform APIs (Telegram, Discord, Slack), allowing the scheduler to deliver messages to origin chats, home channels, or explicit targets without platform-specific code.
- File locking in [`cron/scheduler.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/scheduler.py) prevents race conditions between the gateway ticker and standalone CLI invocations like `hermes cron tick`.

## Frequently Asked Questions

### How does the cron scheduler prevent duplicate job executions when both the gateway and CLI are running?

The `tick()` function in [`cron/scheduler.py`](https://github.com/NousResearch/hermes-agent/blob/main/cron/scheduler.py) acquires a file-level lock before checking for due jobs. This locking mechanism ensures that only one process—whether the gateway's background thread or a standalone `hermes cron tick` command—can execute the scheduler at any given time, preventing double-runs and race conditions.

### Can scheduled jobs send messages to different platforms than where they were created?

Yes. The `deliver` parameter in `_deliver_result()` accepts platform-specific targets such as `telegram`, `discord`, or `slack`. You can also specify explicit channels using the `platform:chat_id` syntax. If no chat ID is provided, the scheduler falls back to the configured home channel (e.g., `TELEGRAM_HOME_CHANNEL` from [`gateway/config.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/config.py)).

### What happens if the messaging gateway is offline when a cron job finishes?

The cron scheduler writes output to disk immediately via `save_job_output()` before attempting delivery. If the gateway is unreachable or the platform API fails, the output remains stored in `~/.hermes/cron/output/`. The next successful tick will not re-send failed deliveries automatically; you must check the output files or logs for failure notifications.

### How does the scheduler handle asynchronous message sending from a background thread?

The `_deliver_result()` function uses `asyncio.run()` to execute the coroutine `_send_to_platform()` synchronously. Because the cron ticker runs in a daemon thread within the gateway process, this pattern works safely. The send-message tool also includes fallback logic (lines 119-128 in [`tools/send_message_tool.py`](https://github.com/NousResearch/hermes-agent/blob/main/tools/send_message_tool.py)) that detects existing event loops and delegates to a temporary thread if necessary, preventing `RuntimeError` exceptions.