The Digest API gives you run health as rollups over a window. This endpoint gives you the raw material instead: one record per individual execution, with its timestamp, status, duration, and — for failed runs — the error message. Use it to feed your own monitoring, warehouse, or incident tooling.

Authentication is the same one-token flow as the Digest API: create a Personal Access Token under Settings → Access Tokens (/a/tokens), then exchange it for a short-lived access key. The Digest API guide walks through both steps; the short version:

JWT=$(curl -s -X POST https://pushmetrics.io/api/v1/security/exchange \
  -H "Authorization: Bearer pmpat_YOUR_TOKEN" | jq -r .access_token)

The endpoint

curl -s "https://pushmetrics.io/api/v1/noteflow_run/history?page_size=100" \
  -H "Authorization: Bearer $JWT"

All parameters are optional:

Parameter Meaning
report_uid Only runs of one report. The uid is the identifier in the report's URL in the app.
status One of pending, running, skipped, failed, success
from / to ISO-8601 bounds on when the run was created, in UTC. from is inclusive, to exclusive. Date-only values like 2026-07-01 work.
page Page number, starting at 0
page_size Rows per page, 1 to 1,000. Default 100.

Invalid input returns a 422 with a message saying what's wrong — a typo'd status or a malformed timestamp never fails silently.

What comes back

{
  "count": 72623,
  "page": 0,
  "page_size": 100,
  "result": [
    {
      "run_uid": "lD6XAqbB2K",
      "report_uid": "pjoVBM6oYP",
      "name": "Weekly KPI Report",
      "status": "success",
      "created_at": "2026-07-27T10:47:34Z",
      "started_at": "2026-07-27T10:47:35Z",
      "finished_at": "2026-07-27T10:47:36Z",
      "duration_s": 1.155,
      "error": null
    }
  ]
}
Field Meaning
run_uid The run's identifier; links to /a/log in the app
report_uid The report this run executed, null for report-less runs (e.g. some agent runs)
name The report's name at execution time
status pending, running, skipped, failed, or success
created_at When the run was queued (UTC)
started_at / finished_at When execution began and ended; null while pending
duration_s Execution time in seconds, null until finished
error The failure message, on failed runs only

Rows are ordered newest first. count is the total matching your filters, so count > (page + 1) * page_size means there are more pages. Retries are not listed as separate rows — a re-run stays attached to its original run, exactly as in the app's Execution Log.

You see the runs of reports shared with you; workspace admins see every run in the workspace. The token's workspace binding applies, same as everywhere else.

Useful one-liners

BASE="https://pushmetrics.io/api/v1/noteflow_run/history"

# Everything that failed this week, with the actual error
curl -s "$BASE?status=failed&from=2026-07-22" -H "Authorization: Bearer $JWT" \
  | jq -r '.result[] | [.created_at, .name, .error] | @tsv'

# Full run log for one report as a CSV
curl -s "$BASE?report_uid=pjoVBM6oYP&page_size=1000" -H "Authorization: Bearer $JWT" \
  | jq -r '.result[] | [.run_uid, .status, .started_at, .finished_at, .duration_s] | @csv'

# Success rate over a window, computed your way
curl -s "$BASE?from=2026-07-01&to=2026-08-01&page_size=1000" -H "Authorization: Bearer $JWT" \
  | jq '[.result[].status] | group_by(.) | map({(.[0]): length}) | add'

A small Python client

import requests

BASE = "https://pushmetrics.io/api/v1"
PAT = "pmpat_..."   # from your secrets store

def get_jwt():
    r = requests.post(f"{BASE}/security/exchange",
                      headers={"Authorization": f"Bearer {PAT}"})
    r.raise_for_status()
    return r.json()["access_token"]

def fetch_runs(jwt, **params):
    """Yield every run matching the filters, walking the pages."""
    page = 0
    while True:
        r = requests.get(f"{BASE}/noteflow_run/history",
                         params={**params, "page": page, "page_size": 1000},
                         headers={"Authorization": f"Bearer {jwt}"})
        if r.status_code == 401:   # key expired mid-walk, get a fresh one
            jwt = get_jwt()
            continue
        r.raise_for_status()
        body = r.json()
        yield from body["result"]
        page += 1
        if page * body["page_size"] >= body["count"]:
            return

jwt = get_jwt()
for run in fetch_runs(jwt, status="failed", **{"from": "2026-07-01"}):
    print(f"{run['created_at']}  {run['name']}: {run['error']}")

Good practices

  • Sync incrementally. Pass from with your last sync time instead of re-walking the full history; runs are immutable once finished.
  • Poll at a sane cadence. Runs land at schedule granularity; every few minutes is plenty, and the digest endpoints are the better fit for once-a-day health summaries.
  • Treat count as your truncation check when exporting — if it grows while you page, new runs arrived; overlap your next from window slightly.
  • Revoke tokens you stop using on the Access Tokens page.