Skip to content

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.

Markdown
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#

Terminal
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:

TypeScript
// 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}`);}
FieldRule
monitor_slugThe deadline monitor's slug
occurrence_idYour 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_atISO 8601 time with an offset, from 5 minutes ago up to 35 days ahead
environmentOptional, production when absent; must be one the monitor watches and the key allows

The body may be up to 4 KB. Answers:

StatusMeaning
200 registered / rescheduled / duplicateAccepted. The same id again with a new due_at moves a waiting run
400The body does not match the fields above
401 / 403Missing or wrong key, a key without the monitors scope or for another project, a browser request, or an environment the key does not allow
404No deadline monitor with this slug in the project
422due_at out of range, an environment the monitor does not watch, or a run that already finished
429Too 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
503Your 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 and belvador.outcome = success (or failure):

TypeScript
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 (or error) whose check_in_id is 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:

Terminal
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