Pollers — Event-Driven Monitoring
Pollers are lightweight scripts that check external services on a schedule and report back when something needs attention. They live inside skills and are discovered automatically by the scheduler.
How It Works
- A skill includes a
pollers.jsonfile alongside itsSKILL.md - The scheduler discovers all
pollers.jsonfiles at startup and whenreload_pollersis called - On each cron tick, the scheduler runs the poller command as a subprocess
- Each line of stdout is parsed as JSON and delivered to the agent as an event
- If there's nothing to report, the poller outputs nothing — silence is the filter
Creating a Poller
1. Write the poller script
The script runs in the skill directory. It receives these environment variables automatically:
| Variable | Description |
|---|---|
STATE_DIR | The skill directory (writable, for cursors/state) |
POLLER_NAME | The poller's name from pollers.json |
Plus any custom env vars from the env field, plus the agent's existing environment.
Output contract:
- stdout: JSONL (one JSON object per line). Each line must have
poller(string) andprompt(string) fields. - stderr: Free-form logging. Not forwarded to the agent.
- Exit 0: Success. Non-zero: Error, this cycle is skipped.
Example poller script:
python#!/usr/bin/env python3 """Check for new items since last poll.""" import json, os, sys from pathlib import Path STATE_DIR = Path(os.environ.get("STATE_DIR", ".")) CURSOR_FILE = STATE_DIR / "cursor.json" def load_cursor(): if CURSOR_FILE.exists(): return json.loads(CURSOR_FILE.read_text()) return {} def save_cursor(cursor): CURSOR_FILE.write_text(json.dumps(cursor, indent=2)) def main(): cursor = load_cursor() # ... check your service, compare against cursor ... new_items = [] # your logic here for item in new_items: event = { "poller": os.environ.get("POLLER_NAME", "my-poller"), "prompt": f"New item: {item['title']}" } print(json.dumps(event)) # Update cursor so next run skips these items save_cursor(cursor) if __name__ == "__main__": main()
2. Create pollers.json in the skill directory
json{ "pollers": [ { "name": "my-service-check", "command": "python poller.py", "cron": "*/5 * * * *", "env": { "SERVICE_URL": "https://example.com/api" } } ] }
Top-level must be a dict with a pollers key (not a bare array).
| Field | Required | Description |
|---|---|---|
name | yes | Unique identifier. Used in logs and event routing. |
command | yes | Shell command, relative to the skill directory. |
cron | yes | Cron expression (5-field, UTC). |
env | no | Additional environment variables for the script. |
3. Register the pollers
After creating or updating pollers.json, call the reload_pollers tool. This re-scans all skill directories and registers any new pollers with the scheduler.
reload_pollers() # → "Reloaded. 2 poller(s) registered: bluesky-mentions, github-issues"
Pollers are also loaded automatically at startup.
File Layout
skills/my-monitor/ ├── SKILL.md ├── pollers.json ← declares pollers ├── poller.py ← the script ├── cursor.json ← poller state (managed by script) └── events.jsonl ← optional local event log
Design Patterns
See design-patterns.md for detailed guidance on:
- State management — cursor pattern, timestamp vs URI cursors, external service state, recovery on first run
- Filtering — selecting actionable notification types, avoiding shared
is_readtraps - Prompt quality — including URIs/CIDs so the agent can act, not just observe
- Error handling — fail silently (exit non-zero), never emit on error
- Anti-patterns — common mistakes and how to avoid them
Security & Privacy
See security.md for guidance on:
- Trust tiers — the follow-gate pattern for sorting trusted vs unknown sources
- Operator in the loop — keeping the human informed without locking everything down
- Credential handling — env vars, per-agent accounts, what not to log
- Prompt injection — honest reporting with context, not sanitization
Key Constraints
- 60-second timeout. If a poller doesn't finish in 60s, it's killed and the cycle is skipped.
- Silence means nothing to report. Only output lines when there's something actionable.
- One JSON object per line. Each line must parse independently.
pollerandpromptare required fields. Lines missing either are dropped.- Pollers are dumb. No LLM calls. Check a service, output what changed, exit. Keep them fast and pure.
- State management is the poller's job. Use
STATE_DIRto store cursors, history, or any persistent state. The scheduler doesn't track state for you.
Debugging
If a poller isn't working:
- Check it was discovered:
reload_pollersreports the count and names - Run it manually:
cd skills/my-monitor && STATE_DIR=. POLLER_NAME=test python poller.py - Check stderr: Poller stderr is logged as
poller_stderrevents - Check exit code: Non-zero exits are logged as
poller_nonzero_exit - Check JSON format: Each stdout line must be valid JSON with
pollerandpromptkeys
Available Tool
| Tool | Description |
|---|---|
reload_pollers | Re-scan all skills/*/pollers.json and register pollers. Call after installing/updating skills. |


