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
UsedefinePollSource 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:
--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 astate schema. Keystroke persists that blob on the trigger row and passes it into run() each tick:
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 callsrun(), 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:.filter() predicates; all of them must pass for the tick to fire:
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: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.