Skip to Content
Next — You're reading the unreleased preview docs. APIs and pages may change before the next release.Switch to the current docs →
How-To GuidesTypeScriptAdding HTTP Endpoints to a TypeScript Golem Agent

Adding HTTP Endpoints to a TypeScript Golem Agent

Overview

Golem agents can be exposed over HTTP using code-first route definitions. This involves:

  1. Adding an http mount to defineAgent(...) with http.mount(...)
  2. Attaching http: http.<verb>(...) endpoints to methods declared with method(...)
  3. Adding an httpApi deployment section to golem.yaml (See the golem-configure-api-domain guide)
GuideDescription
golem-http-params-tsPath/query/header variable mapping, body mapping, supported types, response mapping
golem-make-http-request-tsMaking outgoing HTTP requests from agent code, especially when calling other Golem agent endpoints (required for correct JSON body formatting)
golem-add-http-auth-tsEnabling authentication and receiving Principal
golem-add-cors-tsConfiguring CORS allowed origins
golem-configure-api-domainSetting up httpApi in golem.yaml, security schemes, domain deployments, and subdomain versus domain choices

Steps

  1. Add http: http.mount(...) to defineAgent(...)
  2. Add http: http.<verb>(...) to the methods you want to expose
  3. Add httpApi deployment to golem.yaml (see golem-configure-api-domain guide)
  4. Build and deploy

Mount Path

The http field on defineAgent(...) defines the base HTTP path via http.mount(...). Path variables in {braces} map to the agent’s id record fields:

import { z } from 'zod'; import { defineAgent, method, http } from '@golemcloud/golem-ts-sdk'; export const TaskAgent = defineAgent({ name: 'TaskAgent', id: { name: z.string() }, http: http.mount('/api/tasks/{name}'), methods: { // ... }, });

Rules:

  • Path must start with /
  • Every id field must appear as a {variable} in the mount path (enforced at compile time)
  • Every ordinary {variable} must match an id field name; system variables and an explicit phantomId path selector are excluded
  • Catch-all {*rest} variables are not allowed in mount paths

The mount path template is template-literal typed: an ordinary {var} that does not match an id field (or an id field with no matching {var}) is a tsc error, not just a runtime failure. System variables and the explicit phantom path selector do not count as id fields. Passing a dynamically built (non-literal) string widens to string and defers to the runtime checks.

Endpoint Declaration

Attach an http endpoint to a method with one of the verb builders (http.get, http.post, http.put, http.del, http.patch, or http.custom):

methods: { listItems: method({ input: {}, returns: z.array(Item), http: http.get('/items') }), createItem: method({ input: { name: z.string(), count: z.number() }, returns: Item, http: http.post('/items'), }), updateItem: method({ input: { id: z.string(), name: z.string() }, returns: Item, http: http.put('/items/{id}'), }), deleteItem: method({ input: { id: z.string() }, returns: z.void(), http: http.del('/items/{id}') }), patchItem: method({ input: { id: z.string(), patch: PatchData }, returns: Item, http: http.custom('PATCH', '/items/{id}'), }), }

Endpoint paths are relative to the mount path. To expose a method under multiple routes, pass an array: http: [http.get('/items'), http.get('/all')].

For details on how path variables, query parameters, headers, and request bodies map to method inputs, See the golem-http-params-ts guide.

Durable Stream Route Customization

For a method with stream inputs or outputs, pass durableStreams in the endpoint options:

http: http.post('/events', { durableStreams: { slots: [ { source: 'input', slot: 'input', name: 'uploads' }, { source: 'output', slot: '$result', name: 'events', contentType: 'application/vnd.example.events', }, ], load: { maxConcurrentReadersPerStream: 8, maxAppendRequestsPerSecondPerStream: 25, }, }, }),
  • source and slot select canonical top-level stream slots. $result is the only implicit result selector.
  • name changes the public URL and OpenAPI name; the canonical name is no longer accepted in the public URL.
  • JSON-shaped slots may explicitly set contentType: 'application/json' only. A direct byte stream (stream<u8>) may use a concrete non-text, non-JSON MIME type (also excluding +json) without parameters or wildcards. Do not use text/plain for stream<string>. SSE still uses text/event-stream and base64 data.
  • Reader and append limits must be positive and no greater than the installation maximums. Omitted values use the registry installation defaults.
  • Limits are maintained per route and worker-service node, not as a cluster-wide quota.

Phantom Agents

With no explicit phantomId selector, set phantomAgent: true on the mount to select a fresh phantom identity for each HTTP request. An explicit path or query selector takes precedence over this policy. Requests can run on separate instances. Identity and lifecycle mode are independent: the default mode is durable; add mode: 'ephemeral' to defineAgent for ephemeral execution (which also defaults to fresh phantom selection when no selector is present).

export const GatewayAgent = defineAgent({ name: 'GatewayAgent', id: { name: z.string() }, http: http.mount('/gateway/{name}', { phantomAgent: true }), methods: { /* each HTTP request gets its own agent instance */ }, });

Return Type to HTTP Response Mapping

Golem maps a method’s returned value to HTTP status codes and response bodies according to the table below. This mapping is currently not configurable. In the TypeScript SDK the return shape is declared by the method’s returns schema.

Returned value / returns schemaHTTP StatusResponse Body
z.void() / no value204 No Contentempty
any schema T200 OKJSON-serialized T
T.nullable() / T.optional()200 OK if value, 404 Not Found if null / undefinedJSON T or empty
s.option(T)200 OK if value, 404 Not Found if undefinedJSON T or empty
s.result(ok, err) returning Result.ok / Result.err200 OK if Ok, 500 Internal Server Error if ErrJSON ok or JSON err
s.result(s.option(T), err)200 OK if Result.ok(value), 404 if Result.ok(undefined), 500 if Result.errJSON T, empty, or JSON err
s.unstructuredBinary()200 OK (url → 307 redirect)Raw binary with Content-Type
s.unstructuredText()200 OK (url → 307 redirect)text/plain; charset=utf-8 (+ optional Content-Language)

The value inside an option or Result.ok follows the same rules as a top-level value: s.unstructuredBinary() / s.unstructuredText() wrapped in s.option(...), s.result(..., err), or s.result(s.option(...), err) is sent as the raw body, not as JSON. Use s.option(...) around unstructured markers; zod .nullable() / .optional() wrappers cannot wrap them. A guarded raw download (raw bytes / 404 / JSON error 500):

download: method({ input: { id: z.string() }, returns: s.result(s.option(s.unstructuredBinary({ mimeTypes: ['image/png'] })), z.object({ error: z.string() })), http: http.get('/files/{id}'), }), // Implementation download({ id }) { if (id === 'missing') return Result.ok(undefined); // 404 if (id === 'pending') return Result.err({ error: 'pending' }); // 500, JSON body return Result.ok({ tag: 'inline' as const, val: new Uint8Array([1, 2, 3]), mimeType: 'image/png' }); // 200, raw bytes },

Complete Example

import { z } from 'zod'; import { defineAgent, method, http, s, Result } from '@golemcloud/golem-ts-sdk'; const Task = z.object({ id: z.string(), title: z.string(), done: z.boolean() }); type Task = z.infer<typeof Task>; export const TaskAgent = defineAgent({ name: 'TaskAgent', id: { name: z.string() }, http: http.mount('/task-agents/{name}'), methods: { getTasks: method({ input: {}, returns: z.array(Task), http: http.get('/tasks') }), createTask: method({ input: { title: z.string() }, returns: Task, http: http.post('/tasks') }), getTask: method({ input: { id: z.string() }, returns: Task.nullable(), http: http.get('/tasks/{id}') }), completeTask: method({ input: { id: z.string() }, returns: s.result(Task, z.object({ error: z.string() })), http: http.post('/tasks/{id}/complete'), }), }, }); export const TaskAgentImpl = TaskAgent.implement({ init: () => ({ tasks: [] as Task[] }), methods: { getTasks() { return this.tasks; }, createTask({ title }) { const task: Task = { id: String(this.tasks.length + 1), title, done: false }; this.tasks.push(task); return task; }, getTask({ id }) { return this.tasks.find((t) => t.id === id) ?? null; }, completeTask({ id }) { const task = this.tasks.find((t) => t.id === id); if (!task) return Result.err({ error: 'not found' }); task.done = true; return Result.ok(task); }, }, });
# golem.yaml (add to existing file) httpApi: deployments: local: - subdomain: my-app # resolves to my-app.localhost:9006 by default agents: TaskAgent: {}

Key Constraints

  • An http.mount(...) is required on defineAgent(...) before any method can declare an http endpoint
  • Every id field must be provided via a mount path variable; mount-level header binding is not supported
  • Path/query/header variable names must exactly match id fields (mount) or method input keys (endpoint)
  • Catch-all path variables {*name} can only appear as the last endpoint path segment and are not allowed in mount paths
  • The endpoint path must start with /
  • Use exactly one verb builder per endpoint; pass an array of endpoints to expose a method under multiple routes