---
title: "Automations"
description: "Event-triggered and scheduled automations with natural-language conditions"
---

# Automations

An **automation** is an agent-executed task that fires on a schedule, when an
event occurs, or when a webhook is called: _when X happens, do Y_.
**Scheduled**, **Event**, and **Webhook** are trigger types, not separate
products. The agent executes the instructions with the same action and tool
lifecycle as interactive chat; connected MCP access is limited to the
automation's explicit `mcpTools` allowlist.

The framework originally exposed scheduled automations as **Recurring Jobs**.
That name, the `jobs/<name>.md` storage path, and the `manage-jobs` tool remain
for compatibility. New product guidance uses **Automations** as the umbrella;
see [Recurring Jobs](/docs/recurring-jobs) when maintaining a legacy
schedule-only integration or working with scheduler internals.

<Diagram id="doc-block-jdto24" title="When X happens, do Y" summary="An event fires on the bus, an optional natural-language condition gates it, and the agent runs the automation body with full tool access.">

```html
<div class="auto-flow">
  <div class="diagram-card">
    <span class="diagram-pill">Event</span
    ><small class="diagram-muted"><code>calendar.booking.created</code></small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">Condition</span
    ><small class="diagram-muted"
      >Model checks: &ldquo;email ends with @builder.io&rdquo;</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card accent">
    <span class="diagram-pill accent">Agent runs the body</span
    ><small class="diagram-muted"
      >actions &middot; web-request &middot; MCP &middot; sub-agents</small
    >
  </div>
</div>
```

```css
.auto-flow {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.auto-flow .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  min-width: 180px;
}
.auto-flow .diagram-arrow {
  font-size: 22px;
}
```

</Diagram>

## Trigger types {#trigger-types}

| Type       | Fires when                                                    | Key field                          |
| ---------- | ------------------------------------------------------------- | ---------------------------------- |
| `schedule` | A cron expression matches (same as recurring jobs)            | `schedule` (cron)                  |
| `event`    | A matching event is emitted on the framework event bus        | `event` (name)                     |
| `webhook`  | An HTTP `POST` arrives at the automation's secret webhook URL | The URL returned when it's created |

Event and webhook triggers can include a `condition`: a natural-language string a model checks against the payload before dispatch. If the condition doesn't match, the automation is skipped and records a `skipped` status. If the condition can't be evaluated, the automation doesn't run. See [Conditions](#conditions).

### Webhook triggers {#webhook-triggers}

Creating a webhook automation returns a URL path of the form
`/_agent-native/automations/webhook/<token>`. The token is the secret, so give
the URL only to the service that calls it. Each `POST` to that URL becomes the
automation's payload, up to 64 KB. A body sent with a JSON content type is
parsed, and any other body is passed as text. The endpoint answers `202` and
runs the automation in the background.

Send a stable event ID in an `X-Webhook-Event-Id`, `X-Event-Id`, or
`X-GitHub-Delivery` header, or as a top-level string `id` in a JSON body. A
repeated delivery with the same ID isn't queued again and gets a `200` reply
marked as a duplicate. The ID only stops duplicate deliveries. A failed run can
be retried, and the retry can repeat side effects the failed run already
caused. Once a delivery fails for good, its ID is released, and a later
delivery with that ID is queued as a new one.

## Creating automations {#creating}

### By asking the agent

> "When someone books a meeting with a @builder.io email, message me in Slack."

The agent discovers available events, confirms the plan, and writes the automation for you.

### From the Automations UI

Open **Settings › Automations**. It is the primary place to view scheduled,
event-triggered, and webhook-triggered automations, inspect status, run one
now, pause or resume them, and delete them. Old `/agent#jobs` links redirect
there.

**Run now** is the quickest way to test an automation. It runs the
automation's instructions immediately, with their real side effects. It skips
the trigger and the condition, and it doesn't change the next scheduled run,
with one exception: if the framework paused the automation, a successful run
turns it back on, and a scheduled automation gets its next run calculated
again. The agent can do the same with `manage-automations` and `action=run-now`.

<Callout tone="info">

**Personal and organization automations use the same management surface.**
Organization event automations are visible to members but always run as their
creator. An administrator can manage the definition, but cannot change who it
runs as.

</Callout>

The third path — writing the `jobs/<name>.md` file by hand via `resourcePut` — works exactly as it does for [recurring jobs](/docs/recurring-jobs#creating). For an event-driven automation you add the event-trigger frontmatter below to that same file. An event-triggered job sets `schedule: ""` and supplies `triggerType: event`, an `event` name, and an optional `condition`:

<AnnotatedCode
  id="doc-block-8al47d"
  title="An event-triggered automation"
  filename="jobs/slack-on-builder-booking.md"
  language="markdown"
  code={
    '---\nschedule: ""\nenabled: true\ntriggerType: event\nevent: calendar.booking.created\ncondition: "attendee email ends with @builder.io"\nmode: agentic\ndomain: calendar\nrunAs: creator\n---\nSend a Slack message to #sales with the booking details.\nUse the web-request tool to POST to ${keys.SLACK_WEBHOOK}.'
  }
  annotations={[
    {
      lines: "2",
      label: "No cron",
      note: 'Event triggers set `schedule` to `""` — the cron field stays empty.',
    },
    {
      lines: "4-5",
      label: "The trigger",
      note: "`triggerType: event` plus the `event` name subscribes this automation to the bus.",
    },
    {
      lines: "6",
      label: "Gate",
      note: "An optional natural-language `condition`, checked by a model against the payload before dispatch.",
    },
    {
      lines: "12",
      label: "Server-side secret",
      note: "`${keys.SLACK_WEBHOOK}` is resolved server-side — the raw value never enters the agent's context.",
    },
  ]}
/>

## Automation frontmatter {#frontmatter}

Automations share every field in the [recurring-jobs frontmatter table](/docs/recurring-jobs#frontmatter). These additional fields control event triggers, conditions, and the execution mode:

| Field         | Type                                     | Default      | Description                                                                                                                                   |
| ------------- | ---------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggerType` | `"schedule"` \| `"event"` \| `"webhook"` | `"schedule"` | How the automation fires                                                                                                                      |
| `event`       | string                                   | _(optional)_ | Event name to subscribe to (event triggers only)                                                                                              |
| `condition`   | string                                   | _(optional)_ | Natural-language condition evaluated before dispatch (event and webhook triggers)                                                             |
| `mode`        | `"agentic"`                              | `"agentic"`  | Full agent loop (the only supported mode). `"deterministic"` was removed — it was never implemented, and automations that set it never fired. |
| `domain`      | string                                   | _(optional)_ | Grouping tag (mail, calendar, clips, etc.)                                                                                                    |

For an event or webhook trigger, `schedule` is `""` (empty); for a schedule trigger it carries the cron expression. The dispatcher also writes the same managed `lastRun` / `lastStatus` / `lastError` fields the scheduler does, plus a `"skipped"` status when a condition evaluates to false. When a condition can't be evaluated, see [Conditions](#conditions) for what gets recorded.

`model` selects a per-automation model. `mcpTools` is an explicit list of MCP
tool names such as `mcp__server__tool`. The shared execution runner exposes
only those configured MCP tools and fails clearly if a named tool or its
connected grant is unavailable.

### Organization execution identity {#organization-identity}

Organization Scheduled and Event automations are shared for visibility and
management, not execution identity:

- `createdBy` is required for an organization event automation, and `runAs` is
  `creator`.
- Event automations match only events emitted for that creator. An
  organization-scoped automation is not an organization-wide event broadcast.
- Organization administrators may update or delete the automation, but cannot
  replace `createdBy` or retarget its identity.
- Before execution, the framework verifies that the creator still exists and
  remains an organization member. If the creator was removed—or membership
  cannot be verified—the automation does not run.

## The event bus {#event-bus}

Integrations register events at module load time. The bus validates payloads against [Standard Schema](https://standardschema.dev) definitions and dispatches to subscribers.

### Built-in events {#built-in-events}

| Event                  | Source                                         |
| ---------------------- | ---------------------------------------------- |
| `test.event.fired`     | Manual / `manage-automations` action=fire-test |
| `agent.turn.completed` | Agent chat                                     |
| `calendar.*`           | Calendar integration                           |
| `clip.*`               | Clips integration                              |
| `mail.*`               | Mail integration                               |

Call `manage-automations` with `action=list-events` from the agent to see all registered events with descriptions and payload schemas for the current template.

### Emitting custom events (for developers) {#emitting-events}

The rest of this page is written for the person asking the agent to create automations. Registering a new event type is a code-level change a template developer makes so users and the agent have something to trigger on.

Register an event type in a server plugin, then emit it from actions or webhook handlers:

```ts
import { registerEvent, emit } from "@agent-native/core/event-bus";
import { z } from "zod";

// Register the event type (once, at module load)
registerEvent({
  name: "order.completed",
  description: "A customer completed an order",
  payloadSchema: z.object({
    orderId: z.string(),
    customerEmail: z.string(),
    total: z.number(),
  }),
  example: {
    orderId: "ord_123",
    customerEmail: "jane@example.com",
    total: 49.99,
  },
});

// Emit the event (from an action, webhook handler, etc.)
emit(
  "order.completed",
  {
    orderId: "ord_123",
    customerEmail: "jane@example.com",
    total: 49.99,
  },
  { owner: "user@example.com" },
);
```

The `owner` in the emit metadata scopes which automations fire. Personal and
organization event automations both require an exact match with their
`createdBy` identity.

## Conditions {#conditions}

Conditions are natural-language strings a model checks against the event or webhook payload. This is a yes/no classification, not a generation task.

- **Empty or missing condition** = unconditional (always fires).
- Results are cached for 5 minutes, keyed by a SHA-256 hash of the condition and payload, with at most 500 entries.
- Payload is truncated to 4000 characters before it is sent to the model.

<Callout tone="info">

A condition that can't be evaluated never counts as a match or as a non-match, and the automation doesn't run. A provider error records `lastStatus: error` with the reason in `lastError`, and the event is retried. If no API key is available to evaluate the condition, an event automation records an error for each matching event and pauses itself after three in a row. Once a key is available, a running scheduler turns it back on by itself within about 15 minutes. On a host where no scheduler runs, turn it back on in **Settings › Automations**. A webhook automation in the same state doesn't pause. Its delivery fails and is retried.

</Callout>

Examples of conditions:

- `"attendee email ends with @builder.io"`
- `"the order total is greater than $100"`
- `"the message contains the word 'urgent'"`

## The web-request tool {#web-request}

Automations use the `web-request` tool for outbound HTTP. It supports `${keys.NAME}` placeholders in the URL, headers, and body:

<Callout tone="info">

**Ad-hoc HTTP vs configured workflow systems.** `web-request` calls whatever URL an ad-hoc key resolves to — good for a one-off webhook the agent or a user wires up. If you need a pre-configured, schema-typed connection to an external workflow system like n8n or Zapier — with idempotent callbacks and static allowed origins instead of arbitrary URLs — see [Workflow Connectors](/docs/automation-connectors) instead.

</Callout>

```
POST to ${keys.SLACK_WEBHOOK}

Headers: {"Authorization": "Bearer ${keys.API_TOKEN}"}

Body: {"text": "New booking from ${attendeeEmail}"}
```

Placeholders are resolved **server-side** after the agent emits the tool call — the raw secret value never enters the agent's context.

### Parameters {#web-request-params}

| Parameter      | Type    | Default       | Description                                                                                                                                                                                                                         |
| -------------- | ------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`          | string  | —             | Full URL. May contain `${keys.NAME}` references.                                                                                                                                                                                    |
| `method`       | string  | `GET`         | HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD).                                                                                                                                                                                  |
| `headers`      | string  | `{}`          | JSON object of headers. May contain `${keys.NAME}`.                                                                                                                                                                                 |
| `body`         | string  | —             | Request body. May contain `${keys.NAME}`.                                                                                                                                                                                           |
| `timeout_ms`   | number  | 15000         | Timeout in milliseconds (max 30000).                                                                                                                                                                                                |
| `maxChars`     | number  | 32000         | Maximum response-body characters returned in the agent's context (max 200000). It caps body characters, not the whole formatted tool response, so optional metadata and links can add characters. This does not limit a saved file. |
| `responseMode` | string  | `auto`        | How to return the response: `auto` (HTML becomes markdown, JSON/text stays raw), `raw`, `text`, `markdown`, `links`, `metadata`, or `matches` with `search`.                                                                        |
| `extract`      | string  | `readability` | HTML extraction strategy: `readability`, `all-visible` (full visible body text), or `none` (full document).                                                                                                                         |
| `includeLinks` | boolean | varies        | Whether extracted HTML responses include a compact links list. Defaults to `true` for inline responses and `false` when `saveToFile` is set.                                                                                        |
| `search`       | object  | —             | Optional post-fetch search over the extracted content: `{ query, queries, terms, regex, regexFlags, source, maxMatches, contextChars, caseSensitive }`.                                                                             |
| `saveToFile`   | string  | —             | Workspace file path for saving the full response, up to 20 MiB. With `search`, returns full matches up to `maxChars`; otherwise returns a 2,000-character preview.                                                                  |

By default, `maxChars` caps only response-body characters returned in the agent's context. It does not cap the whole formatted tool response, so optional metadata and links can add characters. Inline responses are limited to 1 MiB, except extracted HTML or XHTML, which can be up to 20 MiB. Raw HTML and other inline responses remain limited to 1 MiB.

`saveToFile` supports responses up to 20 MiB. HTML or XHTML saved with a non-raw `responseMode` is written as extracted Markdown or text. Raw mode and non-HTML responses are saved as decoded response body text without HTML extraction. When `saveToFile` is used with `search`, the tool returns full search matches bounded by `maxChars`. Without `search`, it returns a 2,000-character preview. `searchResultsTruncated` means `maxChars` cut the formatted result. `searchSourceTruncated` means the tool scanned only the first 500,000 characters of the selected search source.

An over-limit error reports the observed size and applicable cap, and explains how to recover. Use `saveToFile` for responses up to 20 MiB, or request a smaller response if it exceeds that limit.

## Keys {#keys}

Keys are ad-hoc secrets created by users or the agent for automation use (e.g. `SLACK_WEBHOOK`, `HUBSPOT_API_KEY`). They differ from registered secrets (`registerRequiredSecret`) in that they have no template-defined metadata or onboarding step.

- Created via the settings UI or the `/_agent-native/secrets/adhoc` API.
- A key created through the API can include a `urlAllowlist` that restricts which origins the key can be sent to (origin-level matching). Keys added in Settings have no allowlist.
- The raw value is never exposed to the AI — only `${keys.NAME}` placeholders appear in the agent's context.
- Resolution falls back from user scope to workspace scope, so users can override shared keys.

## Agent tools {#agent-tools}

Trigger-aware personal and organization operations use `manage-automations`
with an `action` parameter:

| Action        | Purpose                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------- |
| `list-events` | Discover all registered events with descriptions and payload schemas                     |
| `list-hosts`  | List paired execution hosts before assigning one to an automation                        |
| `list`        | List all automations with status; filter by domain or enabled                            |
| `define`      | Create a new automation (name, trigger type, event, condition, body)                     |
| `update`      | Update an existing automation (enabled, condition, body)                                 |
| `delete`      | Delete an automation (always confirms with user first)                                   |
| `run-now`     | Run one automation immediately, with real side effects. Skips the trigger and condition. |
| `fire-test`   | Emit a `test.event.fired` event. Only automations subscribed to that event respond.      |

Additional tool: `web-request` — outbound HTTP with `${keys.NAME}` substitution.

`define` and `update` accept personal or organization `scope`, a `model`
override, and an `mcpTools` allowlist. `manage-jobs` remains the compatibility
tool for existing schedule-only integrations and delivery metadata.

## API endpoints {#api}

| Endpoint                                    | Method | Description                     |
| ------------------------------------------- | ------ | ------------------------------- |
| `/_agent-native/automations`                | GET    | List all automations (parsed)   |
| `/_agent-native/automations/fire-test`      | POST   | Emit a `test.event.fired` event |
| `/_agent-native/automations/webhook/:token` | POST   | Trigger a webhook automation    |
| `/_agent-native/secrets/adhoc`              | GET    | List ad-hoc keys (no values)    |
| `/_agent-native/secrets/adhoc`              | POST   | Create or update an ad-hoc key  |
| `/_agent-native/secrets/adhoc/:name`        | DELETE | Delete an ad-hoc key            |

<Endpoint id="doc-block-uyjlfp" title="Fire a test event" method="POST" path="/_agent-native/automations/fire-test" summary="Emit a test.event.fired event to exercise automations subscribed to it" responses={[
  {
    "status": "200",
    "description": "Event emitted; matching automations are dispatched through the normal condition + ownership path."
  }
]}>

Checks the event path (matching, owner scoping, and condition) for an automation subscribed to `test.event.fired`. It emits only that event, so it can't trigger an automation that listens for a real event such as `calendar.booking.created`. To try a real automation, use **Run now**. Equivalent to the `manage-automations` action `fire-test`.

</Endpoint>

## How dispatch works {#dispatch}

<Diagram id="doc-block-1ee1cky" title="The dispatch path" summary="Event and schedule acquisition stay separate, then converge on one identity-checked background execution lifecycle.">

```html
<div class="disp">
  <div class="diagram-box accent">event fired on the bus</div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">match</span
    ><small class="diagram-muted"
      >load enabled automations subscribed to this event name</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">scope</span
    ><small class="diagram-muted"
      >match <code>createdBy</code> to the event owner; verify org
      membership</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill warn">condition</span
    ><small class="diagram-muted"
      >model yes/no on the payload &mdash; false &rarr; <code>skipped</code>,
      failure &rarr; <code>error</code></small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card accent">
    <span class="diagram-pill accent">run</span
    ><small class="diagram-muted"
      >shared runner resolves identity, model, MCP allowlist, continuation,
      delivery, and usage</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card ok">
    <span class="diagram-pill ok">record</span
    ><small class="diagram-muted"
      >write <code>lastRun</code> / <code>lastStatus</code> /
      <code>lastError</code></small
    >
  </div>
</div>
```

```css
.disp {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 540px;
}
.disp .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
}
.disp .diagram-box {
  align-self: flex-start;
}
.disp .diagram-arrow {
  font-size: 18px;
  align-self: center;
}
```

</Diagram>

## Example {#example}

**User:** "When someone books with a @builder.io email, message me in Slack."

**Agent flow:**

1. Calls `manage-automations` with `action=list-events` — finds `calendar.booking.created`.
2. Confirms the plan with the user.
3. Calls `manage-automations` with `action=define`:
   - `name`: `slack-on-builder-booking`
   - `trigger_type`: `event`
   - `event`: `calendar.booking.created`
   - `condition`: `attendee email ends with @builder.io`
   - `mode`: `agentic`
   - `domain`: `calendar`
   - `body`: `Send a Slack message to #sales with the booking details. Use the web-request tool to POST to ${keys.SLACK_WEBHOOK}.`
4. The automation is saved as `jobs/slack-on-builder-booking.md` and begins listening immediately.

## More examples {#more-examples}

### Notify via webhook when a plan is commented on

Ask the plan agent: _"When someone adds a human comment on a plan, POST a
notification to my webhook."_

```yaml filename="jobs/notify-on-plan-comment.md"
---
schedule: ""
triggerType: event
event: plan.commented
condition: "resolutionTarget is human or resolutionTarget is null"
mode: agentic
domain: plan
enabled: true
---

POST to ${keys.NOTIFY_WEBHOOK} with a JSON body:
{"title": "<plan title>", "excerpt": "<comment excerpt>", "author": "<author email or null>", "url": "<app base url + path>"}
```

Set `NOTIFY_WEBHOOK` to any HTTP endpoint — a Slack incoming webhook, a generic
notification service, or a custom receiver. The `web-request` tool resolves
`${keys.NOTIFY_WEBHOOK}` server-side; the raw URL never appears in the agent's
context. See [Events and Automations — Event reference](/docs/template-plan-automations#event-reference)
for the full `plan.commented` payload reference and all four plan events.

## What's next

- [**Recurring Jobs**](/docs/recurring-jobs) — legacy compatibility and advanced scheduled-job fields
- [**Actions**](/docs/actions-overview) — automations can call any registered action via the agent loop
- [**Workflow Connectors**](/docs/automation-connectors) — pre-configured n8n/Zapier-style workflow invocation, for when ad-hoc `web-request` isn't a strict enough contract
- [**Security**](/docs/security) — input validation and secret handling
- [**Events and Automations**](/docs/template-plan-automations) — the Plan event reference and automation recipes
