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

# Triggers

> All available triggers that can start a Terse workflow.

Triggers define the event that starts your workflow. Each trigger is tied to an integration and fires when a specific event occurs. Your `onTrigger` handler receives a typed event object with the payload.

In TypeScript, `terse generate` writes a single `Triggers` object in `src/terse.generated.ts`. Import it and call the nested helper for your event (for example `Triggers.github.onPROpened(...)`). Each integration and system trigger has its own section below.

## Integration triggers

### GitHub

| Trigger                              | Event type                         | Description                                      |
| ------------------------------------ | ---------------------------------- | ------------------------------------------------ |
| `Triggers.github.onPush()`           | `GithubPushTrigger`                | Code is pushed to a branch                       |
| `Triggers.github.onPROpened()`       | `GithubPROpenedTrigger`            | A pull request is opened                         |
| `Triggers.github.onPRMerged()`       | `GithubPRMergedTrigger`            | A pull request is merged                         |
| `Triggers.github.onPRClosed()`       | `GithubPRClosedTrigger`            | A pull request is closed                         |
| `Triggers.github.onPRSynchronized()` | `GithubPRSynchronizedTrigger`      | New commits are pushed to a PR                   |
| `Triggers.github.onPR()`             | `GithubPRTrigger`                  | Any pull request event                           |
| `Triggers.github.onIssueComment()`   | `GithubIssueCommentCreatedTrigger` | A comment is created on an issue or pull request |
| `Triggers.github.trigger()`          | `GithubTrigger`                    | Any GitHub event you select with `eventTypes`    |

Config: Scoped to specific repositories via `repo` parameter.

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.github.onPROpened({ repo: Repos.MyOrg.MyRepo })]
  ```
</CodeGroup>

<Expandable title="Event payload">
  All GitHub events include:

  * `repository` - `{ id, name, owner, defaultBranch }`
  * `repositoryName`, `username`, `installationId`
  * `sender` - `{ login, email }`
  * `commits` - array of `{ sha, message, name, fileDiffs: [{ filename, diff }] }`

  PR events add:

  * `pullRequest` - `{ id, number, title, body, state, merged, head, base, user, author?, url? }`
  * `branch` (optional)

  Push events add:

  * `branch` - the branch that was pushed to

  Issue comment events add:

  * `issue` - `{ id, number, title, body?, state, url, author, isPullRequest }` (the issue or PR the comment is on)
  * `comment` - `{ id, body, author, url, createdAt, updatedAt }`
</Expandable>

### Slack

| Trigger                            | Event type                  | Description                                  |
| ---------------------------------- | --------------------------- | -------------------------------------------- |
| `Triggers.slack.onMessage()`       | `SlackMessageTrigger`       | A message is posted in a channel             |
| `Triggers.slack.onDm()`            | `SlackMessageTrigger`       | A direct message is received                 |
| `Triggers.slack.onAppMention()`    | `SlackAppMentionTrigger`    | The Slack app is mentioned in a channel      |
| `Triggers.slack.onReactionAdded()` | `SlackReactionAddedTrigger` | An emoji reaction is added to a message      |
| `Triggers.slack.trigger()`         | `SlackTrigger` (union)      | Any Slack event you select with `eventTypes` |

Config: Scoped to specific channels or users.

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.slack.onMessage({ channel: SlackChannel.Engineering })]
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `channelId`, `channelName` - where the message was posted
  * `userId`, `userName` - who sent it
  * `text` - message content
  * `timestamp` - this message's Slack `ts`
  * `threadTimestamp` - parent message's `ts` when this message is in a thread, otherwise `null`
  * `permalink` - link to the message
  * `teamId` - the Slack workspace id
  * `channelType` - `"channel"`, `"group"`, `"mpim"`, or `"im"`
  * `files` - array of attached files
  * `attachments`, `blocks` - rich message content
  * `reaction`, `itemTimestamp`, `itemChannelId`, `itemUserId`, `itemType` (only on `SlackReactionAddedTrigger`)
</Expandable>

### Linear

| Trigger                            | Event type                    | Description                                   |
| ---------------------------------- | ----------------------------- | --------------------------------------------- |
| `Triggers.linear.onIssueCreated()` | `LinearIssueCreatedTrigger`   | An issue is created                           |
| `Triggers.linear.onIssueUpdated()` | `LinearIssueUpdatedTrigger`   | An issue is updated                           |
| `Triggers.linear.onComment()`      | `LinearCommentCreatedTrigger` | A comment is added to an issue                |
| `Triggers.linear.trigger()`        | `LinearTrigger` (union)       | Any Linear event you select with `eventTypes` |

Config: Optionally scoped to a team and/or project using generated `LinearTeam` / `LinearProject` constants (same pattern as `SlackChannel` for Slack).

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.linear.onIssueCreated({ team: LinearTeam.Engineering, project: LinearProject.Backend })]
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `action` - `"create"`, `"update"`, or `"remove"`
  * `type` - the entity that fired the event, `"Issue"` or `"Comment"`
  * `actor` - `{ id, name, email, url, type }`
  * `createdAt`, `organizationId`, `webhookTimestamp`, `webhookId`, `url?`
  * `data` - shape depends on `type`. For `"Issue"`: `{ id, identifier, title, description, priorityLabel, state, team, assignee, labels, url, … }`. For `"Comment"`: `{ id, body?, issueId? }`.
</Expandable>

### Attio

Connect Attio under Integrations, configure which Attio events start each workflow in the Terse app, then run `terse generate`. When Attio input triggers are enabled for your workspace, `Triggers.attio` appears in `src/terse.generated.ts`.

| Trigger                                     | Event type                           | Description                                   |
| ------------------------------------------- | ------------------------------------ | --------------------------------------------- |
| `Triggers.attio.onRecordCreated()`          | `AttioRecordCreatedTrigger`          | A record is created                           |
| `Triggers.attio.onRecordUpdated()`          | `AttioRecordUpdatedTrigger`          | A record is updated                           |
| `Triggers.attio.onRecordMerged()`           | `AttioRecordMergedTrigger`           | Two records are merged                        |
| `Triggers.attio.onRecordDeleted()`          | `AttioRecordDeletedTrigger`          | A record is deleted                           |
| `Triggers.attio.onObjectAttributeCreated()` | `AttioObjectAttributeCreatedTrigger` | An object attribute is created                |
| `Triggers.attio.onObjectAttributeUpdated()` | `AttioObjectAttributeUpdatedTrigger` | An object attribute is updated                |
| `Triggers.attio.onListCreated()`            | `AttioListCreatedTrigger`            | A list is created                             |
| `Triggers.attio.onListUpdated()`            | `AttioListUpdatedTrigger`            | A list is updated                             |
| `Triggers.attio.onListDeleted()`            | `AttioListDeletedTrigger`            | A list is deleted                             |
| `Triggers.attio.onListAttributeCreated()`   | `AttioListAttributeCreatedTrigger`   | A list attribute is created                   |
| `Triggers.attio.onListAttributeUpdated()`   | `AttioListAttributeUpdatedTrigger`   | A list attribute is updated                   |
| `Triggers.attio.onListEntryCreated()`       | `AttioListEntryCreatedTrigger`       | A list entry is created                       |
| `Triggers.attio.onListEntryUpdated()`       | `AttioListEntryUpdatedTrigger`       | A list entry is updated                       |
| `Triggers.attio.onListEntryDeleted()`       | `AttioListEntryDeletedTrigger`       | A list entry is deleted                       |
| `Triggers.attio.onNoteCreated()`            | `AttioNoteCreatedTrigger`            | A note is created                             |
| `Triggers.attio.onNoteContentUpdated()`     | `AttioNoteContentUpdatedTrigger`     | A note's content is updated                   |
| `Triggers.attio.onNoteUpdated()`            | `AttioNoteUpdatedTrigger`            | A note is updated                             |
| `Triggers.attio.onNoteDeleted()`            | `AttioNoteDeletedTrigger`            | A note is deleted                             |
| `Triggers.attio.onCommentCreated()`         | `AttioCommentCreatedTrigger`         | A comment is created                          |
| `Triggers.attio.onCommentResolved()`        | `AttioCommentResolvedTrigger`        | A comment is resolved                         |
| `Triggers.attio.onCommentUnresolved()`      | `AttioCommentUnresolvedTrigger`      | A resolved comment is reopened                |
| `Triggers.attio.onCommentDeleted()`         | `AttioCommentDeletedTrigger`         | A comment is deleted                          |
| `Triggers.attio.onTaskCreated()`            | `AttioTaskCreatedTrigger`            | A task is created                             |
| `Triggers.attio.onTaskUpdated()`            | `AttioTaskUpdatedTrigger`            | A task is updated                             |
| `Triggers.attio.onTaskDeleted()`            | `AttioTaskDeletedTrigger`            | A task is deleted                             |
| `Triggers.attio.onWorkspaceMemberCreated()` | `AttioWorkspaceMemberCreatedTrigger` | A workspace member is added                   |
| `Triggers.attio.onCallRecordingCreated()`   | `AttioCallRecordingCreatedTrigger`   | A call recording is created                   |
| `Triggers.attio.trigger()`                  | `AttioTrigger` (union)               | Any Attio events you select with `eventTypes` |

Config: Pass `object: AttioObject.YourObject` on the record helpers (`onRecordCreated`, `onRecordUpdated`, `onRecordMerged`, `onRecordDeleted`) to scope deliveries to a single CRM object. Omit `object` when you want the union of every generated object's attribute shape.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Triggers } from "./terse.generated"

  triggers: [Triggers.attio.onRecordCreated({ object: AttioObject.People })]
  ```
</CodeGroup>

<Expandable title="Event payload">
  Record triggers include a `record` object with Attio ids/slugs plus `values` (and related fields) shaped from your workspace’s Attio object metadata—flattened for use in `onTrigger` and `filter`. Other Attio triggers carry event-specific payloads (lists, notes, tasks, and so on). Every event still exposes `formatForAgentRunner()` and `debugLog()` from `terse-sdk`.
</Expandable>

### Gmail

| Trigger                    | Event type             | Description                                  |
| -------------------------- | ---------------------- | -------------------------------------------- |
| `Triggers.gmail.onEmail()` | `GmailTrigger`         | A new email is received                      |
| `Triggers.gmail.trigger()` | `GmailTrigger` (union) | Any Gmail event you select with `eventTypes` |

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.gmail.onEmail()]
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `subject` - email subject
  * `from`, `to` - sender and recipient
  * `body` - email body
  * `snippet` - short preview
  * `threadId`, `messageId`, `id` - for threading and message lookup
  * `date`, `internalDate` - timestamps
  * `labelIds` - Gmail label ids on the message
  * `attachments` - array of `{ attachmentId, filename, mimeType, isInline, contentId? }`
</Expandable>

### HeyReach

HeyReach fires when LinkedIn outreach events occur (messages, connection requests, campaign milestones, and more). Connect HeyReach under Integrations in the Terse app, then run `terse generate` so `Triggers.heyReach` and typed `HeyReachCampaign` constants appear in `src/terse.generated.ts`.

Each helper maps to one `HeyReachEventType` value (also exported from `terse-sdk`). Use `Triggers.heyReach.trigger({ eventType: HeyReachEventType.MESSAGE_REPLY_RECEIVED })` when you want to pick the event type dynamically.

| Trigger                                           | Event type                                 |
| ------------------------------------------------- | ------------------------------------------ |
| `Triggers.heyReach.onConnectionRequestSent()`     | `HeyReachConnectionRequestSentTrigger`     |
| `Triggers.heyReach.onConnectionRequestAccepted()` | `HeyReachConnectionRequestAcceptedTrigger` |
| `Triggers.heyReach.onMessageSent()`               | `HeyReachMessageSentTrigger`               |
| `Triggers.heyReach.onMessageReplyReceived()`      | `HeyReachMessageReplyReceivedTrigger`      |
| `Triggers.heyReach.onInmailSent()`                | `HeyReachInmailSentTrigger`                |
| `Triggers.heyReach.onInmailReplyReceived()`       | `HeyReachInmailReplyReceivedTrigger`       |
| `Triggers.heyReach.onFollowSent()`                | `HeyReachFollowSentTrigger`                |
| `Triggers.heyReach.onLikedPost()`                 | `HeyReachLikedPostTrigger`                 |
| `Triggers.heyReach.onViewedProfile()`             | `HeyReachViewedProfileTrigger`             |
| `Triggers.heyReach.onCampaignCompleted()`         | `HeyReachCampaignCompletedTrigger`         |
| `Triggers.heyReach.onLeadTagUpdated()`            | `HeyReachLeadTagUpdatedTrigger`            |

Config: Pass `campaigns` with generated `HeyReachCampaign` instances to handle only those campaigns; omit it to receive events from all campaigns.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Triggers } from "./terse.generated"

  triggers: [Triggers.heyReach.onMessageReplyReceived()]
  ```
</CodeGroup>

<Expandable title="Event payload">
  Shared fields on HeyReach triggers:

  * `eventId`, `createdAt` — event metadata
  * `lead` — optional lead profile (`profile_url`, names, company, tags, and related fields)
  * `campaign` — optional `{ id, name, status }`
  * `linkedInAccount` — optional sender account metadata
  * `messageBody` — present on message-oriented events when HeyReach provides body text
  * `postUrl` — present on liked-post events
  * `tags` — present on lead tag updates

  Use `HeyReachTrigger` as the union type, or the specific trigger type that matches your helper.
</Expandable>

### WorkOS

| Trigger                                   | Event type                                   | Description                                   |
| ----------------------------------------- | -------------------------------------------- | --------------------------------------------- |
| `Triggers.workOS.onUserCreated()`         | `WorkOSUserCreatedTrigger`                   | A user is created                             |
| `Triggers.workOS.onUserUpdated()`         | `WorkOSUserUpdatedTrigger`                   | A user is updated                             |
| `Triggers.workOS.onUserDeleted()`         | `WorkOSUserDeletedTrigger`                   | A user is deleted                             |
| `Triggers.workOS.onMembershipCreated()`   | `WorkOSOrganizationMembershipCreatedTrigger` | An organization membership is created         |
| `Triggers.workOS.onMembershipUpdated()`   | `WorkOSOrganizationMembershipUpdatedTrigger` | An organization membership is updated         |
| `Triggers.workOS.onMembershipDeleted()`   | `WorkOSOrganizationMembershipDeletedTrigger` | An organization membership is deleted         |
| `Triggers.workOS.onMembershipChanged()`   | `WorkOSMembershipTrigger`                    | Any membership change                         |
| `Triggers.workOS.onInvitationCreated()`   | `WorkOSInvitationCreatedTrigger`             | An invitation is created                      |
| `Triggers.workOS.onInvitationResent()`    | `WorkOSInvitationResentTrigger`              | An invitation is resent                       |
| `Triggers.workOS.onInvitationSent()`      | `WorkOSInvitationTrigger`                    | An invitation is created or resent            |
| `Triggers.workOS.onInvitationAccepted()`  | `WorkOSInvitationAcceptedTrigger`            | An invitation is accepted                     |
| `Triggers.workOS.onInvitationRevoked()`   | `WorkOSInvitationRevokedTrigger`             | An invitation is revoked                      |
| `Triggers.workOS.onOrganizationCreated()` | `WorkOSOrganizationTrigger`                  | An organization is created                    |
| `Triggers.workOS.trigger()`               | `WorkOSTrigger` (union)                      | Any WorkOS event you select with `eventTypes` |

Config: Use a typed helper for one event, or pass `eventTypes` to `trigger()` for multiple.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { WorkOSEventType } from "terse-sdk"

  triggers: [Triggers.workOS.trigger({ eventTypes: [WorkOSEventType.USER_CREATED] })]
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `eventId`, `createdAt` - event metadata
  * `user` - present on user events (and optional on invitation events): `{ id, email, firstName?, lastName?, emailVerified, profilePictureUrl? }`
  * `membership` - present on membership events: `{ id, userId, organizationId, role: { slug }, status }`
  * `invitation` - present on invitation events: `{ id, email, organizationId, inviterEmail?, state, acceptedAt? }`
  * `organization` - present on organization events: `{ id, name }`
</Expandable>

### Web Monitor

Track web changes continuously and get notified when there are updates.

You can add multiple `Triggers.webMonitor.onEvent(...)` triggers to the same workflow. Terse runs each monitor independently and calls the same `onTrigger` handler for every matching event.

| Trigger                         | Event type          | Description                                                  |
| ------------------------------- | ------------------- | ------------------------------------------------------------ |
| `Triggers.webMonitor.onEvent()` | `WebMonitorTrigger` | Fires when scheduled web monitoring detects relevant changes |

<CodeGroup>
  ```ts TypeScript theme={null}
  import { createJob, generateText } from "terse-sdk"
  import * as z from "zod"
  import { Skills, SlackChannel, Triggers } from "./terse.generated"

  const outputSchema = z.object({
      summary: z.string(),
      sentiment: z.enum(["positive", "negative", "neutral"])
  })

  createJob({
      name: "Web monitor trigger",
      triggers: [
          Triggers.webMonitor.onEvent({
              query: "What are a16z's latest investments?",
              frequency: { number: 1, unit: "day" },
              outputSchema
          })
      ],
      onTrigger: async event => {
          const summary = event.payload.summary
          const sentiment = event.payload.sentiment
          const sources = event.sourceUrls.join(", ")

          await generateText({
              prompt:
                  "You are alerted when the web monitor finds new material. Read the event below (query, payload, and structured fields), decide what matters, and post a concise summary to Slack.\n\n" +
                  `${event.formatForAgentRunner()}\n\nSummary: ${summary}\nSentiment: ${sentiment}\nSources: ${sources}`,
              skills: [Skills.slack({ channel: SlackChannel.AllTerseInc })]
          })
      }
  })
  ```
</CodeGroup>

If you need to annotate monitor events manually, import `WebMonitorTriggerFor` from `terse-sdk` and bind it to your Zod schema:

```ts theme={null}
import type { WebMonitorTriggerFor } from "terse-sdk";
import * as z from "zod";

const outputSchema = z.object({
  summary: z.string(),
  sentiment: z.enum(["positive", "negative", "neutral"]),
});

type MonitorEvent = WebMonitorTriggerFor<typeof outputSchema>
```

#### Writing effective queries

The monitor matches by intent, not keywords. Write your query as a natural sentence describing what you care about — not a search string.

Do:

* Write in plain language: `"What are a16z's latest investments?"`
* Track narratives and social buzz: `"What are people saying on Twitter about Cloud agents?"`
* Be specific about subject and signal: `"FDA approval decisions for GLP-1 drugs"`
* Combine related signals: `"OpenAI or Anthropic model releases and benchmark results"`

Don’t:

* Use Boolean operators (`AND`, `OR`, `NOT`) — this is not a search engine
* Include specific dates — monitors track new developments going forward, not history
* Write keyword dumps: `"acme corp acme funding raise series B"`

Choosing frequency:

| Frequency | Best for                                                        |
| --------- | --------------------------------------------------------------- |
| `1h`      | Breaking news, social signals, fast-moving markets              |
| `1d`      | Press releases, blog posts, job postings, pricing page changes  |
| `1w`      | Regulatory filings, quarterly reports, slow competitive signals |

## System triggers

### Schedule

| Trigger                    | Event type    | Description              |
| -------------------------- | ------------- | ------------------------ |
| `Triggers.schedule.cron()` | `CronTrigger` | Fires on a cron schedule |

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.schedule.cron({ expression: "0 9 * * 1" })] // Every Monday at 9am
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `isManualTrigger` - whether the run was triggered manually from the dashboard
  * `manualContext` - optional context string provided on manual triggers
  * `inputId` - id of the schedule input
</Expandable>

### Webhook

| Trigger                        | Event type       | Description                                               |
| ------------------------------ | ---------------- | --------------------------------------------------------- |
| `Triggers.webhook.onRequest()` | `WebhookTrigger` | Fires when an HTTP request hits the generated webhook URL |

Webhook URLs are auto-generated on deploy and remain stable across redeploys. After `terse deploy`, the CLI prints that URL for each affected workflow so you can copy it straight into your caller or load test.

Pass a type argument to `Triggers.webhook.onRequest<YourPayload>()` so `onTrigger` and `filter` see your JSON body shape on `event.body` (defaults to `unknown` when omitted).

<CodeGroup>
  ```ts TypeScript theme={null}
  triggers: [Triggers.webhook.onRequest()]
  ```
</CodeGroup>

<Expandable title="Event payload">
  * `body` - parsed JSON request body (typed when you use `Triggers.webhook.onRequest<...>()`)
  * `headers` - HTTP headers
  * `method` - HTTP method (GET, POST, etc.)
</Expandable>

## Common event interface

All triggers resolve to the shared canonical `Trigger` payload shape. Your handler receives the concrete subtype for the trigger you selected.

<CodeGroup>
  ```ts TypeScript theme={null}
  interface Trigger {
    readonly integrationType: string
    readonly eventType: string
  }
  ```
</CodeGroup>

Each trigger subtype adds its own canonical fields, such as Slack message metadata, GitHub PR data, webhook request bodies, or cron trigger context.
