Deadline monitors: scheduled work must finish on time
Register each run with an ingest key or the API, finish it with a span, log or check-in; a run without a success by its deadline alerts.
On this page
A deadline monitor watches work your app schedules for a specific time: a post set to publish at 08:30, an invoice run due on the 1st, a report promised by 09:00. Your app registers each run (an occurrence) before it is due; if no success arrives by the due time plus the grace period, Belvador alerts MONITOR_DEADLINE_MISSED.
A run that was never registered cannot be reported as missed. Register when the work is scheduled, not when it starts.
1. Create the monitor#
Monitors → Create monitor → Deadline. Choose:
Grace after the due time — 0 to 1440 minutes. The deadline of each run is its due time + grace.
Also alert when a started run goes quiet (optional) — see stalled runs.
Environments — each is judged on its own.
The monitor's slug is what your app sends.
2. Get an ingest key#
In Project → Settings → Ingest keys, create a key with the monitors scope (limit it to environments if you like) and store it as a server secret, for example BELVADOR_INGEST_KEY. Ingest keys are secrets: register from your server, never from a browser — requests with an Origin header are refused.
3. Register each run#
curl -fsS -m 10 --retry 3 -X POST "https://ingest.belvador.dev/api/<project id>/occurrences/" \ -H "Authorization: Bearer $BELVADOR_INGEST_KEY" \ -H "Content-Type: application/json" \ -d '{"monitor_slug":"scheduled-posts","occurrence_id":"post_8f2a91","due_at":"2026-11-02T08:30:00Z","environment":"production"}'Or from TypeScript on your server:
// Server only: the ingest key is a secret. Call when the job is scheduled, before it is due.export async function expectRun(occurrenceId: string, dueAt: Date, environment = "production") { const res = await fetch("https://ingest.belvador.dev/api/<project id>/occurrences/", { method: "POST", headers: { authorization: `Bearer ${process.env.BELVADOR_INGEST_KEY}`, "content-type": "application/json", }, body: JSON.stringify({ monitor_slug: "scheduled-posts", occurrence_id: occurrenceId, // the same id again moves its due time due_at: dueAt.toISOString(), environment, }), signal: AbortSignal.timeout(10_000), }); // 200 registered | rescheduled | duplicate. 429 and 503 carry Retry-After: try again later. if (!res.ok) throw new Error(`Belvador occurrence: HTTP ${res.status}`);}| Field | Rule |
|---|---|
monitor_slug | The deadline monitor's slug |
occurrence_id | Your id for this run, 1–128 characters of A–Z a–z 0–9 . _ : -. Use a UUID if the run finishes with a check-in |
due_at | ISO 8601 time with an offset, from 5 minutes ago up to 35 days ahead |
environment | Optional, production when absent; must be one the monitor watches and the key allows |
The body may be up to 4 KB. Answers:
| Status | Meaning |
|---|---|
200 registered / rescheduled / duplicate | Accepted. The same id again with a new due_at moves a waiting run |
400 | The body does not match the fields above |
401 / 403 | Missing or wrong key, a key without the monitors scope or for another project, a browser request, or an environment the key does not allow |
404 | No deadline monitor with this slug in the project |
422 | due_at out of range, an environment the monitor does not watch, or a run that already finished |
429 | Too many waiting runs (plan default 10,000 per monitor, 100,000 per project). Retry after Retry-After (300 s); each refusal is counted on the monitor page |
503 | Your node is unreachable right now. Nothing was registered — retry after Retry-After |
The same call is available as POST /v1/occurrences with an API token (scope monitors:write), for tools and AI agents.
4. Finish the run#
Any of these settles a run with the same id:
a span or log with
belvador.run.id= the occurrence id andbelvador.outcome=success(orfailure):
import { trace } from "@opentelemetry/api";// Inside the job: the span that does the work names the run and its outcome.await trace.getTracer("jobs").startActiveSpan("publish-post", async (span) => { span.setAttribute("belvador.run.id", occurrenceId); try { await publishPost(); span.setAttribute("belvador.outcome", "success"); } catch (err) { span.setAttribute("belvador.outcome", "failure"); throw err; } finally { span.end(); }});a check-in
ok(orerror) whosecheck_in_idis the occurrence id — check-in ids are UUIDs, so use UUID occurrence ids with this path.
A success before the deadline is on time. A failure alerts MONITOR_FAILED. A success that arrives after a missed deadline is recorded as late (up to 7 days later); the missed alert stays in the history and the monitor recovers.
Cancel a run#
If your app moves or drops the work, cancel the waiting run so it never alerts:
curl -fsS -m 10 --retry 3 -X DELETE "https://ingest.belvador.dev/api/<project id>/occurrences/" \ -H "Authorization: Bearer $BELVADOR_INGEST_KEY" \ -H "Content-Type: application/json" \ -d '{"monitor_slug":"scheduled-posts","occurrence_id":"post_8f2a91","environment":"production"}'DELETE /v1/occurrences/<monitor_slug>/<occurrence_id> does the same through the API.
What the monitor page shows#
Next due with a countdown, a strip of the last 24 hours (on time · late · missed · failed · cancelled · waiting), and the table of occurrences. Occurrence ids are shown exactly as sent and are never copied out of your node; alerts never include them.
Spotted something wrong or missing? Tell us: hello@belvador.dev