| name | golem-recurring-task-ts |
| description | Implementing a recurring (cron-like) task in a TypeScript 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. |
Recurring Tasks via Self-Scheduling (TypeScript)
Overview
A Golem agent can act as its own scheduler by scheduling one of its own methods to run again 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.
Because a method handler's this is bound to the agent's state (not to its other methods), factor the self-scheduling logic into a small module-level helper that builds an RPC client for the agent itself with clientFor and calls .schedule().
Basic Pattern
The agent schedules its own poll method to run again after a delay:
import { z } from 'zod';
import { defineAgent, method, clientFor } from '@golemcloud/golem-ts-sdk';
export const PollerAgent = defineAgent({
name: 'PollerAgent',
id: { name: z.string() },
methods: {
start: method({ input: {}, returns: z.void() }),
poll: method({ input: {}, returns: z.void() }),
},
});
function scheduleNext(name: string, delaySecs: bigint): void {
const nowSecs = BigInt(Math.floor(Date.now() / 1000));
clientFor(PollerAgent)({ name }).poll.schedule({ seconds: nowSecs + delaySecs, nanoseconds: 0 });
}
export const PollerAgentImpl = PollerAgent.implement({
init: ({ id }) => ({ name: id.name }),
methods: {
start() {
scheduleNext(this.name, 0n);
},
poll() {
doWork();
scheduleNext(this.name, 60n);
},
},
});
Exponential Backoff
Increase the delay on repeated failures, reset on success. Keep the counters in the agent's state:
export const PollerAgentImpl = PollerAgent.implement({
init: ({ id }) => ({
name: id.name,
consecutiveFailures: 0,
baseIntervalSecs: 60n,
maxIntervalSecs: 3600n,
}),
methods: {
poll() {
const success = tryWork();
let delay: bigint;
if (success) {
this.consecutiveFailures = 0;
delay = this.baseIntervalSecs;
} else {
this.consecutiveFailures++;
const exp = Math.min(this.consecutiveFailures, 6);
delay = this.baseIntervalSecs * BigInt(2 ** exp);
if (delay > this.maxIntervalSecs) delay = this.maxIntervalSecs;
}
scheduleNext(., delay);
},
},
});
Cancellation
.schedule() returns a CancellationToken. Keep it when the pending invocation
must be canceled immediately, and call .cancel() before its scheduled time.
For a recurring loop, also keep a boolean flag in state so a poll that has
already started exits without rescheduling:
import type { CancellationToken } from '@golemcloud/golem-ts-sdk';
export const PollerAgentImpl = PollerAgent.implement({
init: ({ id }) => ({
name: id.name,
cancelled: false,
pending: undefined as CancellationToken | undefined,
}),
methods: {
poll() {
if (this.cancelled) return;
doWork();
const nowSecs = BigInt(Math.floor(Date.now() / 1000));
this.pending = clientFor(PollerAgent)({ name: this.name }).poll.schedule({
seconds: nowSecs + 60n,
nanoseconds: 0,
});
},
() {
. = ;
.?.();
. = ;
},
},
});
Do not include pending in a typed snapshot schema: it is a live host resource,
not ordinary serializable state. The boolean flag remains the durable source of
truth across snapshots and restarts.
Cancellation from the CLI
If you scheduled the invocation from the CLI with an explicit idempotency key, cancel the pending invocation by key:
# 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:
poll() {
const items = fetchPendingItems();
for (const item of items) {
process(item);
}
scheduleNext(this.name, 60n);
}
Periodic Cleanup
Remove expired data or stale resources on a schedule:
cleanup() {
this.entries = this.entries.filter((e) => !e.isExpired());
scheduleNext(this.name, 3600n);
}
Heartbeat / Keep-Alive
Periodically notify an external service that the agent is alive:
heartbeat() {
sendHeartbeat(this.serviceUrl);
scheduleNext(this.name, 30n);
}
Helper for Scheduling Self
Keep the scheduling logic in one module-level helper so every method stays clean. clientFor(PollerAgent) builds a typed RPC client for this same agent type; addressing it by the agent's own id record targets this instance:
function scheduleNext(name: string, delaySecs: bigint): void {
const nowSecs = BigInt(Math.floor(Date.now() / 1000));
clientFor(PollerAgent)({ name }).poll.schedule({ seconds: nowSecs + delaySecs, 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
poll on 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. Set snapshotting on defineAgent — snapshotting: { everyNInvocations: N } or snapshotting: { periodicSeconds: N } — and, for state the default JSON path can't represent, supply custom snapshot: { save, load } on .implement(...). See golem-custom-snapshot-ts.