Trigger: polling¶
The fallback when webhooks aren't an option. Agent Smith asks the tracker every N seconds what's new.
Use polling when:
- Your tracker is on a private network the orchestrator can't be reached from.
- You don't want to manage a public ingress for the orchestrator.
- You're iterating on the config and want a tight feedback loop without re-pointing webhooks.
Don't use polling when webhooks work — they're faster, cheaper on tracker rate limits, and don't have a per-poll delay.
Config¶
Polling is a property of the tracker, so on a server you set it in Configuration → Trackers, in the tracker's polling section: enable it, set the interval, set the jitter. The running server picks the change up on its next cycle, with no restart. The CLI form, and what agent-smith config import takes, is the same thing as YAML:
trackers:
acme-platform:
type: azure_devops
# ... organization, project, auth ...
polling:
enabled: true
interval_seconds: 60 # how often to ask
jitter_percent: 10 # randomize ±10% to avoid thundering herds
Same shape for every tracker family (Azure DevOps, Jira, GitHub Issues, GitLab Issues). The polling block lives on the tracker, not on the project — every project sharing this tracker shares the poll.
What gets polled¶
Per poll, the framework asks the tracker for tickets in open_states that have been updated since the last poll. When the tracker has trigger labels configured (pipeline_from_label), discovery narrows the server-side query to tickets carrying one of those concrete labels — on a shared tracker (the realistic case: your whole team's board) untagged tickets never even reach the matching step. A tracker triggered by status/area-path alone receives everything in its trigger statuses. Either way, a ticket claimed by one run is never picked up for a second. The poller reacts to:
- New tickets that match the project resolution rules and have a
pipeline_from_label. - Status transitions that put a ticket into a
trigger_statusesvalue. A finished ticket a person moved back there is reworked (how). - Label adds (a
pipeline_from_labellabel appears).
A ticket that waits for another ticket it requires is not started, whatever it carries; see Labels.
A polled ticket carries its labels and nothing else (no area path, no source repo), so a project is matched by its tag rule on this path; see Project resolution.
On Jira and Azure DevOps the query also asks, by id, for tickets whose specification was approved and still expects work, even when they carry no trigger label, so an approved ticket isn't lost because nobody labelled it. That list is capped at fifty ids per query, oldest approval first, and the poller reports what didn't fit. GitHub and GitLab discovery is unchanged by this; their query doesn't carry the label guard the id list works around.
A tracker entry runs in one mode. When its polling is enabled, webhooks start nothing for the projects on it: a delivery for such a project is answered as not handled, with the reason project <name> polls tracker <entry>; webhooks start nothing for it, and no comment is written to the ticket. Webhooks and polling never race on one ticket.
Polling vs webhooks¶
Both modes start the same work; webhooks only start it sooner.
| Webhooks | Polling | |
|---|---|---|
| Latency | Sub-second | Up to interval_seconds |
| Tracker network reachability | Tracker must reach orchestrator | Orchestrator must reach tracker |
| Tracker API calls | One request per ticket event | Per interval: the poller's two ticket lists, the change sweep's changed-ticket read (one page usually) and one read of open pull requests per repository |
| Rework acts (keyword comments, Request changes, Wait for author) | Delivered as they happen | Found by the change sweep from a persisted cursor; served by the same rework worker |
Pull-request events (the review label, /agent-smith comments) |
Delivered as they happen | The PR sweep lists each repository's open pull requests every interval and starts each once per label added or comment; opening or pushing starts nothing; a repository is recorded first, so switching to polling replays nothing. Azure DevOps has no label scan in either mode |
| The pull request's record (reviews, votes, comments, merges, closes, reopens) | Delivered as they happen (which deliveries) | See below |
| Secret to verify | Yes (HMAC or basic auth) | Auth token only |
| Survives orchestrator restarts | Tracker retries until it gets 200 | Yes — the sweep's cursors are rows in the database and only move forward |
In polling mode the record of a ticket's pull request is kept current by the same two sweeps, without further requests. One sweep cycle runs the change sweep under a deadline of half the interval, then the PR sweep, then waits a full interval, so a cycle takes the interval plus the work.
- Reviews and votes within a sweep cycle — about one interval when every source fits its deadline; a list cut by its deadline resumes in the next cycle. GitHub lists a pull request whose latest review by someone is newer than the cursor; Azure DevOps lists every pull request with a reviewer, named by every vote, so a changed or reset vote is listed again (after a restart each is read once more). A GitHub dismissal keeps its review's time, so polling sees it only at the next read from another trigger.
- Comments within one cycle on GitHub and GitLab; on Azure DevOps, which reads comments per pull request, within the rotation of ten pull requests a cycle.
- Merges, closes and reopens when a complete open list is read — one not cut by the budget and not resumed from an earlier page. A pull request merged before any sweep listed it open is found too. A record the host can no longer read is asked for again each complete pass.
- A sweep outage longer than two intervals re-records the repository: comments written in the gap are not replayed.
GitHub allows 5,000 REST requests an hour per token. At a 60-second interval with ten GitHub repositories, polling spends about 2,600 of them (the change sweep, the worker's follow-up reads and the pull-request events — three calls a repository a cycle); raise the interval past ten repositories.
For most setups, webhooks for the public trackers (GitHub Cloud, GitLab Cloud, Jira Cloud, Azure DevOps Cloud), polling for self-hosted ones behind a firewall.
Tuning the interval¶
60 seconds is a good default. Faster (10–30 seconds) for development. Slower (300+ seconds) for trackers with aggressive rate limits or when you're paying per API call.
The jitter_percent field randomizes the actual interval by ±N% so multiple orchestrator replicas don't all hit the tracker at the same wall-clock moment. Default 10%. Don't set it to 0 unless you have exactly one replica.
Leader lease¶
If you run more than one orchestrator replica, only one polls. The polling lease is a Redis key agentsmith:leader:poller with a TTL; one replica wins, the others wait. If the leader dies, the lease expires and another replica takes over within 30 seconds.
You don't need to configure this — it's automatic. Same model is used for the housekeeping coordinator (stale-job detection, enqueue reconciliation) and for the change sweep (agentsmith:leader:change-sweep, which finds rework acts on tickets and pull requests changed since its cursor), each on a separate lease. Both the poller and the sweep rebuild in place when the configuration changes.
Mixed mode¶
The mode belongs to the tracker entry, so you can have webhooks on one entry and polling on another in the same config. A project's own polling block does not change it; the dashboard's project panel shows the mode the entry gives it (polling every 60s or webhooks).
Each poller serves only the projects on its own entry. A project on a webhook entry is no longer picked up by a polling entry of the same tracker type: if it relied on that, move it to the polling entry or enable polling on its own. When two entries of different modes own projects on one repository, the configuration probe reports an advisory, and pull-request deliveries for that repository start nothing; the PR sweep serves it instead.
trackers:
acme-github: # webhooks (secret lives in GITHUB_WEBHOOK_SECRET on the server)
type: github
url: https://github.com/acme-org/todolist
auth: github_token
acme-jira-onprem: # polling
type: jira
url: https://jira.acme-internal.example
auth: jira_token
polling: { enabled: true, interval_seconds: 90 }
Next¶
- Webhooks — if you can make webhooks work, do.
- Labels — which labels trigger which pipeline.
- Host it: kubernetes — multi-replica setup where the leader lease matters.