pollhook
Poll REST APIs, deliver webhooks. Bridge any API into Continue's event system.
An autonomous codebase built by the Continue Software Factory
Why?
Many services (Sentry, PagerDuty, Snyk, etc.) have REST APIs but no outgoing webhooks. Setting up custom integrations for each one means writing bespoke glue code, managing state, and handling retries -- over and over.
pollhook does one thing: run a command on an interval, detect new items via ID-based dedup, and POST them to a webhook endpoint. One YAML file replaces all that glue.
Table of Contents
- Quick Start
- Config Format
- Commands
- How It Works
- Design Decisions
- Contributing
- License
Quick Start
git clone https://github.com/continuedev/pollhook.git
cd pollhook
make build
# Or install directly
go install github.com/continuedev/pollhook@latest
Create a pollhook.yaml:
- name: sentry-issues
command: |
curl -s -H "Authorization: Bearer $SENTRY_TOKEN" \
https://sentry.io/api/0/projects/my-org/my-project/issues/?query=is:unresolved
interval: 5m
items: "."
id: "id"
webhook:
url: https://hub.continue.dev/api/webhooks/ingest/your-workflow-id
secret: your-webhook-secret
pollhook test --config pollhook.yaml
# Run for real
pollhook serve --config pollhook.yaml
Config Format
- name: sentry-issues # Unique name for this source
command: | # Shell command (run via sh -c) that outputs JSON
curl -s -H "Authorization: Bearer $SENTRY_TOKEN" \
https://sentry.io/api/0/projects/my-org/my-project/issues/
interval: 5m # Poll interval (Go duration: 30s, 2m, 1h)
items: "." # Dot path to the JSON array ("." = root)
id: "id" # Dot path to unique ID on each item
webhook:
url: https://... # Webhook endpoint URL
secret: optional # Sent as X-Webhook-Secret header
Dot paths: "." means the root is the array. "data.incidents" navigates $.data.incidents. Environment variables in the config are expanded via $VAR or ${VAR}.
Commands
pollhook serve
Run pollers and deliver webhooks.
- Starts a goroutine per source with
time.Ticker - Polls immediately on startup, then on interval
- State persisted to
~/.pollhook/state.jsonevery 30s and on shutdown - Graceful shutdown on SIGINT/SIGTERM
pollhook test
Validate config, run each command once, show extracted items. No webhooks sent, no state touched.
pollhook version
Print the version.
How It Works
For each source, a goroutine runs on a timer:
- Execute command --
sh -cwith 60s timeout, capture stdout - Extract items -- parse JSON, navigate dot path to array
- Extract ID -- get unique ID from each item via dot path
- Dedup -- skip if ID already seen (checked against persisted state)
- Deliver webhook -- POST
{"source": "name", "item": {...}, "polled_at": "..."}to the webhook URL - Mark seen -- only after successful delivery (at-least-once guarantee)
The webhook payload matches what Continue's /api/webhooks/ingest/:workflowId endpoint expects -- any JSON body that gets stringified into the workflow prompt.
Design Decisions
| Decision | Choice | Why |
|---|---|---|
| Dependencies | Only gopkg.in/yaml.v3 |
Everything else is stdlib |
| CLI framework | flag.FlagSet |
2 commands don't justify cobra |
| Change detection | ID-based dedup | Deterministic, order-independent, per-item |
| State cap | 10,000 IDs per source | Sliding window prevents unbounded growth |
| Delivery guarantee | At-least-once | Failed delivery = ID stays unseen = retried next tick |
| Retry | 1 retry on 5xx (2s delay) | 4xx = config error, 5xx = transient |
| Command execution | sh -c |
Lets users write piped multi-line commands |
| Webhook secret | X-Webhook-Secret header |
Matches ingest endpoint's timing-safe comparison |
Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
Apache-2.0 -- see LICENSE for details. Copyright (c) 2025 Continue Dev, Inc.