Postera
2026 — A Go library that lets an AI agent wake its future self
01 Problem
An agent only does something when someone talks to it. Ask it to follow up tomorrow morning and it says it will, then nothing happens, because nothing runs it tomorrow morning.
Postera gives it a way back. The agent writes a posterum, a message addressed to itself for a moment in the future, and when that moment arrives the message is delivered back and the agent wakes up and acts on it.
02 Three calls
Postarius is what the agent talks to. It needs a Store to remember the posterum and a Queue to make the call when the time comes, and both are interfaces with ready-made adapters.
From there the agent has three calls, Create, ListUpcoming and Cancel. The message is written for a future self that remembers nothing of this conversation, so the who, the what and the why all go in.
postarius, err := postera.New(store, queue,
postera.WithTimezoneFromContext(timezoneKey{}),
postera.WithHumanFromContext(humanKey{}),
postera.WithAgentFromContext(agentKey{}),
postera.WithSessionFromContext(sessionKey{}),
)
p, err := postarius.Create(ctx, postera.CreateArgs{
Message: "Follow up on whether they submitted the Q3 report. They were waiting on their manager's approval.",
TriggerAt: "2026-11-06T09:00:00",
})
upcoming, err := postarius.ListUpcoming(ctx)
err = postarius.Cancel(ctx, p.ID)03 Timezones
TriggerAt is a local wall-clock time with no offset. Agents think in 9am tomorrow and models are bad at offset arithmetic, so Postera never asks them to do it.
The zone comes from context as an IANA name like Asia/Jakarta, or from WithDefaultTimezone when the whole deployment lives in one zone. With neither, Create fails, because scheduling into an unknown zone is a mistake and not a default.
LocalizesFromContext tells you the zone is resolved per request, so you can truthfully tell the model no conversion is needed, and LocationFromContext hands that zone back to render times in the frame the agent wrote them in.
04 Identity
There are no human, agent or session parameters anywhere, so a model can't spoof who it is writing for. Your middleware fills the context once, Create stamps it onto the posterum, ListUpcoming filters by it, and Cancel scopes to it.
This is not access control, and it would be lying if it pretended to be. Whoever holds the Store holds the database, so authentication and tenant isolation belong above the library, at the HTTP or gRPC layer.
Forgetting to fill the context is the easy mistake, so WithLogger logs a warning whenever an identity key is configured but its value is empty. It only tells you and never stops the call.
05 When half of it fails
A database and a queue share no transaction, so they can always end up disagreeing. Create enqueues first and then saves, and if the save fails it cancels the task it just queued.
Cancel deletes the row first and then the task, and if the task won't go it saves the row back. If the rollback fails too, both errors come back joined, never just one of them.
if err := p.queue.Enqueue(ctx, posterum); err != nil {
return Posterum{}, fmt.Errorf("postera: create: %w", err)
}
if err := p.store.Save(ctx, posterum); err != nil {
if rollbackErr := p.queue.Cancel(context.WithoutCancel(ctx), posterum.ID); rollbackErr != nil {
return Posterum{}, errors.Join(
fmt.Errorf("postera: create: %w", err),
fmt.Errorf("postera: create: rollback: %w", rollbackErr),
)
}
return Posterum{}, fmt.Errorf("postera: create: %w", err)
}06 PostgreSQL
The postgres package takes anything that can Exec, Query and QueryRow, so a pgx pool or a single connection both fit.
WithAutoMigrate creates the postera table at startup from a schema embedded in the binary. It is safe to run again and takes an advisory lock, so instances starting together don't trip over each other. Without it, the constructor only checks the table is there and fails right away if it isn't.
WithRetry retries connection errors, serialization failures and deadlocks with the delay doubling each time, and it is off by default because how often to retry is your call.
store, err := postgres.NewStore(ctx, pool,
postgres.WithAutoMigrate(),
postgres.WithRetry(3, 100*time.Millisecond),
)07 Cloud Tasks
When the moment arrives, Cloud Tasks POSTs the message as the request body to the endpoint that wakes your agent. Identity isn't in the body, so each field your handler needs is mapped to a header, and WithServiceAccountEmail attaches an OIDC token so the endpoint can check the call really came from Cloud Tasks.
Enqueue only accepts a trigger between 29 seconds and 29 days from now. Cloud Tasks refuses anything past 30 days, and anything sooner could fire before the state that produced it is even written, so ErrScheduleOutOfRange is meant to reach the agent as a 400.
The task name comes from the posterum ID, so enqueueing the same posterum twice still makes one task.
queue, err := cloudtasks.NewQueue(tasksClient, "my-project", "asia-southeast2", "postera",
"https://agent.example.com/wake",
cloudtasks.WithServiceAccountEmail("tasks@my-project.iam.gserviceaccount.com"),
cloudtasks.WithHumanHeader("X-Postera-Human"),
cloudtasks.WithAgentHeader("X-Postera-Agent"),
cloudtasks.WithSessionHeader("X-Postera-Session"),
)08 With ADK
The ADK tools live in agentkit, a separate module, so the core stays free of any agent framework. One call hands the agent wake_future_self, list_upcoming_wakes and cancel_upcoming_wake, with the prompting already written.
import posteraadk "go.trueardian.com/agentkit/postera/adk"
tools, err := posteraadk.Tools(postarius)09 What it doesn't do
It doesn't run the wake. Postera schedules it and Cloud Tasks makes the call, and what the agent does once it's woken is yours.
It has no recurring schedules. A posterum is one moment, so an agent that wants every morning schedules the next one each time it wakes.
It has no metrics or tracing yet, and only the Postgres and Cloud Tasks adapters. A Store is four methods and a Queue is two, so another one is small to write.
Postera
Wakes Ava in Avagenc Chat whenever she has something to come back to, with three calls