Adding HTTP Endpoints to an Effect Golem Agent

SkillCloud & infra

Guides your agent through exposing an Effect-based Golem agent as HTTP endpoints with routes and parameter bindings.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Adding HTTP Endpoints to an Effect Golem Agent skill

About this capability

Exposing an Effect-based Golem agent over HTTP. Use when adding HTTP mounts, REST endpoints, request parameter bindings, or an httpApi deployment to an @golemcloud/effect-golem agent.

What this skill tells your AI

The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/effect/golem-add-http-endpoint-effect/SKILL.md and read by ahel’s review.

Effect agents publish HTTP route metadata with the Http namespace from @golemcloud/effect-golem. The Golem host serves the routes, decodes path, query, header, and JSON body values into method parameters, invokes the agent, and maps the method result to an HTTP response.

Steps

  1. Define the agent and its methods with defineAgent, method, and Effect Schema.
  2. Add http: Http.mount(...) to the agent definition.
  3. Add an http: [Http.get(...) | Http.post(...) | ...] array to every exposed method.
  4. Implement every handler as an Effect and import its module from src/main.ts.
  5. Add the agent type to an httpApi domain deployment in golem.yaml.
  6. Run golem build, then deploy with golem deploy --yes.

Related Skills

SkillWhen to Load
golem-add-agent-effectDefining the Effect agent, method schemas, and durable state
golem-http-params-effectDetailed path, query, header, body, and response mapping
golem-make-http-request-effectMaking outgoing HTTP requests from an Effect handler
golem-add-http-auth-effectEnabling authentication and reading the caller principal
golem-add-cors-effectConfiguring mount-level or endpoint-level CORS
golem-configure-api-domainConfiguring the httpApi deployment domain

Mount Path

Import Http from the Effect Golem SDK and put one mount on the agent definition:

import { Effect, Schema } from "effect";
import { defineAgent, Http, method } from "@golemcloud/effect-golem";

export const TaskAgent = defineAgent({
  name: "TaskAgent",
  mode: "durable",
  id: {
    taskName: Schema.String,
  },
  http: Http.mount("/api/tasks/{taskName}"),
  methods: {
    // ...
  },
}).implement({
  init: () => Effect.void,
  methods: () => ({
    // ...
  }),
});

Mount rules:

  • The path starts with / and does not end with / unless it is exactly /.
  • Every agent id field appears as a {variable} in the mount path.
  • Variable names use the exact TypeScript id keys, including casing.
  • Mount paths cannot contain query parameters or {*rest} catch-all variables.
  • Use {taskName}, not {task-name}, for a constructor field named taskName. This changes only the placeholder name; both forms would match the same concrete URL segment.

For a fresh phantom agent instance per HTTP request, set the mount option rather than changing the agent mode:

http: Http.mount("/gateway/{name}", { phantomAgent: true }),

mode still accepts only "durable" or "ephemeral"; it is independent of phantomAgent.

Endpoint Declarations

Declare routes on the corresponding method. Endpoint paths are relative to the mount:

methods: {
  listItems: method({
    input: {},
    success: Schema.Array(Item),
    http: [Http.get("/items")],
  }),

  createItem: method({
    input: {
      name: Schema.String,
      count: Schema.Number,
    },
    success: Item,
    http: [Http.post("/items")],
  }),

  updateItem: method({
    input: {
      id: Schema.String,
      name: Schema.String,
    },
    success: Schema.NullOr(Item),
    http: [Http.put("/items/{id}")],
  }),

  deleteItem: method({
    input: { id: Schema.String },
    success: Schema.Void,
    http: [Http.del("/items/{id}")],
  }),
},

Use these exact helpers:

HTTP routeEffect Golem helper
GETHttp.get(path, options?)
POSTHttp.post(path, options?)
PUTHttp.put(path, options?)
DELETEHttp.del(path, options?)
Custom methodHttp.custom("METHOD", path, options?)

One method can have multiple routes by adding multiple entries to its http array.

Parameter Mapping

Bindings always refer to exact method input keys:

searchItems: method({
  input: {
    category: Schema.String,
    query: Schema.String,
    minPrice: Schema.NullOr(Schema.Number),
    tenant: Schema.String,
  },
  success: Schema.Array(Item),
  http: [
    Http.get(
      "/categories/{category}/search?q={query}&min-price={minPrice}",
      { headers: { "X-Tenant": "tenant" } as const },
    ),
  ],
}),
  • {category} binds a path segment to params.category.
  • q={query} binds query key q to params.query; the URL key and TypeScript variable may have different names.
  • Missing optional query or header values decode to null when declared with Schema.NullOr(...).
  • headers maps HTTP header names to method parameter names.
  • A parameter can be bound from only one of path, query, or headers in a given endpoint.
  • Http.get has no body, so every parameter must be bound explicitly.
  • For post, put, del, and other bodyful endpoints, every unbound parameter comes from a JSON object field with the same TypeScript name. For example, unbound inStock expects { "inStock": true }.

Endpoint paths start with /. A {*rest} catch-all is allowed only as the final endpoint path segment.

HTTP Response Mapping

The Golem HTTP host maps Effect method result schemas as follows:

Method success schemaHandler success valueHTTP response
Schema.VoidEffect.void204, empty body
TEffect<T>200, JSON T
UnstructuredText(...)inline text reference200, plain text
UnstructuredBinary(...)inline binary reference200, raw bytes
Schema.NullOr(T)T200, JSON T
Schema.NullOr(T)null404, empty body
method with error: EEffect.fail(E)500, JSON E

Schema.NullOr(T) is lowered by the Effect SDK to WIT option<T>, which is what enables the host's 200/404 mapping. Use a declared error schema and Effect.fail(...) for expected typed failures; do not use defects for normal not-found behavior.

Use an unstructured success value when the response itself must be plain text or binary rather than JSON:

import { Unstructured } from "@golemcloud/effect-golem";

const textSuccess = Unstructured.UnstructuredText({
  restrictions: [{ languageCode: "en" }],
});
const binarySuccess = Unstructured.UnstructuredBinary({
  restrictions: [{ mimeType: "application/octet-stream" }],
});

const textValue = {
  _tag: "inline" as const,
  val: {
    data: "hello from Effect",
    textType: { languageCode: "en" },
  },
} satisfies Unstructured.TextReferenceValue;

const binaryValue = {
  _tag: "inline" as const,
  val: {
    data: new Uint8Array([0, 127, 255]),
    binaryType: { mimeType: "application/octet-stream" },
  },
} satisfies Unstructured.BinaryReferenceValue;

Put textSuccess or binarySuccess in the method's success field and return Effect.succeed(textValue) or Effect.succeed(binaryValue) from its handler. Inline text maps to a text/plain body and uses textType.languageCode as Content-Language; inline binary maps to the declared binaryType.mimeType. Unstructured successes cannot also declare a typed method error. Use Schema.Struct(...), arrays, and other ordinary schemas for JSON responses, and Schema.Void for a 204 empty response. These are fixed host mappings, not an arbitrary response builder.

Complete Durable Example

import { Effect, Ref, Schema } from "effect";
import { defineAgent, Http, method, Snapshot } from "@golemcloud/effect-golem";

const TodoItem = Schema.Struct({
  id: Schema.String,
  title: Schema.String,
  done: Schema.Boolean,
});

const TodoState = Schema.Struct({
  items: Schema.Array(TodoItem),
});

export const TodoAgent = defineAgent({
  name: "TodoAgent",
  mode: "durable",
  id: {
    listName: Schema.String,
  },
  http: Http.mount("/todos/{listName}"),
  snapshotting: Snapshot.define({
    schema: TodoState,
    policy: Snapshot.policy.everyN(10),
  }),
  methods: {
    createItem: method({
      input: { title: Schema.String },
      success: TodoItem,
      http: [Http.post("/items")],
    }),
    listItems: method({
      input: {},
      success: Schema.Array(TodoItem),
      http: [Http.get("/items")],
    }),
    completeItem: method({
      input: { id: Schema.String },
      success: Schema.NullOr(TodoItem),
      http: [Http.post("/items/{id}/complete")],
    }),
  },
}).implement({
  init: () => Ref.make({ items: [] as ReadonlyArray<typeof TodoItem.Type> }),
  methods: (state) => ({
      createItem: ({ title }) =>
        Ref.modify(state, ({ items }) => {
          const item = {
            id: String(items.length + 1),
            title,
            done: false,
          };
          return [item, { items: [...items, item] }] as const;
        }),

      listItems: () => Ref.get(state).pipe(Effect.map(({ items }) => items)),

      completeItem: ({ id }) =>
        Ref.modify(state, ({ items }) => {
          const existing = items.find((item) => item.id === id);
          if (existing === undefined) return [null, { items }] as const;

          const updated = { ...existing, done: true };
          return [
            updated,
            {
              items: items.map((item) => (item.id === id ? updated : item)),
            },
          ] as const;
        }),
  }),
  snapshot: Snapshot.ref<{ items: ReadonlyArray<typeof TodoItem.Type> }>(),
});

Register the top-level implementation:

// src/main.ts
import "./todo-agent.js";

Local imports use the emitted .js suffix in generated ESM projects.

Domain Deployment

Add the agent to the existing httpApi deployment without removing other agents:

httpApi:
  deployments:
    local:
      - domain: my-app.localhost:9006
        agents:
          TodoAgent: {}

Current Golem 1.5 manifests contain deployment configuration only. Do not add legacy apiDefinitions, route lists, OpenAPI extension bindings, or Rib response scripts. Route metadata comes from Http.mount(...) and the method-level Http.*(...) declarations. Golem serves the generated OpenAPI document at /openapi.yaml after deployment.

Key Constraints

  • Import Effect APIs from effect and defineAgent, Http, method, and Snapshot from @golemcloud/effect-golem.
  • Do not use decorators or classes from @golemcloud/golem-ts-sdk in an Effect component.
  • Every agent id field must appear in the mount path with exact TypeScript casing.
  • Every bound path, query, or header variable must match a method parameter.
  • Unbound bodyful-method parameters use same-named camelCase JSON body fields.
  • Handlers return Effects, not plain values or async functions.
  • Use Snapshot.ref<Saved>() when a snapshotted Ref contains the saved schema value.
  • Import the implementation module from src/main.ts; otherwise it is not registered.
  • Do not edit generated files under golem-temp/.

Signals

GitHub stars
2k
Forks
211
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
golem-add-http-endpoint-effect
Source
github.com/golemcloud/golem