Keyed timers — detect the silence
You will build: a watchdog that fires when a device goes quiet — per-key timeout timers that turn absence of events into an event.
Prerequisites
- Comfortable with Windows & aggregations. Windows aggregate what arrives — this tutorial is about what doesn't.
1 · Scaffold
pulse new fleetwatch --source webhook --stage streaming:watchdog
2 · Declare the keyed timer
stages:
- name: watchdog
engine: streaming
operators:
- type: process
key: device_id
timeout: "10m"
timer: processing # processing · event
onTimeout: emit # emit · drop
passThrough: true
Each device_id gets its own timer. Every event from that device resets it; if timeout elapses with no event, the operator emits a timeout event for that key. passThrough: true keeps normal heartbeats flowing downstream too.
3 · Deploy and heartbeat two devices
pulse secret set PULSE_WEBHOOK_SECRET dev-secret # the scaffolded webhook source requires it
pulse deploy .
pulse events publish --topic fleetwatch.in --value '{"device_id":"d-1","battery":88}'
pulse events publish --topic fleetwatch.in --value '{"device_id":"d-2","battery":91}'
4 · Watch one go silent
pulse events tail --topic fleetwatch.watchdog.out
Keep d-1 heartbeating; stop sending for d-2. When d-2's timer expires, a timeout event for that key only appears — d-1's timer, still being reset, never fires.
For a quick demo, drop the timeout to "30s" — the semantics are identical.
5 · Chain the alert
Add a rule-based stage after the watchdog to route timeout events to your on-call flow — or an mcp stage to open the ticket directly (see Acting with MCP tools).
What just happened
You used the process operator — a keyed process function with per-key state and timeout timers. Windows answer "what happened per key in this period"; process answers "what should have happened and didn't". Together they cover both halves of monitoring: activity and silence.
Troubleshooting
The timeout never fires
With timer: processing the clock is wall-time from the last event for that key — confirm you actually stopped sending for it. timeout must be positive, and onTimeout: emit (not drop) is what makes the silence visible downstream.