Using Webhooks in a Go Golem Agent
Overview
A Golem webhook lets an agent mint a temporary public URL that an external system POSTs to; the POST body is delivered back to the agent. Under the hood a webhook is a Golem promise: the agent durably suspends while awaiting the callback, consuming no resources, and survives restarts (exactly-once via oplog replay).
A webhook is a promise completed by an HTTP POST:
golem.NewWebhook[T]()creates the promise and the URL that completes it;w.URL()is the URL to hand out.w.ID()returns thegolem.PromiseIDto persist in agent state.golem.PromiseByID[T](id).MustAwait()(orw.MustAwait()) suspends until the POST arrives and returns the body decoded asT;Await()returns(T, error)instead.
This is useful for payment gateways, CI/CD, GitHub/Stripe callbacks, and any event-driven workflow where an external system notifies the agent.
Prerequisites
The agent type must have an HTTP mount (Spec.HTTP = &golem.Mount{Path: "/..."}) and be deployed via an httpApi deployment in golem.yaml. Without that, NewWebhook traps (it is fail-loud). The mount does not need any HTTP endpoints — it only makes the agent HTTP-API-deployable.
Steps
- Add an HTTP mount to the agent’s
Spec(seegolem-add-http-endpoint-go). - Create a webhook; persist its
PromiseIDin state and return/share the URL. - Await the callback in a later invocation via the stored id.
- Deploy behind an
httpApiso the URL is reachable.
API
| Function / method | Description |
|---|---|
golem.NewWebhook[T]() | Create a promise and the public URL that a POST completes; returns *golem.Webhook[T] (a *golem.Promise[T] plus URL()); requires the agent be HTTP-API-deployed |
(*Promise[T]).WebhookURL() string | Mint the URL for a promise created with golem.NewPromise[T]() |
(*Promise[T]).ID() golem.PromiseID | The durable id — persist it to await/complete in a later invocation |
(*Promise[T]).Await() (T, error) | Suspend until completed; returns the POST body decoded as T (JSON, or raw bytes when T is []byte), or an error if it does not decode |
(*Promise[T]).MustAwait() T | Await that panics if the body does not decode |
golem.PromiseByID[T](id) | Rebuild a handle from a stored PromiseID |
golem.CompletePromise[T](id, value) | Complete a promise from another agent (external completers use the URL instead) |
Complete example
Definition (agents/webhookdemo/webhookdemo.go)
// Package webhookdemo is the DEFINITION of a webhook agent. It needs the HTTP
// mount below (plus an httpApi deployment) or NewWebhook traps.
package webhookdemo
import "github.com/golemcloud/golem/sdks/go/golem"
type ID struct{ Queue string } // bound by {queue} in the mount path
// Event is the webhook payload — JSON tags are the contract for the caller's POST.
type Event struct {
OrderID string `json:"orderId"`
Amount int64 `json:"amount"`
}
var Agent = golem.DefineAgent[ID](golem.Spec{
Name: "WebhookDemoAgent",
Description: "Completes a promise from an external webhook POST",
HTTP: &golem.Mount{Path: "/hooks/{queue}"}, // makes the agent HTTP-API-deployable
})
var (
Open = Agent.Method[golem.Unit, string]("open", golem.Description("Create a promise and return its webhook URL"))
Result = Agent.Method[golem.Unit, Event]("result", golem.Description("Await the payload POSTed to the webhook URL"))
)Implementation (agents/webhookdemo/impl/impl.go)
package impl
import (
"myapp/agents/webhookdemo"
"github.com/golemcloud/golem/sdks/go/golem"
)
type state struct {
pending golem.PromiseID
open bool
}
var agent = webhookdemo.Agent.Implement(func(webhookdemo.ID) *state { return &state{} })
func init() {
// Mint a URL; persist the PromiseID so a later invocation can await it.
agent.Handle(webhookdemo.Open, func(ctx *golem.Context[state], _ golem.Unit) string {
w := golem.NewWebhook[webhookdemo.Event]()
ctx.State.pending = w.ID()
ctx.State.open = true
return w.URL() // hand this URL to the external system
})
// Suspend until the external POST arrives; returns the decoded body.
agent.Handle(webhookdemo.Result, func(ctx *golem.Context[state], _ golem.Unit) webhookdemo.Event {
if !ctx.State.open {
panic("no open webhook — call open first")
}
return golem.PromiseByID[webhookdemo.Event](ctx.State.pending).MustAwait()
})
}Payload decoding
Await() decodes the POST body as its type parameter T via JSON, and returns an error when the body does not decode. Use a struct with json tags (as Event above) for structured payloads, or golem.PromiseByID[[]byte](id).Await() to receive the raw request bytes unchanged.
Webhook URL structure and suffix
The minted URL has the shape https://<domain>/<prefix>/<suffix>/<id>:
<domain>— where the HTTP API is deployed.<prefix>— defaults to/webhooks, customizable viawebhookUrlin thehttpApideployment section ofgolem.yaml.<suffix>— defaults to the agent type name in kebab-case; override it withMount.WebhookSuffix.<id>— a unique per-webhook identifier.
HTTP: &golem.Mount{Path: "/api/orders/{id}", WebhookSuffix: "/workflow-hooks"},Key Constraints
- The agent must set
Spec.HTTPand be deployed viahttpApi, orWebhookURL()panics, which traps the component, as in Rust. - Only the agent that created the promise may
Awaitit; awaiting from another agent traps in the host. - Only a
POSTto the URL completes the promise; the URL is one-shot — once completed it is invalid, and a laterAwaitreturns the recorded body from the oplog. - The agent is durably suspended while awaiting — it survives failures, restarts, and updates.
- Go difference from Rust:
Mount.WebhookSuffixmust be literal segments only — path variables like{agent-type}or{name}in the suffix are rejected in the Go SDK (the Rustwebhook_suffixallows them).
Related Guides
| Guide | Description |
|---|---|
golem-add-http-endpoint-go | Set up the HTTP mount + httpApi deployment webhooks require |
golem-add-agent-go | Define the agent and its payload types |
golem-add-http-auth-go | Require authentication on the agent’s other endpoints |