Probe URLs on a cron schedule
Every five minutes, probe a set of target URLs, classify each as healthy or not, store the result, and serve the last recorded status of any target over HTTP.
commons status
---The KV key for a target's status. Namespacing keeps these entries separate fromanything else stored in the same namespace.---fn statusKey(name: String) -> String { "status:\(name)"}
---Is an HTTP status code healthy? A `2xx`/`3xx` code is; `code == 0` is theconvention for "the request never completed" (a network error), which is not.
This is the pure health policy of the monitor — `Int` in, `Bool` out — so itlives in `commons` and is unit-tested directly (see `tests/status.bynk`), whilethe platform `Response`/`FetchError` types and the stored `Status` record stay inthe context that produces them.---fn isHealthy(code: Int) -> Bool { code >= 200 && code < 400}context monitor
uses statusconsumes bynk { Fetch, Logger }consumes bynk.cloudflare { Kv } -- locks this unit to Cloudflare
---The last observed status of a monitored target. Stored as JSON in KV and readback by the status endpoint. `code` is `0` when the request never completed.
The record stays in the context (records carry a context identity, so they aredefined where they are stored/served); the pure health policy that fills `ok`is `isHealthy` in `commons status`.---type Status = { name: String, ok: Bool, code: Int, at: Int,}
---Every five minutes, probe each target and record its status. `at` is theschedule-aligned instant in epoch-ms — cron has no ambient clock, so the time ishanded in.
The fetch/store steps are written inline per target: capabilities (`given`) liveon handlers, not on free functions, and an external type like `Request` can onlybe constructed where it is used — so the effectful work stays in the handler. Theplatform-typed `match` (over `Result[Response, FetchError]`) reduces each outcometo an HTTP code; `isHealthy` (from `commons status`) classifies it.---service checks from cron { on schedule("*/5 * * * *") (at: Int) -> Effect[Result[(), String]] given Fetch, Kv, Logger { let exampleRes <- Fetch.send(Request { method: Get, url: "https://example.com", contentType: None, authorization: None, body: None, }) let exampleCode = match exampleRes { Ok(r) => r.status Err(_) => 0 } let example = Status { name: "example", ok: isHealthy(exampleCode), code: exampleCode, at: at } let _ <- Kv.put(statusKey("example"), Json.encode(example)) let _ <- Logger.info("checked example: \(example.code)")
let cfRes <- Fetch.send(Request { method: Get, url: "https://www.cloudflare.com", contentType: None, authorization: None, body: None, }) let cfCode = match cfRes { Ok(r) => r.status Err(_) => 0 } let cf = Status { name: "cloudflare", ok: isHealthy(cfCode), code: cfCode, at: at } let _ <- Kv.put(statusKey("cloudflare"), Json.encode(cf)) let _ <- Logger.info("checked cloudflare: \(cf.code)")
Ok(()) }}
---Read the last recorded status for a target by name.---service api from http { on GET("/status/:name") by Visitor (name: String) -> Effect[HttpResult[Status]] given Kv { let stored <- Kv.get(statusKey(name)) match stored { None => NotFound Some(s) => match Json.decode[Status](s) { Ok(st) => Ok(st) Err(_) => ServerError("stored status is corrupt") } } }}This example reaches Workers-only shapes (storage bindings, agents, or cron), so it runs with bynk dev rather than in the browser playground. See Install to get started.
How it works
Section titled “How it works”context monitor consumes bynk { Fetch, Logger } and bynk.cloudflare { Kv }. The
pure parts live in commons status: statusKey(name) namespaces a KV key as
status:…, and isHealthy(code) is the health policy — Int in, Bool out, true
for 2xx/3xx, false for 4xx/5xx and for code == 0 (the convention for a
request that never completed). Both are unit-tested without Fetch or Kv.
The cron entry point is service checks from cron { on schedule("*/5 * * * *") (at: Int) … }. Cron has no ambient clock, so the schedule-aligned instant arrives as the
at parameter (epoch-ms). For each target the handler builds a
Request { method: Get, … } and calls Fetch.send, which returns a
Result[Response, FetchError] — a network failure is a value, not an exception. A
match reduces each outcome to an HTTP code (0 on Err), isHealthy classifies
it, and the resulting Status record is written with Kv.put and Json.encode,
with a Logger.info line per check. The fetch/store steps are written inline per
target because capabilities live on handlers, not free functions, and a Request can
only be built where it is used.
The read side is a separate HTTP service: GET /status/:name reads the stored JSON
back through Json.decode[Status], returning NotFound for a missing key and
ServerError for a corrupt value.