A cron job fails at 3 AM. You wake up to a cryptic error in your inbox β or worse, no notification at all. You dig through logs, piece together what happened, and lose an hour before your first coffee.
What if the alert itself told you why the job failed, in plain English?
In this tutorial, weβll build a Python wrapper that executes any cron command, captures its output, and β when it fails β sends the logs to a local Ollama model for analysis. The AI-generated explanation gets delivered straight to Slack or email so you can triage without touching a terminal.
What Weβre Building
The workflow is simple:
- Run the scheduled command and capture stdout, stderr, and the exit code.
- Detect failure β any non-zero exit code triggers analysis.
- Analyze β send the captured output to Ollama for a plain-English explanation.
- Notify β push the explanation to a Slack webhook or send it via email.
You wrap your existing cron entries with this script, and every failure comes with context instead of noise.
Prerequisites
- Python 3.10+
- Ollama running locally with a model pulled (weβll use
llama3.1:8b) - A Slack incoming webhook URL or an SMTP server for email alerts
Install the single dependency we need:
pip install requests
Thatβs it. No frameworks, no SDKs β just the standard library plus requests for the Ollama and Slack HTTP calls.
The Complete Script
Create a file called cron_monitor.py:
#!/usr/bin/env python3
"""AI-powered cron job monitor. Wraps a command, analyzes failures with Ollama,
and sends notifications via Slack or email."""
import subprocess
import requests
import smtplib
import sys
import os
import json
from email.mime.text import MIMEText
from datetime import datetime, timezone
# --- Configuration via environment variables ---
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.1:8b")
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL", "")
SMTP_HOST = os.getenv("SMTP_HOST", "")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SMTP_USER = os.getenv("SMTP_USER", "")
SMTP_PASS = os.getenv("SMTP_PASS", "")
EMAIL_TO = os.getenv("EMAIL_TO", "")
EMAIL_FROM = os.getenv("EMAIL_FROM", SMTP_USER)
def run_command(cmd: list[str]) -> tuple[int, str, str]:
"""Execute a command and return exit code, stdout, stderr."""
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
return result.returncode, result.stdout, result.stderr
def analyze_failure(cmd: str, exit_code: int, stdout: str, stderr: str) -> str:
"""Send failure output to Ollama and get a plain-English explanation."""
# Truncate long output to stay within context limits
max_chars = 4000
stdout_trimmed = stdout[-max_chars:] if len(stdout) > max_chars else stdout
stderr_trimmed = stderr[-max_chars:] if len(stderr) > max_chars else stderr
prompt = f"""A cron job failed. Analyze the output and explain what went wrong
in 2-3 concise paragraphs. Suggest a fix if possible.
Command: {cmd}
Exit code: {exit_code}
Timestamp: {datetime.now(timezone.utc).isoformat()}
STDOUT (last {max_chars} chars):
{stdout_trimmed}
STDERR (last {max_chars} chars):
{stderr_trimmed}"""
resp = requests.post(
f"{OLLAMA_URL}/api/generate",
json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False},
timeout=120,
)
resp.raise_for_status()
return resp.json()["response"]
def notify_slack(cmd: str, exit_code: int, explanation: str):
"""Send the AI explanation to a Slack channel via webhook."""
payload = {
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": "π¨ Cron Job Failed"},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Command:*\n`{cmd}`"},
{"type": "mrkdwn", "text": f"*Exit code:*\n`{exit_code}`"},
],
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*AI Analysis:*\n{explanation}"},
},
]
}
requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=10)
def notify_email(cmd: str, exit_code: int, explanation: str):
"""Send the AI explanation via email."""
body = f"Command: {cmd}\nExit code: {exit_code}\n\nAI Analysis:\n{explanation}"
msg = MIMEText(body)
msg["Subject"] = f"Cron Failure: {cmd[:60]}"
msg["From"] = EMAIL_FROM
msg["To"] = EMAIL_TO
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.send_message(msg)
def main():
if len(sys.argv) < 2:
print("Usage: cron_monitor.py <command> [args...]", file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1:]
cmd_str = " ".join(cmd)
exit_code, stdout, stderr = run_command(cmd)
# Pass through output so normal logging still works
if stdout:
sys.stdout.write(stdout)
if stderr:
sys.stderr.write(stderr)
if exit_code == 0:
sys.exit(0)
# --- Failure path ---
print(f"\n[cron_monitor] Command failed with exit code {exit_code}. Analyzing...",
file=sys.stderr)
try:
explanation = analyze_failure(cmd_str, exit_code, stdout, stderr)
except Exception as e:
explanation = f"AI analysis unavailable: {e}"
if SLACK_WEBHOOK_URL:
notify_slack(cmd_str, exit_code, explanation)
if SMTP_HOST and EMAIL_TO:
notify_email(cmd_str, exit_code, explanation)
# Fallback: always print to stderr
print(f"\n[cron_monitor] AI Explanation:\n{explanation}", file=sys.stderr)
sys.exit(exit_code)
if __name__ == "__main__":
main()
How It Works
The script acts as a transparent wrapper. It runs whatever command you pass it via subprocess.run, capturing both output streams. If the command succeeds (exit code 0), the script exits silently β your existing logging is untouched.
On failure, it builds a prompt containing the command, exit code, timestamp, and the tail end of stdout/stderr. Output gets truncated to 4,000 characters per stream to avoid blowing past the modelβs context window. The prompt asks for a concise explanation and a suggested fix β nothing more.
The Ollama call uses the /api/generate endpoint with stream: False so we get the full response in one shot. If youβre running a different local model, just change the OLLAMA_MODEL environment variable.
Notifications go to Slack, email, or both β depending on which environment variables are set. If neither is configured, the explanation still prints to stderr so it shows up in your cron mail.
Wiring It Into Crontab
Replace your existing cron entries by prepending the monitor:
# Before
0 2 * * * /opt/scripts/backup.sh
# After
0 2 * * * /usr/bin/python3 /opt/scripts/cron_monitor.py /opt/scripts/backup.sh
Set the environment variables at the top of your crontab:
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxxx
0 2 * * * /usr/bin/python3 /opt/scripts/cron_monitor.py /opt/scripts/backup.sh
30 3 * * * /usr/bin/python3 /opt/scripts/cron_monitor.py /opt/scripts/db-vacuum.sh
Every failure now arrives in Slack with an explanation like:
The backup script failed because
/mnt/backupis not mounted. Thersynccommand returned exit code 23, indicating a partial transfer error. Check whether the NFS share is accessible and remount before re-running the job.
Compare that to a raw log dump. Night and day.
Testing It
Simulate a failure to verify the pipeline end-to-end:
# Export config for your shell session
export OLLAMA_MODEL=llama3.1:8b
export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxxx
# Run a command that will fail
python3 cron_monitor.py bash -c "echo 'disk full' >&2; exit 1"
You should see the AI explanation printed to stderr and a Slack message arrive within seconds.
Extending the Monitor
A few ideas to take this further:
- Log to a database β store every run (pass or fail) with timestamps, so you can track flaky jobs over time. This pairs well with an AI log analysis pipeline.
- Severity classification β ask the model to rate the failure as critical, warning, or transient, then route notifications accordingly.
- Auto-remediation β for known failure patterns, have the model suggest a shell command to fix the issue, then optionally execute it. See our guide on AI-driven incident response for patterns around safe auto-remediation.
- Retry logic β add a configurable retry count before triggering analysis, so transient network blips donβt waste an Ollama call.
Why Local Models Matter Here
Cron jobs often handle sensitive operations β database dumps, certificate rotations, deployment scripts. Sending that output to a cloud API means your internal infrastructure details leave your network. Running Ollama locally keeps everything on your machine. The analysis is fast enough (a few seconds on an 8B model) that the latency is negligible for a background monitoring task.
If youβre new to running models locally, the Ollama complete guide covers installation and model management from scratch.
Wrapping Up
In about 100 lines of Python, we built a cron monitor that turns cryptic failures into actionable explanations β all running locally, no API keys required. The wrapper is transparent to successful jobs and only kicks in when something breaks.
Drop it into your crontab, point it at Ollama, and stop deciphering raw logs at 3 AM.