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

# Automations

An **automation** is a rule: _when X happens, do Y_ — described in natural language. The agent executes the instructions, so automations have access to every action, tool, and MCP server the agent can use in an interactive chat.

Automations extend [recurring jobs](/docs/recurring-jobs) with **event triggers**, **natural-language conditions**, and **outbound HTTP** via the `web-request` tool. They use the same `jobs/<name>.md` file format, storage, and "create three ways" workflow as recurring jobs — see [Recurring Jobs](/docs/recurring-jobs#job-file) for the shared format. This page covers only what's new for event-driven automations.

<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"
      >Haiku 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>

## Two 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)    |

Event triggers can include a `condition` — a natural-language string evaluated by Haiku against the event payload before dispatch. If the condition doesn't match, the automation is silently skipped.

## 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 settings UI

Automations appear in the settings panel. Users can view, enable/disable, and delete them there.

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`, evaluated by Haiku 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"` | `"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                                                                                          |
| `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 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.

## 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: "steve@builder.io" },
);
```

The `owner` in the emit metadata scopes which automations fire — only automations owned by the same user (or shared automations) are evaluated.

## Conditions {#conditions}

Conditions are natural-language strings evaluated by Claude Haiku against the event payload. This is a yes/no classification, not a generation task.

- **Empty or missing condition** = unconditional (always fires).
- Results are memoized (SHA-256 of condition + payload) with a 5-minute TTL and 500-entry LRU cache.
- Payload is truncated to 4000 characters before sending to Haiku.

<Callout tone="info">

On API failure, the condition evaluates to `false` (safe default — the automation is skipped rather than run on an unverified condition).

</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 characters to return (max 200000). Raise it to read a large page or dataset in full.                                                          |
| `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 | `true`        | Whether extracted HTML responses include a compact links list.                                                                                                 |
| `search`       | object  | —             | Optional post-fetch search over the extracted content: `{ query, queries, terms, regex, regexFlags, source, maxMatches, contextChars, caseSensitive }`.        |
| `saveToFile`   | string  | —             | Workspace file path to save the full response to instead of returning it inline — useful for large pages or API responses.                                     |

## 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.
- Each key can have a **URL allowlist** that restricts which origins the key can be sent to (origin-level matching).
- 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}

All automation operations are accessed through a single `manage-automations` tool with an `action` parameter:

| Action        | Purpose                                                              |
| ------------- | -------------------------------------------------------------------- |
| `list-events` | Discover all registered events with descriptions and payload schemas |
| `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)               |
| `fire-test`   | Emit a `test.event.fired` event to validate automations              |

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

## 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/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 validate event-triggered automations" responses={[
  {
    "status": "200",
    "description": "Event emitted; matching automations are dispatched through the normal condition + ownership path."
  }
]}>

Confirm an automation's wiring and condition without waiting for a real provider event. Equivalent to the `manage-automations` action `fire-test`.

</Endpoint>

## How dispatch works {#dispatch}

<Diagram id="doc-block-1ee1cky" title="The dispatch path" summary="From a fired event to a completed agent run, gated by ownership scope and the natural-language condition.">

```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"
      >keep only those owned by the event's owner (or shared)</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"
      >Haiku yes/no on the payload &mdash; false &rarr;
      <code>skipped</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"
      ><code>runAgentLoop</code> with body as prompt, payload as context, 5-min
      timeout</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"
---
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) — schedule-triggered automations reuse the same scheduler
- [**Actions**](/docs/actions) — 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
