How Agent-Native Handles Automations with Schedule‑Triggered Conditions

Agent‑Native implements schedule‑triggered automations through a built‑in database scheduler that polls the scheduled_jobs table every minute, executing rules defined in automation_rules via the automation engine in templates/mail/server/lib/automation‑engine.ts.

The BuilderIO/agent‑native repository provides a complete automation subsystem within its Mail template, enabling users to define time‑based rules that execute without manual intervention. This architecture leverages Drizzle ORM for persistence, a lightweight job runner for scheduling, and a robust action engine for performing operations against external APIs like Gmail. Below is a technical walkthrough of how schedule‑triggered conditions are modeled, scheduled, and executed.

Database Schema for Scheduled Automations

The persistence layer resides in templates/mail/server/db/schema.ts and defines three critical tables that power the scheduler.

automation_rules stores the rule definition, including a JSON schedule column that encodes the trigger condition (e.g., { type: "daily", time: "09:00" }). Each row tracks the ownerEmail, enabled flag, filter criteria, and the array of actions to perform.

scheduled_jobs acts as the timer queue. When a rule includes a schedule, the system inserts a row with type = "automation", a computed run_at timestamp, and status "pending". The table supports generic job scheduling (also used for email snoozing) but distinguishes automation triggers via the type field.

automation_settings (accessed within automation‑engine.ts) stores per‑account watermarks and configuration flags, such as the automation‑watermark key that tracks the last processed Gmail message ID for incremental sync.

Performance is ensured through targeted indexes: idx_automation_rules_owner accelerates user‑specific rule lookups, while idx_scheduled_jobs_status_run_at optimizes the polling query that fetches due jobs.

Job Runner and Scheduling Mechanism

The scheduler implementation lives in templates/mail/server/plugins/mail‑jobs.ts. It registers a setInterval loop (approximately one‑minute resolution) that queries scheduled_jobs for rows with status "pending" or "processing" where run_at <= now().

For each due job, the runner:

  1. Loads the associated automation rule(s) from automation_rules.
  2. Invokes processAutomations from templates/mail/server/lib/automation‑engine.ts.
  3. Updates the job status from "pending""processing""done".

The scheduling logic itself, including timestamp calculation for recurring runs, is handled by utilities in templates/mail/server/lib/jobs.ts (see scheduleEmailSend and scheduleSnooze for reference implementations of run_at computation). Because this runs within the Node.js process, no external cron service is required.

Automation Engine and Message Processing

templates/mail/server/lib/automation‑engine.ts contains the core processAutomations function, which orchestrates data fetching and rule evaluation.

The engine first refreshes OAuth tokens for the target account (line 116 logs Token refresh failed for ${accountEmail} upon expiry). It then retrieves inbox messages using a watermark‑based approach: it consults automation_settings to find the last processed message ID, fetches newer messages via the Gmail API, and falls back to a full message list if the History API is unavailable (lines 233‑258).

For each candidate message, the engine evaluates the rule’s filter predicate. Matches trigger runAutomationAction (imported from templates/mail/server/lib/automation‑actions.ts), which executes concrete side effects such as:

  • addLabel – applies a Gmail label ID to the message.
  • starMessage – stars the conversation.
  • forward – sends the message to a specified recipient (requires extending the action union type).

Error handling is best‑effort: individual rule failures are caught and logged (Rule evaluation failed at line 540; Failed for ${account.accountId} at lines 701‑743) without aborting the batch, ensuring one misconfigured rule does not block account processing.

Public API and Manual Triggering

The system exposes REST endpoints under /api/automations defined in templates/mail/server/routes/api/automations/:

  • GET /api/automations (index.get.ts) – lists all rules for the authenticated user.
  • POST /api/automations (index.post.ts) – creates a new rule and inserts the initial scheduled_jobs entry.
  • PATCH /api/automations/:id ([id].patch.ts) – updates rule criteria or schedule.
  • DELETE /api/automations/:id ([id].delete.ts) – removes the rule and cancels pending jobs.
  • POST /api/automations/trigger (trigger.post.ts) – immediately executes all enabled automations, bypassing the schedule.

To manually trigger automations (useful for testing or "run now" UI buttons):

curl -X POST https://your-app.com/api/automations/trigger \
     -H "Authorization: Bearer $AGENT_NATIVE_TOKEN"

The handler lazily imports triggerAutomations from the handlers directory, which then invokes the engine for the requesting user’s account.

Frontend Integration and React Hooks

The UI layer consumes these endpoints through the use‑automations hook located in templates/mail/app/hooks/use‑automations.ts. This React Query wrapper provides cached data fetching and automatic background updates.

When mutations occur (create, update, delete), the hook invalidates the ["automations"] query key, causing templates/mail/app/pages/SettingsPage.tsx to re‑fetch the rule list without a page reload.

Example: Creating a Daily Automation

import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreateScheduledAutomation() {
  const qc = useQueryClient();

  const createAutomation = useMutation({
    mutationFn: async (payload) => {
      const res = await fetch('/api/automations', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      return res.json();
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['automations'] }),
  });

  const handleSubmit = () => {
    createAutomation.mutate({
      name: 'Morning Newsletter Sort',
      filter: { from: '*@newsletter.com' },
      actions: [{ type: 'addLabel', labelId: 'Label_123' }],
      schedule: { type: 'daily', time: '09:00' },
      enabled: true,
    });
  };

  return <button onClick={handleSubmit}>Create Daily Rule</button>;
}

The schedule object is serialized to JSON and stored in automation_rules.schedule; the backend creates the corresponding scheduled_jobs row with the calculated next run time.

Summary

  • Schedule‑triggered automations in Agent‑Native rely on the scheduled_jobs table polled by mail‑jobs.ts to drive execution.
  • Rule definitions reside in automation_rules, including JSON schedules and filter criteria, indexed by owner for fast retrieval.
  • The automation engine in automation‑engine.ts handles token refresh, watermark‑based incremental sync, and best‑effort error isolation.
  • Actions are implemented in automation‑actions.ts, with built‑in support for labeling, starring, and extensible hooks for custom operations.
  • Manual execution is available via POST /api/automations/trigger, while the React frontend uses use‑automations for real‑time synchronization.

Frequently Asked Questions

How does Agent‑Native handle authentication failures during long‑running automation jobs?

The automation engine in templates/mail/server/lib/automation‑engine.ts proactively refreshes OAuth tokens before processing each batch (logging Token refresh failed for ${accountEmail} on line 116 if refresh fails). This ensures that scheduled jobs running hours or days after the initial user login still maintain valid Gmail API credentials without manual re‑authorization.

What happens when a scheduled automation rule fails during execution?

The engine employs best‑effort error isolation: each rule executes inside a try/catch block (see lines 540 and 701‑743 in automation‑engine.ts). If a rule throws—due to invalid filters, API errors, or malformed actions—the error is logged to the console (Rule evaluation failed or Failed for ${account.accountId}), the offending rule is skipped, and processing continues for remaining rules. The job status may update to "done" even if individual actions failed, as failures are logged rather than halting the entire batch.

Is an external cron service required to run schedule‑triggered automations?

No. Agent‑Native uses an in‑process scheduler via setInterval in templates/mail/server/plugins/mail‑jobs.ts, polling the scheduled_jobs table approximately every minute. This design eliminates infrastructure dependencies like cron or distributed job queues, making the Mail template self‑contained and deployable to standard Node.js hosting without additional configuration.

How can I add custom action types to the automation engine?

Extend the Action union type in templates/mail/shared/types.ts to include your new action identifier (e.g., "forward"). Then modify runAutomationAction in templates/mail/server/lib/automation‑actions.ts to handle the new case, implementing the specific API calls or business logic required. The automation engine will automatically pick up the new action type when processing rules, as it iterates over the actions array dynamically.

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 →