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
| Method | Description |
|---|---|
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();