Waiting for External Input with Golem Promises (Go)
Overview
A Golem promise lets an agent suspend its execution until an external event completes it. The agent creates a promise, hands the promise ID to an external system (another agent, a webhook, a UI, an HTTP API call), and then awaits the result. The Golem runtime durably suspends the invocation — consuming no resources, surviving restarts, exactly-once via oplog replay — until the promise is fulfilled.
In Go a promise is a typed handle golem.Promise[T]: the completion payload is decoded as T via JSON (or passed through unchanged when T is []byte).
Steps
- Create a promise inside a handler with
golem.NewPromise[T](). - Persist its ID (
p.ID()) in the agent’s state, and/or hand it to the outside world so someone can complete it later. - Await it with
golem.PromiseByID[T](id).MustAwait()(orp.MustAwait()in the same invocation) — the invocation durably suspends here. - Complete it from another agent with
golem.CompletePromise(id, value), or from off-platform via the worker REST endpoint / a mintedWebhookURL().
API (from promise.go)
| Function / Method | Signature | Description |
|---|---|---|
golem.NewPromise[T] | func NewPromise[T any]() *Promise[T] | Create a fresh promise owned by the current agent |
golem.PromiseByID[T] | func PromiseByID[T any](id PromiseID) *Promise[T] | Rebuild a handle from a stored PromiseID |
(*Promise[T]).ID | func (p *Promise[T]) ID() PromiseID | The durable identity to persist or hand out |
(*Promise[T]).Await | func (p *Promise[T]) Await() (T, error) | Durably block until completed; returns the payload as T, or an error if it does not decode as T |
(*Promise[T]).MustAwait | func (p *Promise[T]) MustAwait() T | Await that panics if the payload does not decode |
(*Promise[T]).AwaitContext | func (p *Promise[T]) AwaitContext(ctx context.Context) (T, error) | Await that gives up with ctx.Err() when the context ends first |
(*Promise[T]).WebhookURL | func (p *Promise[T]) WebhookURL() string | Mint an external URL whose POST body completes the promise |
golem.CompletePromise[T] | func CompletePromise[T any](id PromiseID, value T) bool | Complete a promise from another agent; returns false if already completed |
The error from Await reports only a payload that does not decode as T: the bytes come from whoever completed the promise, so a mismatch is theirs. MustAwait panics instead, and the panic traps the component, as in Rust. Only the agent that created the promise may Await it — awaiting from another agent traps in the host.
p.AwaitContext(ctx) gives up when the context ends — a deadline from context.WithTimeout or a cancellation — and returns ctx.Err(); a payload that does not decode is returned as an error too. Giving up only stops that wait: the promise can still be completed, and a later Await returns its value.
PromiseID structure
golem.PromiseID is JSON-marshalable and carries exactly what the REST complete endpoint needs:
type PromiseID struct {
AgentID golem.AgentID // ComponentID: the owning agent's component; AgentID: agent type + constructor params (the ":agent" path segment)
OplogIndex uint64 // disambiguates promises from the same agent ("oplogIdx")
}Full example: human-in-the-loop approval
Definition (agents/approval/approval.go)
// Package approval is the DEFINITION of the human-in-the-loop approval agent.
package approval
import "github.com/golemcloud/golem/sdks/go/golem"
// Verdict is the promise payload, typed as JSON by golem.Promise.
type Verdict struct {
Approved bool
Reason string
}
type ID struct{ Queue string }
var Agent = golem.DefineAgent[ID](golem.Spec{
Name: "ApprovalAgent",
Description: "Human-in-the-loop approvals via durable promises",
})
var (
// Request opens the promise and returns its id for an external completer.
Request = Agent.Method[golem.Unit, golem.PromiseID]("request", golem.Description("Open an approval request, returning the promise id to complete"))
// Await durably blocks until the open request is completed.
Await = Agent.Method[golem.Unit, Verdict]("await", golem.Description("Durably block until the open request is completed"))
)Implementation (agents/approval/impl/impl.go)
// Package impl is the IMPLEMENTATION of the approval agent. Await durably
// SUSPENDS the invocation until the promise is completed.
package impl
import (
"myapp/agents/approval"
"github.com/golemcloud/golem/sdks/go/golem"
)
// state remembers the open promise across invocations.
type state struct {
pending golem.PromiseID
open bool
}
var agent = approval.Agent.Implement(func(approval.ID) *state { return &state{} })
func init() {
agent.Handle(approval.Request, func(ctx *golem.Context[state], _ golem.Unit) golem.PromiseID {
p := golem.NewPromise[approval.Verdict]()
ctx.State.pending = p.ID() // persist so a later invocation can Await it
ctx.State.open = true
return p.ID()
})
agent.Handle(approval.Await, func(ctx *golem.Context[state], _ golem.Unit) approval.Verdict {
if !ctx.State.open {
panic("no open approval request — call request first")
}
// Rebuild the handle from the stored id and durably suspend until completion.
return golem.PromiseByID[approval.Verdict](ctx.State.pending).MustAwait()
})
}Completing from another agent
// From any agent — agent-to-agent completion:
ok := golem.CompletePromise(pendingID, approval.Verdict{Approved: true, Reason: "looks good"})
_ = ok // false means it was already completed (idempotency signal)Completing from outside Golem (REST)
Hand the three PromiseID fields to the external system; it POSTs to the worker-service complete endpoint:
curl -X POST \
"$GOLEM_URL/v1/components/$COMPONENT_ID/workers/$AGENT_ID/complete" \
-H 'Content-Type: application/json' \
-d '{"oplogIdx": <oplog_index>, "data": [<bytes>]}'The data bytes become the payload Await decodes as T (JSON when T is a struct).
Webhook-style callback
When the agent type is HTTP-API-deployed (its Spec.HTTP is set and it is declared in the manifest’s httpApi deployment), mint a URL whose POST body completes the promise:
p := golem.NewPromise[Event]()
url := p.WebhookURL() // hand this to an off-platform system; its POST body completes the promise
ctx.State.pending = p.ID()
// later, in another invocation:
event := golem.PromiseByID[Event](ctx.State.pending).MustAwait()WebhookURL is fail-loud: it traps if the agent is not HTTP-API-deployed.
Key Constraints
- The promise payload is typed as
Tvia JSON. Usegolem.Promise[[]byte]as an escape hatch to carry raw, non-JSON bytes through unchanged. - Only the creating agent may
Awaita promise; awaiting from a different agent traps. - Persist
p.ID()in the agent’s state if the promise is created in one invocation and awaited in another — a single agent processes invocations sequentially, soRequestreturns beforeAwaitruns. Awaitreturns an error only for a payload that does not decode asT; the wait itself is durable and is not retried by the caller.CompletePromisereturnsfalseif the promise was already completed; treat it as the idempotency signal, not a failure.
Related Guides
| Guide | Description |
|---|---|
golem-call-another-agent-go | An agent completes another agent’s promise via RPC |
golem-parallel-workers-go | Fan out work and collect results via promises |
golem-add-http-endpoint-go | Expose the agent over HTTP so WebhookURL works |
golem-add-agent-go | Define the agent that creates the promise |