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])
    raise

Claude 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"}'
fi

n8n

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.