Skip to main content
A poll trigger runs on a schedule, calls a run() function you provide, and starts a workflow or agent only when that function returns something worth acting on. Use it for sources that don’t push webhooks, such as an inbox, an API you check periodically, or a queue.

Example requests

Ask your coding agent what to watch and when it is worth acting on. It can write the check and the filter.
“Check our vendor API every hour and run the workflow only when a new invoice is ready to process.”
“Check the support inbox every few minutes and start a triage run when an unread ticket arrives.”
“Check this public dataset daily and kick off a run only when the numbers change.”

Define a poll

Use definePollSource with a slug, name, description, a schedule, and a run function, then attach a target. The run function produces the payload; an optional filter decides whether that payload is worth a run.
src/triggers/new-inbox.ts

Use actions inside run()

Poll run() executes inside a workflow-style action runner (no durable replay), so calling await myAction.run(input) auto-resolves the action’s credentials the same way a workflow step would:
Credentials follow the normal resolution order. To pin a specific instance for a poll, bind an assignment with --poll <slug> — poll consumers are the slugs of the actions called inside run().

Cursor state

Polls that need to remember what they already processed — a last-checked timestamp, seen message ids, a pagination token — can declare a state schema. Keystroke persists that blob on the trigger row and passes it into run() each tick:
When a state schema is declared, run always receives the { state, setState } context; without one, run() takes no arguments. state itself is still undefined on the very first tick. Delivery is at-least-once: a failed dispatch retries on the next tick with the same cursor, so the same items may appear again. Workflows that cannot tolerate re-processing should use a durable external dedup log (e.g. upsert a sheet row by thread id) in addition to or instead of platform cursor state.

When to use which strategy

How a poll tick works

On each scheduled tick, Keystroke calls run(), applies the filters, and, if they pass, starts the target with the payload. If a filter returns false, the tick is skipped and no run is created — skipped ticks don’t count toward execution limits. The schedule uses the same cron format as schedule triggers, except polls only accept 5-field expressions (no seconds field) — a poll fires at most once per minute. For “run only if X”, use a poll; a cron runs the target every tick with no filter. One tick produces one payload and, when filters pass, one run per attached target. Returning an array from run() does not fan out into one run per item — the whole return value is the payload. Prefer wrapping a batch in an object (for example { prs: [...] }), filter on emptiness (payload.prs.length > 0), and use transform on the attachment if the workflow expects a different shape. To process items one at a time, loop inside the workflow. On deploy, a poll fires immediately; disable the attachment until the workflow is verified (the disabled state survives redeploys when the attachment target is unchanged — repointing to a new workflow creates a new enabled attachment).

Filtering

The most common pattern is “only run when there’s new work.” Filter the payload before it starts a run:
You can chain additional .filter() predicates; all of them must pass for the tick to fire:
Filters are part of the source; they decide whether to run. To shape what the run receives, use a transform on the attachment (see advanced triggers).

Invoke a poll on demand

You don’t have to wait for the schedule while developing. Invoke a poll tick immediately against the deployed project:
This enqueues the same poll job the schedule uses: run() executes, and if the filters pass, the target starts. Inspect results with keystroke triggers runs list new-inbox --workflow <target-slug> (or --agent <target-slug>). You can also use Invoke on the trigger detail page in the web app.

Next steps

Advanced triggers

Transforms, agent prompts, and filtering in depth.

Schedules

Run on a cron schedule (optional static payload).

App events

Poll a connected app’s API for new events.

Triggers overview

Sources, attach, and attachment ids.