Recurring & Scheduled Tasks in Go
Overview
A recurring task is a method that reschedules itself: it does its work, then schedules the next run. Scheduling uses Method.Schedule(client, at, in), which arranges an invocation at a future time and returns a cancelable *golem.ScheduledInvocation (and an error if the host refuses it; MustSchedule panics instead).
To message itself an agent needs a client for its own instance. Agent.Bind(ctx.AgentID()) returns one: ctx.AgentID() is the running instance’s id, read from the host on each call, so it is also right after a snapshot restore or a fork.
Steps
- Capture the agent’s own
IDin state in theImplementconstructor. - Add a tick method that does the work and then
Schedules itself for the next run. - Start the loop — a
Startmethod (or an external trigger) schedules the first tick.
Example
Definition (agents/poller/poller.go):
package poller
import "github.com/golemcloud/golem/sdks/go/golem"
type ID struct{ Name string }
var Agent = golem.DefineAgent[ID](golem.Spec{Name: "PollerAgent", Description: "Periodic poller"})
var (
Start = Agent.Method[golem.Unit, golem.Unit]("start", golem.Description("Begin the recurring loop"))
Tick = Agent.Method[golem.Unit, golem.Unit]("tick", golem.Description("One iteration; reschedules itself"))
)Implementation (agents/poller/impl/impl.go):
package impl
import (
"time"
"myapp/agents/poller"
"github.com/golemcloud/golem/sdks/go/golem"
)
const interval = time.Minute
type state struct{}
var agent = poller.Agent.Implement(func(poller.ID) *state { return &state{} })
func init() {
agent.Handle(poller.Start, func(ctx *golem.Context[state], _ golem.Unit) golem.Unit {
self := golem.Must(poller.Agent.Bind(ctx.AgentID()))
poller.Tick.MustSchedule(self, time.Now().Add(interval), golem.Unit{})
return golem.Unit{}
})
agent.Handle(poller.Tick, func(ctx *golem.Context[state], _ golem.Unit) golem.Unit {
// ... do the periodic work ...
// schedule the next iteration
self := golem.Must(poller.Agent.Bind(ctx.AgentID()))
poller.Tick.MustSchedule(self, time.Now().Add(interval), golem.Unit{})
return golem.Unit{}
})
}Kick off the loop by invoking start once (from the CLI, an HTTP endpoint, or another agent).
Scheduling vs immediate self-messaging
Schedule(client, at, in) (*ScheduledInvocation, error)— run at a futuretime.Time. Keep the handle to cancel it:inv.Cancel().MustSchedulepanics instead of returning the error.Trigger(client, in)/MustTrigger(client, in)— enqueue immediately, no delay (use it to advance a state machine to the next step now).- Never self-
Call— a synchronous call into the same instance deadlocks (one instance processes one invocation at a time). Self-messaging must useTriggerorSchedule.
Key Constraints
- Build the self client from
ctx.AgentID(), not from anIDkept in state: a snapshot restore does not run the constructor, so state set only there is empty after a restore. - The reschedule must live in the tick handler itself (a package-level descriptor referencing its own handler in an
init()is fine; there is no var-initialization cycle because the handler is registered ininit, not in avarinitializer). - Compute the next time with
time.Now().Add(interval); the scheduled time is recorded durably.
Related Guides
| Guide | Description |
|---|---|
golem-fire-and-forget-go | Enqueue an immediate invocation (Trigger) |
golem-call-another-agent-go | Await a result from another agent |
golem-add-agent-go | Define the agent |