Triggers

August 2, 2026 · View on GitHub

Background tasks and external events.

Overview

Triggers are long-running async tasks that run concurrently with the agent loop. They are spawned when the agent starts and can push notifications to the agent via the connection. Common use cases include periodic health checks, heartbeat monitors, file watchers, and external event listeners.

Python SDK Comparison

The trigger concept is consistent across both SDKs. In Python, triggers are registered via Agent(triggers=[...]). In Rust, triggers are registered through the builder's .trigger() or .triggers() methods and implement the Trigger trait.


Trigger Trait

The Trigger trait defines a single run method that receives the active connection and executes for the lifetime of the agent:

use antigravity_sdk_rust::triggers::TriggerContext;

/// A trait for defining asynchronous background tasks that execute
/// during a connection lifecycle.
pub trait Trigger: Send + Sync {
    /// Launches the trigger task.
    ///
    /// Runs for the lifetime of the agent, or until the runner is stopped.
    async fn run(&self, context: TriggerContext) -> Result<(), anyhow::Error>;
}

TriggerContext has exactly one method — send(message) — which pushes a message into the agent's event stream. Triggers used to receive the whole AnyConnection, which let a background task disconnect the agent, answer a tool confirmation, or halt a turn the user had just started. Nudging the agent is a trigger's job, so nudging is all the context exposes.


DynTrigger (Object-Safe Wrapper)

Like the Hook/DynHook pattern, triggers have an object-safe counterpart that wraps the async run method in a BoxFuture:

use futures_util::future::BoxFuture;
use antigravity_sdk_rust::triggers::TriggerContext;

/// Object-safe version of `Trigger`, automatically implemented
/// via a blanket impl for any `T: Trigger`.
pub trait DynTrigger: Send + Sync {
    fn run(&self, context: TriggerContext) -> BoxFuture<'_, Result<(), anyhow::Error>>;
}

// Blanket impl: any type implementing Trigger automatically implements DynTrigger.
// impl<T: Trigger + ?Sized> DynTrigger for T { ... }

You always implement Trigger — the SDK handles the DynTrigger conversion automatically.


TriggerRunner

The TriggerRunner orchestrates the spawning and lifecycle of all registered triggers:

use antigravity_sdk_rust::triggers::TriggerRunner;
use std::sync::Arc;

// TriggerRunner stores triggers as Vec<Arc<dyn DynTrigger>>

API

MethodDescription
TriggerRunner::new(triggers)Creates a runner wrapping a Vec<Arc<dyn DynTrigger>>
runner.start(connection)Spawns each trigger as an independent task. Errors if already running
runner.stop()Signals every trigger to stop. Idempotent
runner.is_running()Whether the trigger tasks are live

When start() is called, each trigger is cloned (via Arc) and spawned into its own task. If a trigger's run method returns an error, it is logged via tracing::error! but does not crash the agent.

Starting twice is refused rather than silently doubling every trigger — a heartbeat firing at twice its configured rate is hard to diagnose from the outside. Agent::stop() calls stop() before disconnecting, so triggers no longer outlive the agent, parked in a sleep and holding a connection.

Note

Triggers run independently — one trigger failing does not affect others. Errors are logged but silently swallowed to maintain agent stability.


Trigger Helpers

every() — Periodic Timer

The every() factory function creates a trigger that fires at regular intervals, sending a message to the agent each time:

every() runs a callback on each tick, so a trigger can decide whether it has anything to say. every_notification() is the fixed-message case. Both reject a zero interval, which would spin the task at full speed.

use antigravity_sdk_rust::trigger_helpers::{every, every_notification};
use std::time::Duration;

// Decide per tick
let monitor = every(Duration::from_secs(30), |ctx| async move {
    if queue_depth() > 100 {
        ctx.send("the queue is backing up").await?;
    }
    Ok(())
})?;

// Or just send the same thing every time
let heartbeat = every_notification(Duration::from_secs(30), "check_status")?;
# fn queue_depth() -> usize { 0 }
# Ok::<(), anyhow::Error>(())

The returned PeriodicTrigger loops indefinitely:

loop {
    sleep(interval)
    connection.send_trigger_notification(message)
}

PeriodicTrigger Internals

use std::time::Duration;

/// A trigger that fires at regular intervals.
/// Created via `every()`.
pub struct PeriodicTrigger {
    /// How long to wait between notifications.
    interval: Duration,
    /// The message sent to the agent on each tick.
    message: String,
}

Types

TriggerDelivery

Controls when trigger notifications are delivered to the agent:

/// Controls when trigger notifications are delivered to the agent.
#[derive(Debug, Clone, Copy)]
pub enum TriggerDelivery {
    /// Deliver the notification immediately, even if the agent is busy.
    SendImmediately,
    /// Wait until the agent is idle before delivering.
    WaitIdle,
}

FileChange

Represents a single filesystem change event, useful for file-watching triggers:

/// The kind of filesystem change detected by a file-watching trigger.
#[derive(Debug, Clone, Copy)]
pub enum FileChangeKind {
    /// A new file was created.
    Added,
    /// An existing file was modified.
    Modified,
    /// A file was deleted.
    Deleted,
}

/// A single filesystem change event.
#[derive(Debug, Clone)]
pub struct FileChange {
    /// The type of change.
    pub kind: FileChangeKind,
    /// The path of the affected file.
    pub path: String,
}

Examples

1. Heartbeat Trigger

The simplest trigger — periodic status checks using the built-in every() helper:

use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::trigger_helpers::every;
use antigravity_sdk_rust::triggers::DynTrigger;
use std::sync::Arc;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let heartbeat = every(Duration::from_secs(60), "heartbeat_check");

    let agent = Agent::builder()
        .trigger(Arc::new(heartbeat) as Arc<dyn DynTrigger>)
        .allow_all()
        .build();

    let agent = agent.start().await?;
    // The heartbeat trigger is now running in the background,
    // sending "heartbeat_check" every 60 seconds.

    let response = agent.chat("Monitor the system").await?;
    println!("{}", response.text);

    agent.stop().await?;
    Ok(())
}

2. Custom Monitoring Trigger

A trigger that monitors a resource and sends notifications when conditions change:

use antigravity_sdk_rust::connection::AnyConnection;
use antigravity_sdk_rust::connection::Connection;
use antigravity_sdk_rust::triggers::Trigger;
use std::time::Duration;

struct DiskSpaceMonitor {
    path: String,
    threshold_mb: u64,
    check_interval: Duration,
}

impl DiskSpaceMonitor {
    fn new(path: impl Into<String>, threshold_mb: u64) -> Self {
        Self {
            path: path.into(),
            threshold_mb,
            check_interval: Duration::from_secs(300), // every 5 minutes
        }
    }
}

impl Trigger for DiskSpaceMonitor {
    async fn run(&self, connection: AnyConnection) -> Result<(), anyhow::Error> {
        loop {
            tokio::time::sleep(self.check_interval).await;

            // Simulate checking disk space
            // In production, use a filesystem API
            let available_mb = 500u64; // placeholder

            if available_mb < self.threshold_mb {
                let msg = format!(
                    "⚠️ Low disk space on {}: {}MB remaining (threshold: {}MB)",
                    self.path, available_mb, self.threshold_mb
                );
                connection.send_trigger_notification(&msg).await?;
            }
        }
    }
}

3. Builder Integration

Registering multiple triggers with the agent builder:

use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::trigger_helpers::every;
use antigravity_sdk_rust::triggers::DynTrigger;
use std::sync::Arc;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let agent = Agent::builder()
        // Add individual triggers
        .trigger(Arc::new(every(Duration::from_secs(30), "health_check")) as Arc<dyn DynTrigger>)
        .trigger(Arc::new(every(Duration::from_secs(300), "metrics_report")) as Arc<dyn DynTrigger>)
        .allow_all()
        .build();

    let agent = agent.start().await?;
    // Both triggers are now running concurrently in the background.

    agent.stop().await?;
    Ok(())
}

Or register after construction but before starting:

use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::trigger_helpers::every;
use antigravity_sdk_rust::triggers::DynTrigger;
use std::sync::Arc;
use std::time::Duration;

let mut agent = Agent::builder().allow_all().build();
agent.register_trigger(
    Arc::new(every(Duration::from_secs(60), "late_trigger")) as Arc<dyn DynTrigger>
).expect("trigger registration failed");
// agent.start().await?;

4. Batch Triggers via .triggers()

You can set all triggers at once using the plural form:

use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::trigger_helpers::every;
use antigravity_sdk_rust::triggers::DynTrigger;
use std::sync::Arc;
use std::time::Duration;

let all_triggers: Vec<Arc<dyn DynTrigger>> = vec![
    Arc::new(every(Duration::from_secs(30), "check_a")),
    Arc::new(every(Duration::from_secs(60), "check_b")),
    Arc::new(every(Duration::from_secs(120), "check_c")),
];

let agent = Agent::builder()
    .triggers(all_triggers)
    .allow_all()
    .build();