> ## Documentation Index
> Fetch the complete documentation index at: https://app.keystroke.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced triggers

> Filter and transform payloads before they run.

Every trigger source ([schedule](/learn/triggers/schedules), [webhook](/learn/triggers/webhooks), or [poll](/learn/triggers/polling)) 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:

| Concern       | Question                         | Where it lives                                            |
| ------------- | -------------------------------- | --------------------------------------------------------- |
| **Filter**    | Should this run at all?          | The **source**: webhook `request`/`filter`, poll `filter` |
| **Transform** | What 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.

```ts src/triggers/incoming-message.ts theme={null}
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](/learn/agents/overview). An agent target takes a `prompt` instead of a `transform`; the payload becomes a prompt rather than a typed input.

```ts src/triggers/signup.ts theme={null}
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:

```ts theme={null}
.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:

```ts src/triggers/inbox-check.ts theme={null}
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:

```ts theme={null}
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](/learn/triggers/overview#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](/learn/triggers/polling#filtering) 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](/learn/triggers/webhooks#shared-endpoints).

## Inspect what ran

Every trigger-driven run is recorded under the [attachment id](/learn/triggers/overview#attachment-id). Audit them from the CLI:

```bash theme={null}
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](/cli#triggers) and [run history](/learn/logs/overview).

## Next steps

<CardGroup cols={2}>
  <Card title="App events" href="/learn/triggers/app-events">
    React to events from connected third-party apps.
  </Card>

  <Card title="Webhooks" href="/learn/triggers/webhooks">
    Endpoints, validation, filters, and shared routes.
  </Card>

  <Card title="Run agents" href="/learn/agents/run-agents">
    How agents run and how to inspect their sessions.
  </Card>

  <Card title="Run history" href="/learn/logs/overview">
    Review trigger-driven runs in the web app.
  </Card>
</CardGroup>
