# Automation triggers

> A trigger is what causes an automation to run. Automations can be triggered by content events, a schedule, an incoming webhook, or a call from Uniform's AI agent.

Source: https://docs.uniform.app/docs/guides/automations/triggers

A trigger is what causes an [automation](https://docs.uniform.app/docs/guides/automations) to run. An automation declares one or more triggers, and every run knows which one fired it (via `context.trigger`).

| Trigger | Fires when |
| --- | --- |
| [Content events](#content-events) | Something happens to content in the project, such as an entry or composition being saved. |
| [Schedule](#schedule) | A recurrence rule comes due. |
| [Incoming webhook](#incoming-webhooks) | An external system calls a URL that Uniform hosts for the automation. |
| [AI tool](#ai-tool) | Scout chooses to call the automation as a tool. |

Triggers can be [combined](#multiple-triggers). Content-event and incoming-webhook triggers can also carry an optional [filter](#filtering), which is a small expression that decides whether an event is even worth a run before one is created.

## Content events

Content-event triggers use the same event catalog as [webhooks](https://docs.uniform.app/docs/guides/webhooks), so an automation can subscribe to events such as `entry.changed`, `entry.published`, or `composition.published`. See the [event catalog](https://www.svix.com/event-types/us/org_2HgMLYs57QWpjfM80UPmW98qTgT/) for the full list of events and their payloads.

```ts
triggers: [{ type: 'entry.changed' }];
```

An automation can subscribe to any number of content events, and each one fires the automation independently. Decide what an event is worth in two places: a [filter](#filtering) to drop uninteresting events before a run is created, and your handler code for anything finer, such as "only this entity", "only drafts", or "only this workflow stage". The automation fires for every event that passes its filter and decides the rest in code, returning `rejected` for the events it doesn't act on.

## Schedule

A schedule trigger takes an [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545) recurrence rule and an IANA timezone to compute that rule in, which prevents daylight saving time issues:

```ts
triggers: [
  {
    type: 'schedule',
    rrule: 'FREQ=DAILY;BYHOUR=2;BYMINUTE=0;BYSECOND=0',
    timezone: 'America/Los_Angeles',
  },
];
```

Execution granularity is about one minute, so a rule that asks to run every 30 seconds actually runs about once a minute. Using a `COUNT` rule requires also specifying a `DTSTART` to anchor when the count should start. The next upcoming run time is shown on the automations list in the dashboard.

A scheduled automation can also be run on demand from the dashboard, which is the way to exercise one without waiting for its next occurrence. This works even while the automation is disabled, so you can try it before you turn it on.

## Incoming webhooks

An incoming webhook trigger gives the automation a URL that external systems can call with an arbitrary payload. Your handler receives the raw request (`method`, `headers`, `query`, `rawBody`). Because every source system authenticates webhooks differently, you validate the request in your code: verify its authenticity and parse the body yourself.

```ts
import { defineAutomation } from '@uniformdev/automations-sdk';
import * as z from 'zod/mini';

const schema = z.object({ id: z.string(), action: z.string() });

export default defineAutomation({
  metadata: { name: 'On inbound webhook', triggers: [{ type: 'incomingWebhook' }] },
  handler: async ({ input, log }) => {
    if (!verifySignature(input)) {
      return { outcome: 'unauthorized' };
    }

    const body = schema.safeParse(JSON.parse(input.rawBody));
    if (!body.success) {
      log.warning(`Ignoring malformed payload:\n${z.prettifyError(body.error)}`);
      return { outcome: 'rejected' };
    }

    log.info(`Handling ${body.data.action} for ${body.data.id}`);
  },
});
```

The webhook endpoint acknowledges receipt immediately with a `200` and runs the automation asynchronously, so a `200` does not indicate the automation succeeded.

The captured request (body, headers, and query combined) may be at most 64 KB. Anything larger is rejected with a `413` and no run is created.

A [filter](#filtering) can drop uninteresting requests before a run. For example, use `filter: 'input.method == "POST"'`, or match a header that identifies the event kind, which is handy for a single endpoint that receives many event types.

## AI tool

An AI-tool automation is exposed to [Scout](https://docs.uniform.app/docs/guides/ai/scout) as a tool. The `name` and `description` drive the agent's decision to call it, and `inputSchema` defines the arguments the agent provides:

```ts
export default defineAutomation({
  metadata: {
    name: 'Monster info',
    description: 'Gets information about a D&D 5e monster',
    triggers: [{ type: 'aiTool' }],
    inputSchema: z.object({ name: z.string().describe('The monster name, lowercase') }),
  },
  handler: async ({ input, log }) => {
    // make api calls, etc

    // logs of all levels are returned to the Scout context
    log.info(JSON.stringify(result));
  },
});
```

Two things differ from the other triggers:

- The run executes under the **identity of the user who invoked Scout**, so you cannot assign it a role. See [the automation identity](https://docs.uniform.app/docs/guides/automations/code-automations#the-automation-identity).
- The run's logs and outcome are returned to the agent as the tool result, and AI tool runs do not appear on the automation runs list.

## Multiple triggers

An automation can declare several triggers and share one handler. `context.trigger` tells you which trigger fired, and `input` is a discriminated union you narrow on `input.eventType`. Schedule and incoming-webhook runs carry an `eventType` of `schedule` and `incomingWebhook`, so every kind of trigger narrows the same way:

```ts
export default defineAutomation({
  metadata: {
    name: 'Reindex on publish or nightly',
    triggers: [
      { type: 'entry.published' },
      { type: 'schedule', rrule: 'FREQ=DAILY;BYHOUR=3;BYMINUTE=0;BYSECOND=0', timezone: 'Etc/UTC' },
    ],
    permissions: { role: 'developer' },
  },
  handler: async ({ input, log }) => {
    if (input.eventType === 'schedule') {
      log.info(`Nightly reindex at ${input.firedAt}`);
      return;
    }
    // input is narrowed to the entry.published payload here
    log.info(`Reindexing published entry ${input.id}`);
  },
});
```

Two rules apply when combining triggers:

- You cannot combine an `aiTool` trigger with any other trigger, because it has a different security model.
- You may only have one trigger of each type. For example, an automation cannot have two schedules.

## Filtering

Content-event and incoming-webhook triggers can carry a `filter`: a boolean expression that gates a run **before** it is created, so events you don't care about never create a run. Filters are written in [CEL](https://github.com/google/cel-spec) (Common Expression Language) and evaluated against `{ input, trigger }`, where `input` is the event payload (or the webhook request envelope) and `trigger` is the trigger's config.

```ts
triggers: [
  // Only run when an "article" entry changes; skip every other content type.
  { type: 'entry.changed', filter: 'input.type == "article"' },
];
```

A filter must evaluate to a boolean, and the run proceeds only when it is `true`.

Filters are the coarse gate. Your handler code does everything a filter can't, such as anything needing secrets, external lookups, or richer logic, and it returns `rejected` for the rest. Use a filter when a simple expression over the payload will do, since it avoids the cost, log noise, and run-history clutter of a run that would immediately reject itself.

Two things to know about filters:

- **Only content events and incoming webhooks take a filter.** A schedule has no payload to test (change the recurrence rule instead), and an AI-tool call was already a deliberate choice by the agent.
- **An evaluation error is recorded, not swallowed.** If a filter throws at runtime, for example because it references a field that isn't present on a particular payload, the automation records a `failure` run with the error in its logs instead of silently dropping the event. That keeps the problem visible.
