Adding HTTP Endpoints to a Scala Golem Agent
Overview
Golem agents can be exposed over HTTP using code-first route definitions. This involves:
- Adding a
mountparameter to@agentDefinition - Annotating methods with
@endpoint - Adding an
httpApideployment section togolem.yaml(See thegolem-configure-api-domainguide)
Related Guides
| Guide | Description |
|---|---|
golem-http-params-scala | Path/query/header variable mapping, body mapping, supported types, response mapping |
golem-make-http-request-scala | Making outgoing HTTP requests from agent code, especially when calling other Golem agent endpoints (required for correct JSON body formatting) |
golem-add-http-auth-scala | Enabling authentication |
golem-add-cors-scala | Configuring CORS allowed origins |
golem-configure-api-domain | Setting up httpApi in golem.yaml, security schemes, domain deployments, and subdomain versus domain choices |
Steps
- Add
mount = "/path/{param}"to@agentDefinition(...) - Add
@endpoint(method = "GET", path = "/...")to trait methods - Add
httpApideployment togolem.yaml(seegolem-configure-api-domainguide) - Build and deploy
Mount Path
The mount parameter on @agentDefinition defines the base HTTP path. Path variables in {braces} map to constructor parameters defined in the class Id(...):
import golem.runtime.annotations.{agentDefinition, endpoint}
import golem.BaseAgent
import scala.concurrent.Future
@agentDefinition(mount = "/api/tasks/{name}")
trait TaskAgent extends BaseAgent {
class Id(val name: String)
// methods...
}Constructor Parameter Naming in Scala
The path variable names must exactly match the class Id parameter names — do not use kebab-case in path variables. Use the same camelCase (or single-word) identifier that appears in the class Id:
- Single parameter:
class Id(val name: String)→ use{name}in path - Multiple parameters:
class Id(val arg0: String, val arg1: Int)→ use{arg0},{arg1}in path - Custom Id class with
@id: use the parameter names from the annotated class
// ✅ Correct — path variable matches parameter name exactly
@agentDefinition(mount = "/api/tasks/{taskName}")
trait TaskAgent extends BaseAgent {
class Id(val taskName: String)
}
// ❌ Wrong — {task-name} does not match parameter name taskName
@agentDefinition(mount = "/api/tasks/{task-name}")
trait TaskAgent extends BaseAgent {
class Id(val taskName: String)
}// Named parameters with @id annotation
import golem.runtime.annotations.id
@agentDefinition(mount = "/api/catalog/{region}/{catalog}")
trait CatalogAgent extends BaseAgent {
@id
class CatalogParams(val region: String, val catalog: String)
}Rules:
- Path must start with
/ - Every constructor parameter must appear as a
{variable}in the mount path - Every
{variable}must exactly match a constructor parameter name (same casing) - Catch-all
{*rest}variables are not allowed in mount paths
Endpoint Annotation
The @endpoint annotation marks a method as an HTTP endpoint. Specify the HTTP method and path:
@endpoint(method = "GET", path = "/items")
def listItems(): Future[List[Item]]
@endpoint(method = "POST", path = "/items")
def createItem(name: String, count: Int): Future[Item]
@endpoint(method = "PUT", path = "/items/{id}")
def updateItem(id: String, name: String): Future[Item]
@endpoint(method = "DELETE", path = "/items/{id}")
def deleteItem(id: String): Future[Unit]Endpoint paths are relative to the mount path. Supported HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE, or any custom string.
For details on how path variables, query parameters, headers, and request bodies map to method parameters, See the golem-http-params-scala guide.
Durable Stream Route Customization
For a method with stream inputs or outputs, use @durableStreamSlot for each customized slot and @durableStreams for route policy:
@endpoint(method = "POST", path = "/events")
@durableStreamSlot(source = "input", slot = "input", name = "uploads")
@durableStreamSlot(
source = "output",
slot = "$result",
name = "events",
contentType = "application/vnd.example.events"
)
@durableStreams(
allowExternalWrites = true,
allowStreamDelete = false,
allowInvocationDelete = false,
maxConcurrentReadersPerStream = 8,
maxAppendRequestsPerSecondPerStream = 25
)
def events(
input: golem.schema.AgentStream[golem.UByte]
): Future[golem.schema.AgentStream[golem.UByte]]Import durableStreamSlot and durableStreams from golem.runtime.annotations. If a method has multiple @endpoint annotations, identify the target in each durable-stream annotation with matching endpointMethod and endpointPath arguments.
sourceandslotselect canonical top-level stream slots.$resultis the only implicit result selector.namechanges the public URL and OpenAPI name; the canonical name is no longer accepted in the public URL.contentTypeis allowed only for a directAgentStream[golem.UByte]slot and must be a concrete non-text, non-JSON MIME type without parameters or wildcards. JSON-shaped streams stayapplication/json; do not usetext/plainforAgentStream[String]. SSE still usestext/event-streamand base64 data.- The three
allow*options default totrue. Setting one tofalseremoves that protocol operation and produces405with an exactAllowheader. - Omit either numeric argument to use the worker-service default. An explicit live-reader limit must be 1 through 16 and applies to long-poll and SSE. An explicit append limit must be positive and cannot be set when external writes are disabled. A route-local limit rejection is
429withRetry-After: 1. - Limits are maintained per route and worker-service node, not as a cluster-wide quota.
Phantom Agents
Set phantomAgent = true to create a new agent instance for each HTTP request, enabling fully parallel processing:
@agentDefinition(
mount = "/webhook/{agent-type}/{value}",
phantomAgent = true
)
trait WebhookHandler extends BaseAgent {
class Id(val value: String)
// Each HTTP request gets its own agent instance
}Custom Types
All types used in endpoint parameters and return values must have a zio.blocks.schema.Schema instance:
import zio.blocks.schema.Schema
final case class Task(id: String, title: String, done: Boolean) derives SchemaFor collections, use List[T] instead of Array[T]. Array does not have automatic Schema derivation support.
Return Type to HTTP Response Mapping
Golem maps method return types to HTTP status codes and response bodies according to the table below. This mapping is currently not configurable.
Scala Either[E, T] is mapped to the WIT result<T, E> type, with Right treated as Ok and Left as Err.
| Return Type | HTTP Status | Response Body |
|---|---|---|
Future[Unit] | 204 No Content | empty |
Future[T] | 200 OK | JSON-serialized T |
Future[Option[T]] | 200 OK if Some, 404 Not Found if None | JSON T or empty |
Future[Either[E, T]] | 200 OK if Right, 500 Internal Server Error if Left | JSON T or JSON E |
Future[Either[E, Unit]] | 204 No Content if Right, 500 if Left | empty or JSON E |
Future[Either[Unit, T]] | 200 OK if Right, 500 if Left | JSON T or empty |
The current Scala SDK does not expose UnstructuredText or UnstructuredBinary. Use ordinary
schema-backed values, which are JSON encoded, or choose an SDK that supports rich raw-body types
when an endpoint must return plain text or arbitrary binary bytes.
Complete Example
import golem.runtime.annotations.{agentDefinition, agentImplementation, endpoint, header}
import golem.BaseAgent
import zio.blocks.schema.Schema
import scala.concurrent.Future
final case class Task(id: String, title: String, done: Boolean) derives Schema
@agentDefinition(mount = "/task-agents/{name}")
trait TaskAgent extends BaseAgent {
class Id(val name: String)
@endpoint(method = "GET", path = "/tasks")
def getTasks(): Future[List[Task]]
@endpoint(method = "POST", path = "/tasks")
def createTask(title: String): Future[Task]
@endpoint(method = "GET", path = "/tasks/{id}")
def getTask(id: String): Future[Option[Task]]
@endpoint(method = "POST", path = "/report")
def submitReport(@header("X-Tenant") tenantId: String, data: String): Future[String]
}import golem.runtime.annotations.agentImplementation
import scala.concurrent.Future
@agentImplementation()
final class TaskAgentImpl(private val name: String) extends TaskAgent {
private var tasks: List[Task] = Nil
override def getTasks(): Future[List[Task]] =
Future.successful(tasks)
override def createTask(title: String): Future[Task] = Future.successful {
val task = Task(id = (tasks.length + 1).toString, title = title, done = false)
tasks = tasks :+ task
task
}
override def getTask(id: String): Future[Option[Task]] =
Future.successful(tasks.find(_.id == id))
override def submitReport(tenantId: String, data: String): Future[String] =
Future.successful(s"Report from $tenantId: $data")
}# golem.yaml (add to existing file)
httpApi:
deployments:
local:
- subdomain: my-app # resolves to my-app.localhost:9006 by default
agents:
TaskAgent: {}Key Constraints
- A
mountpath is required on@agentDefinitionbefore any@endpointannotations can be used - All constructor parameters (from
class Id) must be provided via mount path variables - Path/query variable names must exactly match method parameter names
- Header parameters use
@header("Header-Name")annotation on individual method parameters - Catch-all path variables
{*name}can only appear as the last path segment - The endpoint path must start with
/ - Custom types require a
zio.blocks.schema.Schemainstance (usederives Schemain Scala 3) - The
scalacOptions += "-experimental"flag is required for macro annotations