Skip to main content
Every trigger source (schedule, webhook, or poll) attaches to a target the same way. This page covers what happens between the source firing and the run starting: filtering, transforming, attaching to agents, and skipping.

Filter on the source, transform on the attachment

Two separate concerns, in two separate places:
ConcernQuestionWhere it lives
FilterShould this run at all?The source: webhook request/filter, poll filter
TransformWhat input does the run receive?The attachment: transform on a workflow target
Keeping them separate means “should this fire?” sits next to the source definition, and “what does the run get?” sits next to the binding.

Transform a workflow input

A workflow target accepts an optional transform that maps the (already validated and filtered) source payload into the workflow’s input. Use it to reshape a third-party payload into the clean shape your workflow expects.
src/triggers/incoming-message.ts
import { defineWebhookSource } from "@keystrokehq/keystroke/trigger";
import { z } from "zod";
import workflow from "../workflows/incoming-message";

const IncomingMessageSchema = z.object({
  text: z.string().trim().min(1),
  from: z.string().optional(),
});

export default defineWebhookSource({
  slug: "incoming-message",
  endpoint: "incoming-message",
  request: IncomingMessageSchema,
}).attach({
  workflow,
  transform: (payload) => {
    const parsed = payload as z.infer<typeof IncomingMessageSchema>;
    return { message: parsed.text, sender: parsed.from ?? "anonymous" };
  },
});
The transform’s return value is parsed against the workflow’s input schema, so the workflow still gets a validated payload. If you omit transform, the matched payload passes straight through to input (extra fields are ignored).

Attach to an agent

Instead of a workflow, a source can attach to an agent. An agent target takes a prompt instead of a transform; the payload becomes a prompt rather than a typed input.
src/triggers/signup.ts
import { defineWebhookSource } from "@keystrokehq/keystroke/trigger";
import { z } from "zod";
import signupResearcher from "../agents/signup-researcher";

export default defineWebhookSource({
  slug: "signup",
  endpoint: "signup",
  request: z.object({ name: z.string(), email: z.string().email() }),
}).attach({
  agent: signupResearcher,
  prompt: (payload) => `Research the new signup ${payload.name} <${payload.email}>.`,
});
The prompt can be a static string or a function of the payload. A transform is not used for agent targets; the agent receives the payload directly through its prompt.

Prompt interpolation

For a static prompt string, you can interpolate payload fields with {{payload.path}} placeholders instead of writing a function:
.attach({
  agent: support,
  prompt: "You have {{payload.count}} unread emails. Triage them.",
})

Fan out to multiple targets

One source can drive several workflows and/or agents. Chain .attach() once per target — each call binds the same source to another workflow or agent:
src/triggers/inbox-check.ts
import { defineCronSource } from "@keystrokehq/keystroke/trigger";
import assistant from "../agents/assistant";
import assistantVm from "../agents/assistant-vm";

export default defineCronSource({
  slug: "inbox-check",
  schedule: "*/5 * * * *",
})
  .attach({ agent: assistant, prompt: "Check my email and list new emails." })
  .attach({ agent: assistantVm, prompt: "Check my email and list new emails." });
You can mix workflow and agent targets, and you can default-export an array instead of chaining:
const inbox = defineCronSource({ slug: "inbox-check", schedule: "*/5 * * * *" });

export default [
  inbox.attach({ workflow: triage }),
  inbox.attach({ agent: assistant, prompt: "Summarize new mail." }),
];
This is still one trigger. Each fire — a cron tick, a poll cycle, or a matching webhook request — evaluates the source once (a poll’s run() and filters execute a single time) and then fans out to every attached target. The fire is recorded once and links to all the runs and sessions it started. Every target keeps its own attachment id ({sourceSlug}:{targetSlug}), so the two agents above are inbox-check:assistant and inbox-check:assistant-vm. This works for all three source kinds (schedule, webhook, and poll). Each {source}:{target} id must be unique, so a given workflow or agent can only attach to a source once.

Skipping a run

A source can decide a particular event isn’t worth a run:
  • Poll: if a filter returns false, the tick is skipped and no run is created. Skipped poll ticks don’t count toward execution limits.
  • Webhook on a shared endpoint: a payload that matches no trigger on the endpoint returns { ok: true, skipped: true }. See shared endpoints.

Inspect what ran

Every trigger-driven run is recorded under the attachment id. Audit them from the CLI:
keystroke triggers runs list signup --workflow signup-researcher
keystroke triggers runs get signup <run-id> --workflow signup-researcher --include workflows,trace
See the CLI reference and run history.

Next steps

App events

React to events from connected third-party apps.

Webhooks

Endpoints, validation, filters, and shared routes.

Run agents

How agents run and how to inspect their sessions.

Run history

Review trigger-driven runs in the web app.