Kyōmei Nāda · Telemetry

Expect.

A scheduled-work observability library for Ruby projects. Declare what you expect, record what runs, detect the absence.

v1 · April 2026 Ruby 3.4 · Rails 8 Path-pinned · per-invitation

The class of failure this addresses

Every project has scheduled jobs that nobody watches until they break — and "break" usually means "stop running." A LaunchAgent gets disabled in macOS's override database. A cron entry rotates out. A Solid Queue worker exits before the job opens its run record. The schedule advances. Nothing logs the absence. The job's expected output simply never appears, and you find out when a downstream system notices three weeks later.

This library addresses that class of failure structurally rather than catching each instance individually. Projects declare what they expect to run, when, and within what grace window. The library records each run as it happens. A backend sweep compares actual runs against declared expectations and emits a missing event when a tick passes with no matching record.

The signal you get is the absence itself. A job that was supposed to run at 03:00 with a 30-minute grace window becomes missing at 03:30 — without anyone needing to remember to alert.

The API

Three methods, all on the Kn namespace.

# Declare what you expect to run, where, on what schedule.
# Run once at boot — for Rails, place in config/initializers/.
Kn.expect(
  "nightly-rollup",
  schedule: "0 3 * * * America/Chicago",
  grace: 30.minutes
)

# Wrap the actual work. The block runs; the wrapper records start,
# completion, outcome, and exit code automatically.
Kn.run("nightly-rollup") do
  RollupService.compute_for(Date.yesterday)
end

# For long-running or paused work, emit a heartbeat so the run
# isn't reported as lost.
Kn.checkin("nightly-rollup", metadata: { rows_processed: 12_482 })

The Kn.run wrapper opens an expectation_run row in the backend database before yielding, captures the block's outcome (ok, failed, error, timeout), records exit information, and closes the row on completion. If the block raises, the rescue path records failed and re-raises — the library does not swallow exceptions.

For LaunchAgent and cron jobs

Jobs that aren't already inside a Rails process use the bundled CLI. Same semantics — the CLI process opens the row, executes the script, captures the exit code, closes the row.

# In a LaunchAgent plist or cron line:
bundle exec kn run nightly-rollup -- /path/to/script.sh

Reference

Kn.expect(name, schedule:, grace:, **opts)
name — unique key within your project. schedule — five-field cron with optional IANA timezone suffix ("0 3 * * * America/Chicago"). Omitting the timezone falls back to UTC, which is rarely what you want. graceActiveSupport::Duration; the window in which a tick can land before being declared missing.
Kn.run(name, &block)
Opens a run record bound to name, yields, then closes the record with the block's outcome. Re-raises on exception after recording failed. Returns the block's return value.
Kn.checkin(name, metadata: {})
Heartbeat emit. Useful inside long blocks where the absence of progress for hours would otherwise look like a crash. Metadata is JSON-serialized into the run record.

How missing becomes observable

A backend job sweeps every minute. For each active expectation, it checks whether the most recent expected tick (computed from the cron schedule + timezone) has a matching run record within the grace window. If not, it inserts a synthetic row with outcome: "missing". Downstream observability surfaces — dashboards, session-start digests, alerts — read from the run records table.

The detection is structural, not heuristic: there is no "are you sure it's broken?" judgment to make. The expectation declares the contract; the absence of a matching record is the violation.

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│  Kn.expect  │ ──────▶ │  scheduled  │ ──────▶ │ tick fires  │
│  (at boot)  │         │ expectation │         │             │
└─────────────┘         └─────────────┘         └──────┬──────┘
                                                       
                                       ┌───────────────┴───────────────┐
                                                                      
                              ┌─────────────┐                 ┌─────────────┐
                              │  Kn.run     │                 │   no run    │
                              │  opens row  │                 │   (silent)  │
                              └──────┬──────┘                 └──────┬──────┘
                                                                    
                                                                    
                              ┌─────────────┐                 ┌─────────────┐
                              │   block     │                 │ grace +     │
                              │   yields    │                 │ sweep tick  │
                              └──────┬──────┘                 └──────┬──────┘
                                                                    
                                                                    
                              ┌─────────────┐                 ┌─────────────┐
                              │ row closes  │                 │ synthetic   │
                              outcome=ok  │                 │ outcome=     
                              /failed     │                 │ missing
                              └─────────────┘                 └─────────────┘
Lifecycle of one expectation, from boot to outcome

Origin

The library was written as a structural response to a recurring observation in a partner project: scheduled jobs failing silently, the failures invisible across multiple monitoring layers, the truth living only in tables nobody read. A pattern catalog accumulated seventeen distinct instances across ten consecutive nights before crystallizing as Asymmetric Observability — multiple sibling layers reporting success in concert while one uninspected layer held the lie.

This library is the layer that does not lie. The expectation row is the contract. The run records table is the audit log. The synthetic-missing sweep is the detector. There is no surface to misread.

It first shipped April 24, 2026, was wrapped around its own dreamer (the partner project's nightly memory-consolidation job) on day one, and caught a real silent failure inside that project on day two — the system's first observable output was identifying an instance of the pattern it was built to detect, in its own substrate.

Integrating into your project

The gem is part of the Kyōmei Nāda project. It is path-pinned within the workspace; integration is currently per-invitation rather than via a public registry.

If your project lives in the same workspace and you want to add it:

  1. Add the gem (path or git source) to your Gemfile.
  2. Create config/initializers/kyomei_nada.rb with one or more Kn.expect(...) calls.
  3. Wrap each declared expectation's actual work — Kn.run("name") { … } for in-Rails jobs; bundle exec kn run name -- … in LaunchAgents and cron lines.
  4. Run bin/rails runner 'KyomeiNada::BootSyncJob.perform_now' to push the declarations to the backend.
  5. Confirm the expectation registered: GET /api/projects/telemetry/health.

The first nightly tick after step 4 will be observable. Subsequent failures — silent or otherwise — will not be silent.

A note on what's not here

The library does not page anyone. It does not write to Slack. It does not send email. It deliberately stops at the database boundary: expectation_runs is the source of truth; the choice of how to surface that truth is left to the consumer. In the partner project, the consumer is a session-start digest that runs the interrupt-worthy standard — silent if everything is normal, loud if a tick is missing or a deadline is approaching. Other consumers (dashboards, push notifications, on-call alerts) are equally valid, and equally separable.

Keeping the structural detection separate from the human-attention surface is the point. The detection cannot lie about a missing tick. The surface can change without changing the contract.