Skip to Content
InvokeMaking Custom APIsMake Custom HTTP APIs for your Golem App

Make Custom HTTP APIs for your Golem App

Golem’s gateway service not only exposes the low-level REST API for invocations but also provides a way to expose agent methods as HTTP APIs.

In Golem 1.5, HTTP endpoints are defined directly in agent code using language-native decorators and macros — no YAML route definitions or scripting languages needed.

For raw streaming request/response handling, framework-owned routing, or static and live file mappings, see HTTP routers and file serving. Complete code-first endpoints take precedence over those mounted fallback behaviors.

Mount Points

Every agent that wants to expose HTTP endpoints must first define a mount point — the base URL prefix under which all its endpoints are available. The mount path can contain placeholders like {name} that map to the agent’s constructor parameters.

export const Tasks = defineAgent({ name: 'Tasks', id: { name: z.string() }, http: http.mount('/task-agents/{name}'), methods: { // ... }, });

If there are multiple agent constructor parameters, they all have to be mapped in the mount path.

Endpoints

Once the mount is defined, individual agent methods can be exposed as HTTP endpoints. Each endpoint specifies an HTTP method and a path suffix relative to the mount point. Path placeholders are mapped to method parameters. Parameters not mapped through the path or headers are taken from the request body.

createTask: method({ input: { request: CreateTaskRequest }, returns: Task, http: http.post('/tasks'), }), getTasks: method({ input: {}, returns: z.array(Task), http: http.get('/tasks') }), completeTask: method({ input: { id: z.number() }, returns: Task.nullable(), http: http.post('/tasks/{id}/complete'), }),

Supported HTTP methods: get, post, put, delete, patch, head, options, connect, trace, and custom (for arbitrary method strings, TypeScript only).

Query Parameters

Query parameters can be bound to method parameters using standard HTTP query syntax in the endpoint path:

search: method({ input: { query: z.string(), limit: z.number() }, returns: z.array(SearchResult), http: http.get('/search?q={query}&limit={limit}'), }),

Header Mapping

Custom HTTP headers can be mapped to function parameters:

example: method({ input: { location: z.string(), name: z.string() }, returns: z.string(), http: http.get('/example', { headers: { 'X-Foo': 'location', 'X-Bar': 'name' } }), }),

Catch-all Path Segments

The {*variable} syntax captures all remaining path segments. It must be the last segment in the path:

serveFile: method({ input: { rest: z.string() }, returns: FileData, http: http.get('/files/{*rest}'), }),

Response Conventions

Return types control the HTTP response:

Return typeHTTP response
A value (e.g. Task)200 OK with JSON body
Option<T> / T | nullSome/non-null → 200; None/null → 404 Not Found
Result<T, E>Ok → 200; Err → 400 Bad Request with JSON error
void / ()204 No Content

Binary Request Bodies

Use the UnstructuredBinary type to receive raw binary data in the request body:

upload: method({ input: { file: s.unstructuredBinary() }, returns: z.number(), http: http.post('/upload'), }),

CORS

CORS can be configured at the mount level (applies to all endpoints) and optionally overridden per endpoint:

export const MyAgent = defineAgent({ name: 'MyAgent', id: { name: z.string() }, http: http.mount('/api/{name}', { cors: ['https://app.example.com'] }), methods: { // All endpoints inherit the mount-level CORS setting // This endpoint adds additional allowed origins getPublic: method({ input: {}, returns: Data, http: http.get('/public', { cors: ['*'] }) }), }, });

Per-endpoint CORS settings are unioned with the mount-level settings.

Authentication

Golem API gateway has built-in authentication support. Authentication can be enabled at the mount level or per endpoint using auth = true. See the API authentication page for setup details.

export const SecureAgent = defineAgent({ name: 'SecureAgent', id: { name: z.string() }, http: http.mount('/secure/{name}', { auth: true }), // require auth on all endpoints methods: { // ... }, }); // Or per-endpoint: adminOnly: method({ input: { principal: s.principal() }, // principal contains the authenticated user's identity returns: AdminData, http: http.get('/admin', { auth: true }), }),

When authentication is enabled, agent constructors and methods can optionally receive a Principal parameter containing information about the authenticated caller.

Phantom Agents

Endpoints can be served by phantom agents — separate instances with the same constructor parameters and an additional UUID. Without an explicit selector, phantomAgent: true creates a fresh phantom for each ordinary HTTP request. This is useful for gateway-like agents that dispatch to internal agents, allowing requests to be processed completely in parallel. Phantom identity is distinct from the agent’s durable or ephemeral execution mode; a durable phantom can retain state and be addressed again by its UUID.

export const Gateway = defineAgent({ name: 'Gateway', id: { name: z.string() }, http: http.mount('/gateway/{name}', { phantomAgent: true }), methods: { // A new instance is created per request }, });

Selecting a known phantom UUID

A mount can declare a path or query selector to address an exact phantom instead of choosing a fresh one. For example, /decisions/d7/state?instance=dc7f9909-2975-42b3-9635-2f5ba7c73782 addresses that phantom of DecisionAgent("d7"). The UUID is scoped to the component and constructor parameters: using the same UUID with d8 selects a different agent.

Selection is not cloning. If the selected UUID has not been used, Golem follows normal agent creation. To copy existing state, fork through the SDK or Durable Streams fork operation, then select the resulting phantom UUID. Subsequent requests with the same constructor parameters and UUID reconnect to that instance.

The following mount fragments declare an optional query selector. Omit the optional flag to make it required:

http: http.mount('/decisions/{decision}', { phantomId: http.phantomId.query('instance', { optional: true }), })

For path selection, use /decisions/{decision}/{instance} and change query to path in TypeScript, Effect, Rust and MoonBit; in Scala, use phantomIdPath instead of phantomIdQuery. Only decision is a constructor parameter. The declared instance capture belongs to phantom selection, not to the constructor or endpoint inputs. An optional path selector generates both /decisions/{decision}/{instance} and /decisions/{decision} before endpoint suffixes are appended; the whole selector segment disappears when omitted.

RequestResult
Valid UUID suppliedSelect that exact phantom.
Optional selector omittedSelect the original agent, even with phantomAgent: true.
Required query selector omitted400 Bad Request.
Required path selector omittedNo matching route (404).
Empty, malformed, or repeated query selector (even repeated identical UUIDs)400 Bad Request.

Uppercase UUIDs are accepted and returned links use canonical lowercase spelling. A selector must belong to the mount; it cannot collide with endpoint inputs or protocol controls. Optional path expansion must not introduce ambiguous routes. Explicit selectors are supported only on durable, non-router mounts without filesystem exposure (including static bindings).

Authentication and authorization run before selection; a UUID is an address, not a credential. Requests, conditional-response caches, streams and callbacks remain scoped to the selected identity. Without a declared selector, all existing original, fresh-phantom, ephemeral and Durable Streams session-selection policies remain unchanged.

Customizing Durable Stream Routes

An endpoint with stream inputs or outputs is exposed as a Durable Streams URL family. By default, each URL uses the canonical method parameter name (and $result for a direct stream result), JSON streams use application/json, byte streams use application/octet-stream, external writes and both deletion operations are enabled, and the worker-service defaults control admission.

Route metadata can customize that HTTP surface without changing the agent method or the durable stream itself:

CustomizationEffect
Slot nameReplaces the canonical slot name in the public URL and OpenAPI document. The canonical name remains the source selector in metadata and is not also accepted as a URL alias.
Slot contentTypeReplaces application/octet-stream for a direct stream<u8> slot. Ordinary reads and metadata, append negotiation, and OpenAPI use the custom MIME type; SSE remains text/event-stream with base64 data.
allowExternalWrites: falseRemoves POST from writable input slots.
allowStreamDelete: falseRemoves DELETE from stream URLs.
allowInvocationDelete: falseRemoves DELETE from invocation-session URLs.
Reader limitCaps concurrent admitted live readers (long-poll and SSE) per stream for this route.
Append limitCaps admitted append requests per second per stream for this route.

Disabled operations return 405 Method Not Allowed with an Allow header that lists only the enabled methods. Admission exhaustion returns 429 Too Many Requests with Retry-After: 1. Limits are maintained independently for each deployed route and worker-service node; they do not replace executor protocol limits or form a cluster-wide quota.

These options preserve the official Durable Streams wire protocol: they choose URL names, an opaque byte-stream media type, optional protocol operations, and stricter gateway admission. Custom media types are supported only for direct byte streams (stream<u8>) and must be concrete non-text, non-JSON types without parameters or wildcards. JSON-shaped streams remain application/json. Native text/plain for stream<string> is not supported; use the JSON representation so byte sub-offset forks cannot split a UTF-8 value.

Only $result has an implicit canonical name. Other slot selectors must identify an existing top-level input or output slot. Public names must be unique across the route, and canonical selectors must remain valid and unique even when a public name is supplied.

The same route customization looks like this in each SDK:

http: http.post('/events', { durableStreams: { slots: [ { source: 'input', slot: 'input', name: 'uploads' }, { source: 'output', slot: '$result', name: 'events', contentType: 'application/vnd.example.events', }, ], allowExternalWrites: true, allowStreamDelete: false, allowInvocationDelete: false, load: { maxConcurrentReadersPerStream: 8, maxAppendRequestsPerSecondPerStream: 25, }, }, }),

Deployments

To make the code-first routes accessible, a deployment section is needed in the application manifest (golem.yaml). This maps agents to domains per environment:

httpApi: deployments: local: - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: Tasks: {}

You can specify which agents to deploy to which (sub)domains, organized by environment (e.g. local, staging, prod).

Deploying the API

Once the deployment is configured in golem.yaml, deploy with:

golem deploy

OpenAPI Specification

Golem automatically generates an OpenAPI specification from the code-first route definitions. Each deployment exposes openapi.yaml and openapi.json endpoints:

curl http://app-name.localhost:9006/openapi.yaml

This returns an OpenAPI 3.1 specification describing the deployed endpoints, including path parameters and request/response schemas. Download this deployment-specific document when generating an HTTP client; the Golem management API specification does not describe your agents’ routes.

Methods with stream parameters or results appear as Durable Streams URL families:

  • PUT {base} creates a server-assigned session and returns its URL in Location.
  • PUT, GET, HEAD, and DELETE on {base}/invocations/{session} create, inspect, or cooperatively cancel a session.
  • Each stream has a concrete path under {base}/invocations/{session}/streams/. Input streams support POST for appending; output streams do not. DELETE cancels and tombstones an individual stream.

The document includes the schema and direction of every slot, with x-golem-route-mode: durable-streams and x-golem-stream-slot metadata. Without a public name override, the direct result slot is spelled %24result in the OpenAPI path. JSON reads return arrays of the element type; byte streams use their configured media type. Reads also support long-poll and SSE through the live query parameter. SSE returns text/event-stream, so clients need an SSE parser rather than a JSON response parser; byte-stream SSE data is base64 encoded and declares Stream-SSE-Data-Encoding: base64.

Non-stream arguments belong to session creation and retain their path, query, header, or body bindings. Create the session before appending when the method requires body or header arguments. Append operations document producer retry headers and Stream-Closed for atomic closure. JSON array bodies are batches; an array-valued message needs an outer batch array. Empty append bodies are valid only when closing the stream.

Troubleshooting

See the troubleshooting page for some common issues and their solutions.