Recurring Tasks via Self-Scheduling (Rust)
SkillProductivityImplementing a recurring (cron-like) task in a Rust Golem agent by self-scheduling future invocations. Use when the user asks about periodic tasks, recurring jobs, cron-like scheduling, polling loops, heartbeats, or self-scheduling agents.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Recurring Tasks via Self-Scheduling (Rust) skill
What this skill tells your AI
The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/rust/golem-recurring-task-rust/SKILL.md and read by ahel’s review.
Overview
A Golem agent can act as its own scheduler by calling schedule_ on itself at the end of each invocation. This creates a durable, crash-resilient recurring task — if the agent restarts, the scheduled invocation is still pending and will fire at the designated time.
Basic Pattern
The agent schedules its own method to run again after a delay:
use golem_rust::wasip2::clocks::wall_clock::Datetime;
#[agent_definition]
pub trait PollerAgent: HasSchema {
fn new(name: String) -> Self;
fn start(&mut self);
fn poll(&mut self);
}
impl PollerAgent for PollerAgentImpl {
fn new(name: String) -> Self {
Self { name }
}
fn start(&mut self) {
// Kick off the first poll
self.poll();
}
fn poll(&mut self) {
// 1. Do the recurring work
do_work();
// 2. Schedule the next run (60 seconds from now)
let mut client = PollerAgentClient::get(self.name.clone());
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
client.schedule_poll(Datetime {
seconds: now_secs + 60,
nanoseconds: 0,
});
}
}
Exponential Backoff
Increase the delay on repeated failures, reset on success:
fn poll(&mut self) {
let success = try_work();
let delay = if success {
self.consecutive_failures = 0;
self.base_interval_secs // e.g. 60
} else {
self.consecutive_failures += 1;
let backoff = self.base_interval_secs * 2u64.pow(self.consecutive_failures.min(6));
backoff.min(self.max_interval_secs) // cap at e.g. 3600
};
let mut client = PollerAgentClient::get(self.name.clone());
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
client.schedule_poll(Datetime {
seconds: now_secs + delay,
nanoseconds: 0,
});
}
Cancellation with CancellationToken
The Rust SDK generates schedule_cancelable_{method} variants that return a CancellationToken. Store the token and cancel it to stop the next scheduled invocation:
fn poll(&mut self) {
if self.cancelled {
return; // stop the loop
}
do_work();
// Schedule next run and store the cancellation token
let mut client = PollerAgentClient::get(self.name.clone());
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
self.pending_token = Some(client.schedule_cancelable_poll(Datetime {
seconds: now_secs + 60,
nanoseconds: 0,
}));
}
fn cancel(&mut self) {
self.cancelled = true;
// Cancel the pending scheduled invocation so it never fires
if let Some(token) = self.pending_token.take() {
token.cancel();
}
}
Cancellation via State Flag
For simpler cases, just use a boolean flag — the next scheduled poll checks it and exits early:
fn poll(&mut self) {
if self.cancelled {
return;
}
do_work();
self.schedule_next(60);
}
fn cancel(&mut self) {
self.cancelled = true;
}
Cancellation from the CLI
Schedule with an explicit idempotency key and cancel the pending invocation:
# Schedule with a known idempotency key
golem agent invoke --trigger --schedule-at 2026-03-15T10:30:00Z -i 'poll-next' 'PollerAgent("my-poller")' poll
# Cancel the pending invocation
golem agent invocation cancel 'PollerAgent("my-poller")' 'poll-next'
Common Use Cases
Periodic Polling
Check an external API or queue for new work at regular intervals:
fn poll(&mut self) {
let items = fetch_pending_items();
for item in items {
process(item);
}
self.schedule_next(60); // poll again in 60s
}
Periodic Cleanup
Remove expired data or stale resources on a schedule:
fn cleanup(&mut self) {
self.entries.retain(|e| !e.is_expired());
self.schedule_next(3600); // run hourly
}
Heartbeat / Keep-Alive
Periodically notify an external service that the agent is alive:
fn heartbeat(&mut self) {
send_heartbeat(&self.service_url);
self.schedule_next(30); // every 30s
}
Helper for Scheduling Self
Extract the scheduling logic into a helper to keep methods clean:
impl PollerAgentImpl {
fn schedule_next(&self, delay_secs: u64) {
let mut client = PollerAgentClient::get(self.name.clone());
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
client.schedule_poll(Datetime {
seconds: now_secs + delay_secs,
nanoseconds: 0,
});
}
}
Key Points
- The agent is durable — if it crashes, the pending scheduled invocation still fires and the agent recovers
- Invocations are sequential — no concurrent executions of
pollon the same agent - Each
schedule_call is a fire-and-forget enqueue; the current invocation completes immediately - Use a state flag or generation counter to stop the loop gracefully
- Keep the scheduled method idempotent — it may be retried on recovery
Recovery & Oplog Growth
Each scheduled tick (heartbeat, poll, cleanup) appends entries to the agent's oplog. For long-running or high-frequency recurring tasks, the oplog grows unboundedly, and recovery on crash will replay the full history — which becomes slow over time.
You cannot opt out of oplog writes for a durable agent. The fix is snapshot-based recovery: enable periodic snapshotting so recovery starts from the latest snapshot instead of replaying every prior tick. See golem-custom-snapshot-rust for the snapshotting = "every(N)" / snapshotting = "periodic(...)" attribute and serde-based or custom save/load implementations.
Signals
- GitHub stars
- 2k
- Forks
- 212
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
golem-recurring-task-rust- Source
- github.com/golemcloud/golem