Integrations
Works with every environment
CronObserver accepts a simple HTTPS check-in. If your platform can make an HTTP request, you can monitor it.
Agent platforms with a built-in scheduler
When the scheduler lives inside the agent, it can't be its own watchdog. Step-by-step setup, tested on each platform:
AI agents & LLM pipelines
Signal start, success and failure. A failed run alerts right away with the reason; a run that hangs past its max runtime, or never starts, alerts too.
Python agent (LangGraph, CrewAI or any LLM SDK)
The same runId ties start and end, so CronObserver records the duration. Empty output counts as a failure.
import json, os, urllib.request, uuid
BASE = os.environ["CRONOBSERVER_CHECKIN_URL"] # https://cronobserver.com/checkin/<token>
RUN_ID = str(uuid.uuid4())
def ping(suffix: str = "", **payload) -> None:
req = urllib.request.Request(
BASE + suffix,
data=json.dumps({"runId": RUN_ID, **payload}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
urllib.request.urlopen(req, timeout=10)
except Exception:
pass # monitoring must never break the job
ping("/start")
try:
result = run_agent()
if result.items == 0:
raise RuntimeError("agent produced nothing")
ping(model=result.model, tokens_in=result.tokens_in, tokens_out=result.tokens_out,
cost_usd=result.cost_usd, items_processed=result.items)
except Exception as exc:
ping("/fail", exitCode=1, message=str(exc)[:500])
raiseClaude Code / Codex routine
Cron calls a script (a crontab entry must fit on one line). test -s fails on an empty output, so an agent that "succeeds" with nothing to show still triggers an alert.
# crontab -e (every day at 21:00, host time)
CO_URL=https://cronobserver.com/checkin/<token>
0 21 * * * /opt/agents/summary/run.sh
# /opt/agents/summary/run.sh (chmod +x)
#!/bin/sh
cd /opt/agents/summary || exit 1
curl -fsS --retry 3 -X POST "$CO_URL/start"
if claude -p "$(cat prompt.md)" > out.md && test -s out.md; then
curl -fsS --retry 3 -X POST "$CO_URL"
else
curl -fsS --retry 3 -X POST "$CO_URL/fail" -H 'Content-Type: application/json' \
-d '{"exitCode":1,"message":"empty or failed run"}'
fin8n
Put the HTTP Request as the last node of the success path: if a node before it errors, no ping is sent. Add an Error Trigger workflow that POSTs to …/checkin/<token>/fail with the error, so the alert carries the reason right away.
Schedule Trigger → … your AI nodes … → HTTP Request
Method: POST
URL: https://cronobserver.com/checkin/<token>
Body: JSON (optional) {"status": "success", "items_processed": {{ $json.count }}}
Error Trigger → HTTP Request
Method: POST
URL: https://cronobserver.com/checkin/<token>/fail
Body: JSON {"message": "{{ $json.execution.error.message }}"}Make
Add it as the final module: Make stops the scenario on errors, so a failed run never checks in. On the error handler route, POST to …/checkin/<token>/fail to get the reason in the alert.
Scenario: … → HTTP › Make a request URL: https://cronobserver.com/checkin/<token> Method: POST Error handler route → HTTP › Make a request URL: https://cronobserver.com/checkin/<token>/fail Method: POST
GitHub Actions running an agent
Check in on success; report the failure (with a message) when any step fails.
on:
schedule:
- cron: "0 6 * * *"
jobs:
agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python agents/triage.py
- name: Check in
if: success()
run: |
curl -fsS --retry 3 -X POST "${{ secrets.CRONOBSERVER_CHECKIN_URL }}"
- name: Report failure
if: failure()
run: |
curl -fsS --retry 3 -X POST "${{ secrets.CRONOBSERVER_CHECKIN_URL }}/fail" \
-H 'Content-Type: application/json' -d '{"exitCode":1,"message":"workflow failed"}'Cron, containers and CI
Linux / Unix Cron
Add a curl call after your script finishes and include grace periods to avoid noisy alerts.
# crontab entries are one line each 0 * * * * /app/scripts/report.sh && curl -fsS --retry 3 -X POST https://cronobserver.com/checkin/<token>
Kubernetes CronJob
Wrap your job container and call the check-in endpoint once the work is done. Your image needs curl (alpine: apk add --no-cache curl).
apiVersion: batch/v1
kind: CronJob
metadata:
name: report-job
spec:
schedule: "*/30 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: curlimages/curl:8.11.1
command: ["sh", "-c", "run-task && curl -fsS --retry 3 -X POST https://cronobserver.com/checkin/<token>"]Laravel Scheduler
Use onSuccess hooks to send the heartbeat only when the artisan command completes successfully.
Schedule::command('reports:daily')
->daily()
->onSuccess(fn () =>
Http::post('https://cronobserver.com/checkin/<token>')
);Node.js / Fetch
Await your job promise, then fire a POST request to confirm the run.
await runJob();
await fetch('https://cronobserver.com/checkin/<token>', {
method: 'POST'
});CI Pipelines
Use curl or HTTP libraries in GitHub Actions, GitLab CI or any pipeline after the final step.
- name: heartbeat
run: |
curl -fsS -X POST https://cronobserver.com/checkin/<token>Need a specific stack?
If it can make an HTTP request when the job finishes, it works with CronObserver. Tell us your stack and we'll point you to the closest example.