# How Cron Jobs Persist Output and Integrate with the Task Panel in Hermes WebUI

> Learn how Hermes WebUI's cron jobs persist output to markdown files and integrate with the Task panel. Discover the API endpoint for fetching content snippets and SSE event handling.

- Repository: [Nathan Esquenazi/hermes-webui](https://github.com/nesquena/hermes-webui)
- Tags: how-to-guide
- Published: 2026-06-01

---

**Hermes WebUI writes cron job results to disk as markdown files, emits completion events via Server-Sent Events, and refreshes the Task panel by fetching bounded content snippets through the `/api/crons/output` endpoint.**

The `nesquena/hermes-webui` repository implements a complete workflow for managing automated cron jobs through its Python backend and JavaScript frontend. When any scheduled or manual job executes, the system handles output persistence, status tracking, and real-time UI updates through a tightly coupled three-phase architecture. This article examines the specific mechanisms that enable cron job output persistence and task panel integration according to the source code.

## Executing and Persisting Cron Job Output

When a cron job runs—whether triggered manually via `POST /api/crons/run` or on a schedule—the backend executes the task within an isolated profile subprocess and immediately persists the results.

### The Tracked Execution Flow

The entry point `_run_cron_tracked` in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) orchestrates the execution. This helper spawns the actual job via `_run_cron_job_in_profile_subprocess` and receives the raw output, success flag, and any error messages upon completion.

Immediately after the subprocess returns, the system persists the data through two distinct calls:

```python

# From api/routes.py - the persistence sequence

save_job_output(job_id, output)                     # Line 876

mark_job_run(job_id, success, error, delivery_error)  # Line 849

```

The `save_job_output` function writes the complete output to the filesystem, while `mark_job_run` updates the job's status history with the success boolean, error message, and any delivery errors.

### Recording Status and Results

According to the implementation in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py), the `save_job_output` function creates timestamped markdown files under `cron/output/<job_id>/` that serve as the permanent record for each execution. This disk-based storage ensures output survives server restarts and remains accessible for historical analysis.

After persistence completes, the system notifies the frontend that new data is available.

## Broadcasting Completion Events

Hermes WebUI uses an in-process Server-Sent Events (SSE) mechanism to push real-time updates to connected browsers without requiring polling.

### SSE Event Publishing in session_events.py

The `publish_session_list_changed` function in [`api/session_events.py`](https://github.com/nesquena/hermes-webui/blob/main/api/session_events.py) handles the notification layer. When a tracked run finishes—regardless of success or failure—the backend invokes this publisher at line 3900 of [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) with a specific reason code:

```python
publish_session_list_changed("cron_complete")

```

The implementation in [`api/session_events.py`](https://github.com/nesquena/hermes-webui/blob/main/api/session_events.py) (lines 11-21) increments an internal version counter and broadcasts a JSON payload to all subscribers:

```json
{"type":"sessions_changed","version":42,"reason":"cron_complete"}

```

This lightweight event signals the frontend that cron-related data has changed, triggering a refresh of the Task panel displays.

## Retrieving and Displaying Output in the Task Panel

The frontend consumes the SSE notification and fetches the actual content through a dedicated API endpoint that implements content windowing to manage payload size.

### The Output API Endpoint

The `GET /api/crons/output` endpoint defined in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) (lines 9347-9363) serves stored execution results. The `_handle_cron_output` handler locates the job-specific directory, reads the newest markdown files, and returns them as a JSON array:

```python

# Simplified logic from api/routes.py

def _handle_cron_output(handler, parsed):
    job_id = parse_qs(parsed.query).get("job_id", [""])[0]
    out_dir = CRON_OUT / job_id  # e.g., ~/.hermes/cron/output/<id>

    
    for f in sorted(out_dir.glob("*.md"), reverse=True)[:limit]:
        txt = f.read_text(encoding="utf-8")
        outputs.append({
            "filename": f.name,
            "content": _cron_output_content_window(txt)
        })
    return j(handler, {"job_id": job_id, "outputs": outputs})

```

### Content Windowing for Performance

To prevent overwhelming the UI with large prompt dumps, the `_cron_output_content_window` function at lines 640-660 of [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) extracts only the relevant sections. This helper specifically preserves the `## Response` section containing the agent's reply while truncating earlier context, ensuring the Task panel displays concise, actionable information.

### Frontend Integration

The browser-side code in [`static/workspace.js`](https://github.com/nesquena/hermes-webui/blob/main/static/workspace.js) subscribes to the SSE stream via `subscribe_session_events()`. When it receives a `sessions_changed` event with `reason: "cron_complete"`, it triggers `refreshCronRunsCard()` to call the output endpoint and rebuild the Runs card in the Task sidebar.

```javascript
// From static/workspace.js
const evtQueue = subscribeSessionEvents();
evtQueue.addEventListener("message", ev => {
  const payload = JSON.parse(ev.data);
  if (payload.reason === "cron_complete") {
    refreshCronRunsCard();   // Re-fetches /api/crons/output
  }
});

```

The UI template references the `cron_last_output` internationalization key (verified in [`tests/test_sprint10.py`](https://github.com/nesquena/hermes-webui/blob/main/tests/test_sprint10.py) lines 109-112) to display these snippets inline within the Task panel, completing the integration cycle.

## Summary

- **Hermes WebUI** stores cron job output as markdown files in `cron/output/<job_id>/` via `save_job_output` in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py).
- **Status tracking** occurs through `mark_job_run`, which records success flags, error messages, and delivery errors alongside the persisted output.
- **Real-time updates** flow through `publish_session_list_changed` in [`api/session_events.py`](https://github.com/nesquena/hermes-webui/blob/main/api/session_events.py), emitting SSE events with reason `"cron_complete"`.
- **The Task panel** fetches bounded snippets via `GET /api/crons/output`, processed by `_cron_output_content_window` to extract only the `## Response` section.

- **Frontend listeners** in [`static/workspace.js`](https://github.com/nesquena/hermes-webui/blob/main/static/workspace.js) subscribe to these events and automatically refresh the Runs card when execution completes.

## Frequently Asked Questions

### Where does Hermes WebUI store cron job output files?

The system writes execution output to the filesystem under `cron/output/<job_id>/` as markdown files. The `save_job_output` function in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) handles this disk persistence at line 876, ensuring results remain available after server restarts and can be retrieved later by the frontend through the output API endpoint.

### How does the Task panel know when a cron job finishes?

The backend emits a Server-Sent Event via `publish_session_list_changed("cron_complete")` defined in [`api/session_events.py`](https://github.com/nesquena/hermes-webui/blob/main/api/session_events.py) (lines 11-21). The JavaScript frontend subscribes to this SSE stream in [`static/workspace.js`](https://github.com/nesquena/hermes-webui/blob/main/static/workspace.js) and triggers `refreshCronRunsCard()` upon receiving `sessions_changed` events with reason `"cron_complete"`, causing an immediate refresh of the displayed output.

### What mechanism prevents large cron outputs from slowing down the UI?

The `_cron_output_content_window` function in [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py) (lines 640-660) implements content windowing by extracting only the `## Response` section from stored markdown files. This limits the payload size returned by `GET /api/crons/output`, ensuring the Task panel receives concise agent replies without transmitting full prompt histories.

### Which API endpoints handle manual cron execution and output retrieval?

Manual execution flows through `POST /api/crons/run`, which invokes `_run_cron_tracked` to handle the subprocess creation and persistence sequence. Output retrieval uses `GET /api/crons/output`, handled by `_handle_cron_output` at lines 9347-9363 of [`api/routes.py`](https://github.com/nesquena/hermes-webui/blob/main/api/routes.py), which reads the newest markdown files and applies content windowing before returning JSON to the frontend.