$npx -y skills add tkellogg/open-strix --skill pollersCreate and manage pollers — lightweight monitoring scripts that check external services on a schedule. Use when the user wants to monitor something (Bluesky, GitHub, RSS, APIs), create a new poller, debug why a poller isn't firing, or manage pollers.json files in skills.
| 1 | # Pollers — Event-Driven Monitoring |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## How It Works |
| 6 | |
| 7 | 1. A skill includes a `pollers.json` file alongside its `SKILL.md` |
| 8 | 2. The scheduler discovers all `pollers.json` files at startup and when `reload_pollers` is called |
| 9 | 3. On each cron tick, the scheduler runs the poller command as a subprocess |
| 10 | 4. Each line of stdout is parsed as JSON and delivered to the agent as an event |
| 11 | 5. If there's nothing to report, the poller outputs nothing — silence is the filter |
| 12 | |
| 13 | ## Creating a Poller |
| 14 | |
| 15 | ### 1. Write the poller script |
| 16 | |
| 17 | The script runs in the skill directory. It receives these environment variables automatically: |
| 18 | |
| 19 | | Variable | Description | |
| 20 | |----------|-------------| |
| 21 | | `STATE_DIR` | The skill directory (writable, for cursors/state) | |
| 22 | | `POLLER_NAME` | The poller's name from pollers.json | |
| 23 | |
| 24 | Plus any custom env vars from the `env` field, plus the agent's existing environment. |
| 25 | |
| 26 | **Output contract:** |
| 27 | - **stdout:** JSONL (one JSON object per line). Each line must have `poller` (string) and `prompt` (string) fields. |
| 28 | - **stderr:** Free-form logging. Not forwarded to the agent. |
| 29 | - **Exit 0:** Success. **Non-zero:** Error, this cycle is skipped. |
| 30 | |
| 31 | Example poller script: |
| 32 | |
| 33 | ```python |
| 34 | #!/usr/bin/env python3 |
| 35 | """Check for new items since last poll.""" |
| 36 | import json, os, sys |
| 37 | from pathlib import Path |
| 38 | |
| 39 | STATE_DIR = Path(os.environ.get("STATE_DIR", ".")) |
| 40 | CURSOR_FILE = STATE_DIR / "cursor.json" |
| 41 | |
| 42 | def load_cursor(): |
| 43 | if CURSOR_FILE.exists(): |
| 44 | return json.loads(CURSOR_FILE.read_text()) |
| 45 | return {} |
| 46 | |
| 47 | def save_cursor(cursor): |
| 48 | CURSOR_FILE.write_text(json.dumps(cursor, indent=2)) |
| 49 | |
| 50 | def main(): |
| 51 | cursor = load_cursor() |
| 52 | # ... check your service, compare against cursor ... |
| 53 | |
| 54 | new_items = [] # your logic here |
| 55 | |
| 56 | for item in new_items: |
| 57 | event = { |
| 58 | "poller": os.environ.get("POLLER_NAME", "my-poller"), |
| 59 | "prompt": f"New item: {item['title']}" |
| 60 | } |
| 61 | print(json.dumps(event)) |
| 62 | |
| 63 | # Update cursor so next run skips these items |
| 64 | save_cursor(cursor) |
| 65 | |
| 66 | if __name__ == "__main__": |
| 67 | main() |
| 68 | ``` |
| 69 | |
| 70 | ### 2. Create pollers.json in the skill directory |
| 71 | |
| 72 | ```json |
| 73 | { |
| 74 | "pollers": [ |
| 75 | { |
| 76 | "name": "my-service-check", |
| 77 | "command": "python poller.py", |
| 78 | "cron": "*/5 * * * *", |
| 79 | "env": { |
| 80 | "SERVICE_URL": "https://example.com/api" |
| 81 | } |
| 82 | } |
| 83 | ] |
| 84 | } |
| 85 | ``` |
| 86 | |
| 87 | **Top-level must be a dict** with a `pollers` key (not a bare array). |
| 88 | |
| 89 | | Field | Required | Description | |
| 90 | |-------|----------|-------------| |
| 91 | | `name` | yes | Unique identifier. Used in logs and event routing. | |
| 92 | | `command` | yes | Shell command, relative to the skill directory. | |
| 93 | | `cron` | yes | Cron expression (5-field, UTC). | |
| 94 | | `env` | no | Additional environment variables for the script. | |
| 95 | |
| 96 | ### 3. Register the pollers |
| 97 | |
| 98 | 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. |
| 99 | |
| 100 | ``` |
| 101 | reload_pollers() |
| 102 | # → "Reloaded. 2 poller(s) registered: bluesky-mentions, github-issues" |
| 103 | ``` |
| 104 | |
| 105 | Pollers are also loaded automatically at startup. |
| 106 | |
| 107 | ## File Layout |
| 108 | |
| 109 | ``` |
| 110 | skills/my-monitor/ |
| 111 | ├── SKILL.md |
| 112 | ├── pollers.json ← declares pollers |
| 113 | ├── poller.py ← the script |
| 114 | ├── cursor.json ← poller state (managed by script) |
| 115 | └── events.jsonl ← optional local event log |
| 116 | ``` |
| 117 | |
| 118 | ## Design Patterns |
| 119 | |
| 120 | See [design-patterns.md](design-patterns.md) for detailed guidance on: |
| 121 | - **State management** — cursor pattern, timestamp vs URI cursors, external service state, recovery on first run |
| 122 | - **Filtering** — selecting actionable notification types, avoiding shared `is_read` traps |
| 123 | - **Prompt quality** — including URIs/CIDs so the agent can act, not just observe |
| 124 | - **Error handling** — fail silently (exit non-zero), never emit on error |
| 125 | - **Anti-patterns** — common mistakes and how to avoid them |
| 126 | |
| 127 | ## Security & Privacy |
| 128 | |
| 129 | See [security.md](security.md) for guidance on: |
| 130 | - **Trust tiers** — the follow-gate pattern for sorting trusted vs unknown sources |
| 131 | - **Operator in the loop** — keeping the human informed without locking everything down |
| 132 | - **Credential handling** — env vars, per-agent accounts, what not to log |
| 133 | - **Prompt injection** — honest reporting with context, not sanitization |
| 134 | |
| 135 | ## Key Constraints |
| 136 | |
| 137 | - **60-second timeout.** If a poller doesn't finish in 60s, it's killed and the cycle is skipped. |
| 138 | - **Silence means nothing to report.** Only output lines when there's something actionable. |
| 139 | - **One JSON object per line.** Each line must parse independently. |
| 140 | - **`poller` and |