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, 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
| Option | Required | What it does |
|---|---|---|
slug | Yes | Stable trigger slug (used in the attachment id and run history) |
schedule | Yes | A cron expression for how often to poll |
run | Yes | () => payload, produces the value checked by filters and passed to the target |
filter | No | (payload) => boolean, return false to skip this tick |
state | No | Zod schema for cursor state persisted between poll ticks |
id | No | Poll group id; sources sharing an id poll together (one run() per tick). Cannot be combined with state. |
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:
| API | What it does |
|---|---|
state | Zod schema describing the persisted cursor shape |
run({ state, setState }) | state is the last committed value (or undefined on first run); call setState(next) to update it |
| Commit timing | State is saved after a successful run() — including when the filter rejects (nothing new to process). If run() throws or dispatch fails, the cursor does not advance. |
When to use which strategy
| Strategy | Good for | Tradeoff |
|---|---|---|
Platform cursor (state + setState) | “Process items since last check” — timestamps, pagination tokens | At-least-once; re-delivery possible if dispatch fails mid-tick |
| Durable external log | Workflows that must never re-process the same item (upsert by id in a sheet/DB) | You manage storage; strongest dedup guarantee |
| Source-side consumption | APIs that track read state themselves (is:unread, mark-as-read) | Ephemeral — a still-unread item reappears on the next tick |
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. For “run only if X”, use a poll; a cron runs the target every tick with no filter.
On deploy, a poll fires immediately; disable the attachment until the workflow is verified (the disabled state survives redeploys).
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).
Run a poll on demand
You don’t have to wait for the schedule while developing. Trigger a poll tick immediately:run() and, if the filters pass, starts the target on the same path the schedule takes. Inspect results with keystroke triggers runs list new-inbox --workflow <target-slug> (or --agent <target-slug>).
Next steps
Advanced triggers
Transforms, agent prompts, and filtering in depth.
Schedules
Run on a cron schedule with no payload.
App events
Poll a connected app’s API for new events.
Triggers overview
Sources, attach, and attachment ids.