vigoo's software development blog

Golem 1.5 features - Part 1: Code-first routes

Posted on April 08, 2026

Introduction

I am writing a series of short posts showcasing the new features of Golem 1.5, to be released at the end of April, 2026. The episodes of this series will be short and assume the reader knows what Golem is. Check my other Golem-related posts for more information!

Parts released so far:

Code-first routes

In the previous Golem release we introduced code-first agents - we started defining everything in code, with the help of some TypeScript decorators and Rust annotations. With this we could define agents that expose a typed interface, can call each other and so on - but to expose these interfaces via regular HTTP endpoints, we had to define these endpoints in a OpenAPI-like YAML section and use a custom scripting language called Rib to map between the request/response and the underlying agent interface.

In Golem 1.5 this is no longer the case - no custom scripting language, no YAML description of endpoints, everything is possible directly from our agent's code!

Mount points

First we have to define a mount point for our agent:

@agent({
  mount: '/task-agents/{name}',
})
export class Tasks extends BaseAgent {
  // ...
}
#[agent_definition(mount = "/task-agents/{name}")]
pub trait Tasks {
    // ...
}
@agentDefinition(mount = "/task-agents/{name}")
trait Tasks extends BaseAgent {
  // ...
}
#derive.agent
#derive.mount("/task-agents/{name}")
pub(all) struct Tasks {
  // ...
}

In the mount path we can use placeholders like {name} that identifies our agent - it maps directly to our agent constructor's name parameter. If there are multiple agent parameters, they all have to be mapped in the mount path.

Endpoints

Once we have our mount we can export individual agent methods as various endpoints:

@endpoint({ post: "/tasks" })
async createTask(request: CreateTaskRequest): Promise<Task> {
    // ...
}

async getTasks(): Promise<Task[]> {
    // ...
}

@endpoint({ post: "/tasks/{id}/complete" })
async completeTask(id: number): Promise<Task | null> {
    // ...
}
#[endpoint(post = "/tasks")]
fn create_task(&mut self, request: CreateTaskRequest) -> Task;

#[endpoint(get = "/tasks")]
fn get_tasks(&self) -> Vec<Task>;

#[endpoint(post = "/tasks/{id}/complete")]
fn complete_task(&mut self, id: usize) -> Option<Task>;
@endpoint(method = "POST", path = "/tasks")
def createTask(request: CreateTaskRequest): Future[Task]

@endpoint(method = "GET", path = "/tasks")
def getTasks(): Future[Array[Task]]

@endpoint(method = "POST", path = "/tasks/{id}/complete")
def completeTask(id: Int): Future[Option[Task]]
#derive.endpoint(post="/tasks")
pub fn Tasks::create_task(self : Self, request : CreateTaskRequest) -> Task {
  // ...
}

#derive.endpoint(get="/tasks")
pub fn Tasks::get_tasks(self: Self) -> Array[Task] {
  // ...
}

#derive.endpoint(post="/tasks/{id}/complete")
pub fn Tasks::complete_task(self: Self, id: UInt32) -> Option[Task] {
  // ...
}

Endpoint paths are relative to the mount point, and they can also use placeholders mapped to parameters. Unmapped parameters are set from the request body. Query parameters are also supported in the path patterns.

Additional features

Custom headers can also be mapped to function parameters:

@endpoint({
    get: '/example',
    headers: { 'X-Foo': 'location', 'X-Bar': 'name' },
  })
async example(location: string, name: string): Promise<String> {
  // ...
}
#[endpoint(get = "/example", headers("X-Foo" = "location", "X-Bar" = "name"))]
fn example(&self, location: String, name: String) -> String;
@endpoint(method = "GET", path = "/example")
def example(@header("X-Foo") location: String, @header("X-Bar") name: String): Future[String]
#derive.endpoint(get="/example")
#derive.endpoint_header("X-Foo", "location")
#derive.endpoint_header("X-Bar", "name")
pub fn ExampleAgent::example(
  self : Self,
  location: String,
  name: String
) -> String {
  // ...
}

Additionally, endpoint decorators support CORS and authentication. For CORS, we can add something like cors = ["*"] to the decorator (syntax slightly varies by language). We can turn on authentication on the mount level or per individual endpoints. When authentication is enabled, agent constructors and methods optionally can receive a Principal parameter that contains information about the authenticated user.

It's also possible to tell the HTTP layer to create a phantom agent for each request - this is useful for ephemeral, stateless agents serving as a gateway to internal agents as it allows requests to be processed completely parallel. This is a single line change (phantomAgent = true) in the code, just changes how Golem internally maps each individual request to agent instances. Details of this technique will be provided in the updated documentation site.

Deployments

There is still a small step necessary in the application manifest file to make these code-first routes deployed:

httpApi:
  deployments:
    local:
    - domain: app-name.localhost:9006
      agents:
        Tasks: {}

We can specify what agents to deploy to what (sub)domains, and specify this per environment (such as local/staging/prod, for example).

OpenAPI

Defining endpoints in code does not mean we cannot have proper OpenAPI specifications for them. Golem automatically adds an openapi.yaml endpoint to each deployment:

$ curl http://routes.localhost:9006/openapi.yaml
components: {}
info:
  title: Managed api provided by Golem
  version: 1.0.0
openapi: 3.0.0
paths:
  /openapi.yaml:
    get:
      responses:
        "200":
          content:
            application/yaml:
              schema:
                additionalProperties: true
                type: object
          description: Response 200
  /task-agents/{name}/tasks:
    get:
      parameters:
        - description: 'Path parameter: name'
          explode: false
          in: path
          name: name
          required: true
          schema:
            type: string
          style: simple
      responses:
        "200":
# ...