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 |
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.
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 |
Future[UnstructuredBinary] | 200 OK | Raw binary with Content-Type |
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:
- domain: my-app.localhost:9006
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