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.
TypeScript
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.
TypeScript
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:
TypeScript
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:
TypeScript
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:
TypeScript
serveFile: method({
input: { rest: z.string() },
returns: FileData,
http: http.get('/files/{*rest}'),
}),Response Conventions
Return types control the HTTP response:
| Return type | HTTP response |
|---|---|
A value (e.g. Task) | 200 OK with JSON body |
Option<T> / T | null | Some/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:
TypeScript
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:
TypeScript
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.
TypeScript
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.
TypeScript
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:
TypeScript
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.
| Request | Result |
|---|---|
| Valid UUID supplied | Select that exact phantom. |
| Optional selector omitted | Select the original agent, even with phantomAgent: true. |
| Required query selector omitted | 400 Bad Request. |
| Required path selector omitted | No 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:
| Customization | Effect |
|---|---|
Slot name | Replaces 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 contentType | Replaces 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: false | Removes POST from writable input slots. |
allowStreamDelete: false | Removes DELETE from stream URLs. |
allowInvocationDelete: false | Removes DELETE from invocation-session URLs. |
| Reader limit | Caps concurrent admitted live readers (long-poll and SSE) per stream for this route. |
| Append limit | Caps 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:
TypeScript
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 deployOpenAPI 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.yamlThis 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 inLocation.PUT,GET,HEAD, andDELETEon{base}/invocations/{session}create, inspect, or cooperatively cancel a session.- Each stream has a concrete path under
{base}/invocations/{session}/streams/. Input streams supportPOSTfor appending; output streams do not.DELETEcancels 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.