cronlag

Usage

Report

cronlag                                   # repo from the git remote of the current directory
cronlag owner/repo --days 14
cronlag owner/repo -w nightly.yml -f markdown   # paste into an issue

One block per scheduled workflow: the cron line(s), a strip with one glyph per slot (bar height = lag, ✕ = no run, ? = no run yet), lag min/median/p90/max, on-time and within-1-hour counts, a verdict, and the cause checks with the fix to apply.

Org-wide scan

cronlag org my-org
cronlag --org my-org --days 14 --fail-on late
cronlag org my-org -f markdown

Lists every non-archived repo in the org, finds those with on: schedule workflows, runs the same report pipeline on each (in --lite mode for fewer API calls), and prints a compact summary sorted worst first. Text, JSON and Markdown are supported; --fail-on exits 1 if any scanned repo hits the threshold. Progress goes to stderr when the terminal is a TTY. Archived repos are skipped; non-archived forks are still scanned (the fork cause check explains why schedules never fire).

Catch-up

cronlag catchup owner/repo              # dry run: what would be dispatched
cronlag catchup owner/repo --dispatch   # do it

For each scheduled workflow, cronlag takes the latest slot older than the grace period (--grace, default 15m) and younger than 24h. If neither a scheduled run nor a workflow_dispatch run was created after it, --dispatch starts the workflow on the default branch. Running it again does nothing until the next slot is due, so call it every 10–15 minutes from a scheduler that isn't GitHub's:

# crontab on any server
*/10 * * * *  GITHUB_TOKEN=github_pat_… npx -y github:Blackman99/cronlag catchup you/repo --dispatch >> ~/cronlag.log 2>&1

The workflow must have a workflow_dispatch: trigger.

Guard: no double runs

When GitHub's late scheduled run finally arrives after catch-up already ran that slot, the guard tells it to skip:

jobs:
  guard:
    runs-on: ubuntu-latest
    permissions: { actions: read, contents: read }
    outputs: { skip: ${{ steps.g.outputs.skip }} }
    steps:
      - id: g
        uses: Blackman99/cronlag@v0
        with: { mode: guard }
  work:
    needs: guard
    if: needs.guard.outputs.skip != 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "the real job"

On non-schedule events the guard always says skip=false. It also outputs slot and lag-minutes for this run.

Explain a cron line

cronlag explain "0 */6 * * *"
cronlag explain "30 5 * * 1-5" --tz America/New_York -n 10

GitHub Action (report)

on:
  schedule: [{ cron: "41 7 * * 1" }]   # weekly; yes, a schedule — it reports on all the others
  workflow_dispatch:
permissions: { actions: read, contents: read }
jobs:
  lag:
    runs-on: ubuntu-latest
    steps:
      - uses: Blackman99/cronlag@v0
        with:
          days: 7
          fail-on: dropped

Writes the Markdown report to the job summary and sets outputs verdict, missed, median-lag-minutes, json.

MCP server

{
  "mcpServers": {
    "cronlag": { "command": "npx", "args": ["-y", "github:Blackman99/cronlag", "mcp"], "env": { "GITHUB_TOKEN": "…" } }
  }
}

Tools (all read-only): schedule_report {repo, days?, workflow?}, catchup_plan {repo, grace_minutes?}, cron_explain {cron, timezone?, count?}. The plan never dispatches; an agent that should act runs cronlag catchup --dispatch itself, under whatever approval you require.