# Golem Recurring Task TS

> 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.

- Skill: `golemcloud/golem-recurring-task-ts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add golemcloud/golem-recurring-task-ts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/golemcloud/golem-recurring-task-ts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: golemcloud (https://skillmd.com/u/golemcloud)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/golemcloud/golem-recurring-task-ts

---


# 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 uses the definition's RPC client and calls `.schedule()`.

## Basic Pattern

The agent schedules its own `poll` method to run again after a delay:

```typescript
import { z } from 'zod';
import { defineAgent, method } 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() }),
    },
});

// Self-scheduling helper: enqueue this agent's own `poll` to run after a delay.
function scheduleNext(name: string, delaySecs: bigint): void {
    const nowSecs = BigInt(Math.floor(Date.now() / 1000));
    PollerAgent.client.get({ name }).poll.schedule({ seconds: nowSecs + delaySecs, nanoseconds: 0 });
}

export const PollerAgentImpl = PollerAgent.implement({
    init: ({ id }) => ({ name: id.name }),
    methods: {
        start() {
            // Kick off the loop by enqueueing the first poll.
            scheduleNext(this.name, 0n);
        },
        poll() {
            // 1. Do the recurring work.
            doWork();
            // 2. Schedule the next run (60 seconds from now).
            scheduleNext(this.name, 60n);
        },
    },
});
```

## Exponential Backoff

Increase the delay on repeated failures, reset on success. Keep the counters in the agent's state:

```typescript
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(this.name, 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:

```typescript
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; // stop the loop
            doWork();
            const nowSecs = BigInt(Math.floor(Date.now() / 1000));
            this.pending = PollerAgent.client.get({ name: this.name }).poll.schedule({
                seconds: nowSecs + 60n,
                nanoseconds: 0,
            });
        },
        cancel() {
            this.cancelled = true;
            this.pending?.cancel();
            this.pending = undefined;
        },
    },
});
```

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:

```shell
# 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:

```typescript
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:

```typescript
cleanup() {
    this.entries = this.entries.filter((e) => !e.isExpired());
    scheduleNext(this.name, 3600n); // run hourly
}
```

### Heartbeat / Keep-Alive

Periodically notify an external service that the agent is alive:

```typescript
heartbeat() {
    sendHeartbeat(this.serviceUrl);
    scheduleNext(this.name, 30n); // every 30s
}
```

## Helper for Scheduling Self

Keep the scheduling logic in one module-level helper so every method stays clean. `PollerAgent.client` is the typed RPC factory for this same agent type; addressing it by the agent's own id record targets this instance:

```typescript
function scheduleNext(name: string, delaySecs: bigint): void {
    const nowSecs = BigInt(Math.floor(Date.now() / 1000));
    PollerAgent.client.get({ 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`](../golem-custom-snapshot-ts/SKILL.md).

