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

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 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:


# 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, 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 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 with a specific reason code:

publish_session_list_changed("cron_complete")

The implementation in api/session_events.py (lines 11-21) increments an internal version counter and broadcasts a JSON payload to all subscribers:

{"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 (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:


# 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 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 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.

// 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 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.

  • 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, 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 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 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 (lines 11-21). The JavaScript frontend subscribes to this SSE stream in 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 (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, which reads the newest markdown files and applies content windowing before returning JSON to the frontend.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →