How to Implement Automations in Agent-Native Based on Event Triggers

Agent-Native provides a built-in automation engine that processes event-driven workflows through a three-layer architecture—trigger, processing, and action—using LLM evaluation to match Gmail messages against user-defined rules and execute side-effects like labeling or archiving.

The BuilderIO/agent-native repository ships with a complete automation framework that monitors Gmail inboxes and reacts to new messages in real time. This guide explains how to implement automations in Agent-Native based on event triggers, covering the debounced trigger logic, LLM-powered rule evaluation, and concrete action execution against the Gmail API.

Automation Architecture Overview

The engine splits responsibilities across three distinct layers defined in templates/mail/server/lib/automation-engine.ts:

  • Trigger layer – Decides when processing starts via cron, manual requests, or custom events. Entry points include triggerAutomationsDebounced and processAutomations.
  • Processing layer – Loads active rules, fetches new data, evaluates conditions with an LLM, and orchestrates actions via processAutomationsForAccount, fetchNewInboxMessages, evaluateRules, and executeActions.
  • Action layer – Performs concrete side-effects (label, archive, star, trash) against Gmail through templates/mail/server/lib/automation-actions.ts.

Triggering Automations

Agent-Native supports three trigger mechanisms, all converging on the processAutomations() function in automation-engine.ts.

Cron-Based Polling

A server-side cron invokes processAutomations() once per minute. The scripts/qa-dispatch-automations-smoke.ts file contains the test harness that validates this behavior.

Manual Trigger

The trigger-automations action (templates/mail/actions/trigger-automations.ts) exposes an imperative API:

import { defineAction } from "@agent-native/core";
import { getRequestUserEmail } from "@agent-native/core/server";

export default defineAction({
  description: "Trigger automation processing now",
  schema: z.object({}),
  http: false,
  run: async () => {
    const ownerEmail = getRequestUserEmail();
    const { triggerAutomationsDebounced } = await import(
      "../server/lib/automation-engine.js"
    );
    const result = await triggerAutomationsDebounced(ownerEmail);
    return result.triggered
      ? "Automation processing triggered."
      : `Skipped: ${result.reason}`;
  },
});

Debounce Logic

To prevent overlapping runs, a per-owner in-memory map (_lastTriggerTimeByOwner) guarantees the engine fires no more than once every 30 seconds.

Loading and Storing Automation Rules

Rules persist in the automationRules table. The server loads active configurations via loadActiveRules in automation-engine.ts:

async function loadActiveRules(
  ownerEmail: string,
  domain: string,
): Promise<RuleRecord[]> {
  return db
    .select()
    .from(schema.automationRules)
    .where(and(
      eq(schema.automationRules.ownerEmail, ownerEmail),
      eq(schema.automationRules.domain, domain),
      eq(schema.automationRules.enabled, 1),
    ));
}

Each RuleRecord contains:

  • condition – A textual description evaluated by the LLM.
  • actions – A JSON-encoded array of AutomationAction objects defining side-effects.

Fetching Event Data

For Gmail integrations, the engine watches inbox events using two persistence mechanisms:

  1. Watermark – Stored per user (automation-watermark) tracking the last processed Gmail historyId.
  2. Processed IDs – A cache (automation-processed-ids) prevents duplicate message handling.

The fetchNewInboxMessages function attempts the Gmail history endpoint first, falling back to listMessages when the history token expires. It returns message metadata required for rule evaluation.

Evaluating Rules with LLM

The evaluateRules function constructs a prompt containing active rules and a batch of up to 10 email summaries, then invokes the configured LLM (Anthropic by default) via callModel:

const prompt = `You are an email classification engine...
Rules:
${rulesText}

Emails:
${emailsText}
...`;
const text = await callModel(prompt, ownerEmail, modelSettings);
const parsed = JSON.parse(cleanedResponse) as Array<{ emailId: string; matches: RuleMatch[] }>;

The LLM returns a JSON mapping of email IDs to matched rule IDs, which the engine converts to a Map<string, string[]> for execution.

Executing Actions

For every match, executeActions iterates over rule.actions (typed as AutomationAction[]) and dispatches them through executeAction in automation-actions.ts:

switch (action.type) {
  case "label":
    const labelId = await resolveLabelId(action.labelName, ctx);
    await gmailModifyMessage(ctx.accessToken, ctx.messageId, [labelId]);
    break;
  case "archive":
    await gmailModifyMessage(ctx.accessToken, ctx.messageId, undefined, ["INBOX"]);
    break;
  // … star, trash, etc.
}

Concrete Gmail API calls (gmailModifyMessage, gmailTrashMessage) reside in google-api.ts, while label resolution logic lives in automation-actions.ts.

Managing Automations via the Dispatch API

Client-side code interacts with automations through the Dispatch API defined in packages/dispatch/src/lib/automations.ts:

  • ListlistDispatchAutomations() performs a GET request to /_agent-native/automations.
  • TogglesetDispatchAutomationEnabled(input) sends a PATCH request to the same endpoint.
export async function listDispatchAutomations(): Promise<DispatchAutomationItem[]> {
  const response = await fetch(agentNativePath("/_agent-native/automations"));
  return response.ok ? await response.json() : [];
}

The UI consumes these helpers through templates/mail/app/hooks/use-automations.ts.

Practical Implementation Examples

Manually Trigger Automations from a React Component

import { useActionMutation } from "@agent-native/core";
import { TriggerAutomations } from "@/actions/trigger-automations";

export function RunNowButton() {
  const { mutateAsync: runNow, isLoading } = useActionMutation(TriggerAutomations);

  return (
    <button
      onClick={() => runNow()}
      disabled={isLoading}
      className="btn-primary"
    >
      {isLoading ? "Running…" : "Run Automations Now"}
    </button>
  );
}

List All Automations in a Custom Dashboard

import { listDispatchAutomations } from "@agent-native/dispatch";

export async function showAutomations() {
  const automations = await listDispatchAutomations();
  console.table(automations.map(a => ({
    ID: a.id,
    Name: a.name,
    Trigger: a.triggerType,
    Enabled: a.enabled,
  })));
}

Create a New Rule Server-Side

import { db, schema } from "../db/index.js";
import { eq } from "drizzle-orm";

export async function createAutomationRule(
  owner: string, 
  domain: string, 
  name: string, 
  condition: string, 
  actions: AutomationAction[]
) {
  await db.insert(schema.automationRules).values({
    ownerEmail: owner,
    domain,
    name,
    condition,
    actions: JSON.stringify(actions),
    enabled: 1,
    createdAt: Date.now(),
    updatedAt: Date.now(),
  });
}

Summary

  • Agent-Native automations rely on a three-layer architecture (trigger, processing, action) implemented primarily in templates/mail/server/lib/automation-engine.ts.
  • Event triggers include a one-minute cron, manual invocation via trigger-automations.ts, and debounce logic that enforces a 30-second minimum between runs.
  • Rule evaluation uses an LLM to compare Gmail message summaries against user-defined conditions, returning structured JSON that maps messages to rules.
  • Action execution handles Gmail side-effects (label, archive, star, trash) through automation-actions.ts with deduplication via watermarks and processed-ID caches.
  • Client management occurs through the Dispatch API (packages/dispatch/src/lib/automations.ts), enabling UI components to list and toggle automations.

Frequently Asked Questions

How does the debounce mechanism prevent duplicate automation runs?

The triggerAutomationsDebounced function in automation-engine.ts maintains an in-memory map called _lastTriggerTimeByOwner that tracks the last invocation timestamp per user. If a trigger request arrives within 30 seconds of the previous run, the function returns { triggered: false, reason: "Debounce active" }, ensuring the engine never processes overlapping batches for the same account.

What LLM does Agent-Native use for rule evaluation?

By default, the evaluateRules function calls Anthropic’s Claude model via the callModel utility. The prompt includes the full text of active rules and a batch of up to 10 email summaries, requesting a JSON response that maps each emailId to an array of matching rule IDs.

How does the engine avoid processing the same Gmail message twice?

Agent-Native implements a two-tier deduplication strategy. First, it stores a watermark (automation-watermark) containing the last processed Gmail historyId or timestamp. Second, it maintains a cache of processed IDs (automation-processed-ids) that prevents re-evaluation of specific message IDs even if the watermark resets or the history token expires.

Can I trigger automations manually instead of waiting for the cron?

Yes. Import and call the trigger-automations action from templates/mail/actions/trigger-automations.ts with useActionMutation in the frontend, or invoke triggerAutomationsDebounced(ownerEmail) directly in server code. This bypasses the one-minute cron interval, though the 30-second debounce still applies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →