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

# Triggers

> The four ways a playbook starts

## The closed vocabulary

Every [playbook](/autopilot/playbooks) starts from one of exactly four trigger types. Nothing else. The closed vocabulary is deliberate. It keeps agents auditable, predictable, and easy to reason about.

| Trigger            | Started by                           | Use for                                                |
| ------------------ | ------------------------------------ | ------------------------------------------------------ |
| **Schedule**       | Cron expression                      | Daily briefings, weekly digests, time-based SLA checks |
| **Data change**    | Change-stream events on a collection | Reactive work tied to record state                     |
| **Webhook**        | Inbound HTTP from an external system | Stripe events, SendGrid bounces, GitHub issues         |
| **User-triggered** | Explicit button click in the app     | "Draft me a recap," "Run this playbook now"            |

## Schedule

A cron-style schedule. The runtime fires the playbook at the specified time.

```yaml theme={null}
trigger:
  type: schedule
  cron: "0 9 * * 1-5"   # 9 AM, Monday to Friday
  timezone: "America/New_York"
```

### Common cron patterns

| Pattern        | Meaning                          |
| -------------- | -------------------------------- |
| `0 8 * * *`    | Every day at 8 AM                |
| `0 9 * * 1-5`  | Weekdays at 9 AM                 |
| `0 */4 * * *`  | Every 4 hours                    |
| `0 0 1 * *`    | First of every month at midnight |
| `*/15 * * * *` | Every 15 minutes                 |

### Timezone handling

For [personal-scope](/autopilot/scopes) playbooks, the timezone resolves to the user's timezone. For app-wide playbooks, the timezone is set on the trigger.

```yaml theme={null}
trigger:
  type: schedule
  cron: "0 8 * * 1-5"
  timezone: "{{ user.timezone }}"   # personal scope
```

### When to use schedule

* Recurring digests, briefings, and reports
* Time-based SLA enforcement ("any open ticket older than 24 hours")
* Periodic refreshes ("recompute pipeline forecast every Monday")

## Data change

A change-stream event on a collection. The runtime observes inserts, updates, and deletes, and fires the playbook when an event matches the filter.

```yaml theme={null}
trigger:
  type: data_change
  collection: deals
  on: [update]
  where: "stage = 'lost' AND amount > 10000"
```

### Operations

| Operation | Fires when                         |
| --------- | ---------------------------------- |
| `create`  | A new record is inserted           |
| `update`  | An existing record's fields change |
| `delete`  | A record is removed                |

You can listen to multiple operations in one trigger:

```yaml theme={null}
trigger:
  type: data_change
  collection: tickets
  on: [create, update]
  where: "priority = 'p0'"
```

### Field-level filters

The `where` clause can reference the new state of the record. For updates, it can also reference what changed:

```yaml theme={null}
trigger:
  type: data_change
  collection: deals
  on: [update]
  where: "changed('stage') AND new.stage = 'lost'"
```

### When to use data change

* Reactive work tied to a state change ("when a deal moves to lost…")
* Onboarding flows ("when a new contact is created…")
* Anomaly detection ("when an order's total exceeds \$50,000…")

<Note>
  The change-stream pattern is the same one Gainable uses internally for the email daemon and other observation services. The runtime does the heavy lifting; the playbook just describes the filter.
</Note>

## Webhook

An inbound HTTP request from an external system. The runtime exposes a unique URL per webhook and fires the playbook when a request arrives.

```yaml theme={null}
trigger:
  type: webhook
  source: stripe
  event: invoice.payment_failed
  secret: "{{ secrets.stripe_webhook }}"
```

### How webhook URLs work

When you configure a webhook trigger, Gainable provisions a URL like:

```
https://api.gainable.dev/webhooks/{app_id}/{trigger_id}
```

You configure the source system to POST to that URL. The runtime verifies the signature (when applicable), parses the body, and exposes it as `trigger.body` to the playbook.

### Common sources

| Source   | Example events                                            |
| -------- | --------------------------------------------------------- |
| Stripe   | `invoice.payment_failed`, `customer.subscription.deleted` |
| SendGrid | `bounce`, `spamreport`                                    |
| GitHub   | `pull_request.opened`, `issue_comment.created`            |
| Calendar | `event.created`, `event.updated`                          |
| Generic  | Any system that can POST JSON                             |

### When to use webhook

* Reacting to events in external systems you don't own
* Bridging Gainable apps to third-party tools
* Receiving callbacks from long-running external jobs

See [Connect outbound](/autopilot/connect-outbound) for the full inbound and outbound integration story.

## User-triggered

An explicit button click in the app. A user opens a record (or a page) and clicks a button labeled "Run this playbook now."

```yaml theme={null}
trigger:
  type: user_triggered
  button_label: "Draft recap"
  surfaces: [deal_detail_page, dashboard]
```

### How it appears

Gainable renders user-triggered playbooks as buttons in the [Autopilot](/autopilot/inbox) widget on the page you specified. When the user clicks:

1. The runtime fires the playbook with `trigger.user_id` set to the clicker
2. The playbook runs with that user's permissions
3. The resulting draft (if any) lands in their personal Autopilot inbox

### When to use user-triggered

* On-demand drafts ("write me a follow-up for this deal right now")
* Bulk operations the user wants to start manually
* Anything the user wants control over the timing of, even if the rest of the playbook is automatic

## Choosing the right trigger

| Question                                            | Answer             |
| --------------------------------------------------- | ------------------ |
| Does this need to happen at a specific time of day? | **Schedule**       |
| Does this react to a record changing?               | **Data change**    |
| Does this come from an external system?             | **Webhook**        |
| Should the user choose when this runs?              | **User-triggered** |

If two answers are yes, you probably have two playbooks pretending to be one. Split them.

## Best practices

<AccordionGroup>
  <Accordion title="Prefer data change over polling on a schedule">
    A `schedule` trigger that scans a collection every 5 minutes is almost always worse than a `data_change` trigger that fires only on the relevant transition. Faster, cheaper, and more accurate.
  </Accordion>

  <Accordion title="Filter at the trigger, not in the playbook">
    The more selective the trigger's `where` clause, the less work the runtime does and the cleaner your action log gets.
  </Accordion>

  <Accordion title="Use timezones for personal schedules">
    A daily briefing fired at 8 AM UTC is 3 AM in California. Bind the timezone to the user.
  </Accordion>

  <Accordion title="Test webhooks in simulation">
    Webhook triggers can be tested by replaying captured payloads. Always do this before going live.
  </Accordion>
</AccordionGroup>

## Learn more

<CardGroup cols={2}>
  <Card title="Playbooks" icon="book-open" href="/autopilot/playbooks">
    Trigger, steps, and guardrails together
  </Card>

  <Card title="Tools" icon="screwdriver-wrench" href="/autopilot/tools">
    What playbooks call after the trigger fires
  </Card>

  <Card title="Connect outbound" icon="plug" href="/autopilot/connect-outbound">
    Webhooks and external systems
  </Card>

  <Card title="Inbox" icon="inbox" href="/autopilot/inbox">
    Where user-triggered buttons render
  </Card>
</CardGroup>
