> ## 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.

# App events

> Run workflows when connected apps emit events.

A common goal is "run this when something happens in another app," such as a new GitHub issue, a paid Stripe invoice, or a Linear status change. Keystroke doesn't have a separate app-event trigger type; instead you react to app events with the [trigger sources](/learn/triggers/overview) you already have, pointed at the app. This page covers the patterns.

<Note>
  There's no `defineAppEventSource`. The three sources ([webhook](/learn/triggers/webhooks), [schedule](/learn/triggers/schedules), and [poll](/learn/triggers/polling)) cover app events. Pick the one that matches how the app delivers events.
</Note>

## When the app sends webhooks

Most modern apps can push events to a URL. This is the best option when it's available: point the app's webhook at a [webhook trigger](/learn/triggers/webhooks).

1. Define a `defineWebhookSource` with a `request` schema for the app's payload.
2. Get the URL with `keystroke triggers url <endpoint>` and register it in the app's settings.
3. Use a `filter` to narrow to the event types you care about.

```ts src/triggers/stripe-invoice-paid.ts theme={null}
import { defineWebhookSource } from "@keystrokehq/keystroke/trigger";
import { z } from "zod";
import workflow from "../workflows/invoice-paid";

export default defineWebhookSource({
  slug: "stripe-invoice-paid",
  endpoint: "stripe",
  request: z.object({ type: z.string(), data: z.object({ id: z.string() }) }),
  filter: z.object({ type: z.literal("invoice.paid") }),
}).attach({
  workflow,
  transform: (payload) => ({ invoiceId: (payload as { data: { id: string } }).data.id }),
});
```

Providers that send every event type to one URL fit the [shared endpoint](/learn/triggers/webhooks#shared-endpoints) pattern: many triggers on a single `endpoint`, each filtering for its own event.

## When the app doesn't push events

If an app has no webhooks (or you'd rather pull), use a [poll trigger](/learn/triggers/polling). Its `run()` checks the app's API on a schedule and starts a run only when there's something new:

```ts src/triggers/new-issues.ts theme={null}
import { definePollSource } from "@keystrokehq/keystroke/trigger";
import workflow from "../workflows/handle-issue";

export default definePollSource({
  slug: "new-issues",
  schedule: "*/10 * * * *",
  run: async () => fetchOpenIssues(),
  filter: (result) => result.issues.length > 0,
}).attach({ workflow });
```

When the source you're polling needs credentials, resolve them through an [action](/learn/actions/overview) that you call from `run()`, or read them from your project's [credentials](/learn/credentials/overview).

## Chat apps: the Slack gateway

Conversations are different from events. To let people talk to an [agent](/learn/agents/overview) from Slack (mention it in a channel or DM it), use the Slack **gateway** rather than a trigger. The gateway delivers inbound messages to an agent and posts replies back. See [external channels](/learn/agents/external-channels).

## Finding integrations

Keystroke ships 1,000+ [integrations](/integrations). Each one lists the actions it provides (and whether it exposes triggers), which you can use inside a poll's `run()` or as steps and tools once connected. Browse the catalog to see what's available for the app you want to react to, then wire it up with a webhook or poll as above.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" href="/learn/triggers/webhooks">
    Receive events the app pushes to a URL.
  </Card>

  <Card title="Polling" href="/learn/triggers/polling">
    Pull events from an app on a schedule.
  </Card>

  <Card title="External channels" href="/learn/agents/external-channels">
    Connect agents to Slack and other chat surfaces.
  </Card>

  <Card title="Integrations" href="/integrations">
    Browse 1,000+ built-in integrations.
  </Card>
</CardGroup>
