Triggers

Early access feature

The "Automations" feature is in early access and only available to select customers. If you are interested in getting access, please contact us.

A trigger is what causes an automation to run. An automation declares one or more triggers, and every run knows which one fired it (via context.trigger).

TriggerFires when
Content eventsSomething happens to content in the project, such as an entry or composition being saved.
ScheduleA recurrence rule comes due.
Incoming webhookAn external system calls a URL that Uniform hosts for the automation.
AI toolScout chooses to call the automation as a tool.

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

Content-event triggers use the same event catalog as webhooks, so an automation can subscribe to events such as entry.changed, entry.published, or composition.published. See the event catalog for the full list of events and their payloads.

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

A schedule trigger takes an RFC 5545 recurrence rule and an IANA timezone to compute that rule in, which prevents daylight saving time issues:

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.

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.

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

An AI-tool automation is exposed to Scout as a tool. The name and description drive the agent's decision to call it, and inputSchema defines the arguments the agent provides:

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

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:

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.

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

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.