---
name: iii-hq/iii-http
source: https://app.decimal.ai/s/iii-hq-iii-http@1/SKILL.md
source_sha256: d6fc77148275
---

# iii-http

The `iii-http` worker exposes registered functions as HTTP endpoints. It runs an HTTP server on the configured host/port, matches each inbound request against the `http` triggers you bind, and invokes the bound function with the request. The handler returns a response envelope that the worker serializes to the wire — JSON, plain text, or bytes, depending on the `Content-Type` the handler sets.

The worker has no callable functions of its own — you never invoke an `http::*` id. Its surface is the `http` trigger TYPE plus a middleware chain that runs before each handler. You expose an endpoint by registering a normal function and binding an `http` trigger to it.

## Do NOT stand up your own web server

This is the way to serve HTTP on iii, and the only one: register a function, then bind an `http` trigger to it. Do NOT write or run your own web server — no `express`, no `http.createServer`, no `fastify`, no framework listening on a port. iii does not route to a server you start yourself; such a process is unreachable as an iii endpoint, and running it as a foreground process (e.g. inside a sandbox) just hangs. If you are reaching for a web framework or a `listen()` call, stop — that instinct is from another ecosystem. The iii equivalent is one `iii.registerTrigger({ type: 'http', ... })` per route; this worker is the server.

## Get the contract from the engine

This page explains WHEN and HOW to serve HTTP; the engine is the source of truth for the exact shapes. Before you bind a trigger or write a handler, fetch the contract and build to it — do not guess field names, types, or defaults from memory:

```
engine::triggers::info { id: "http" }
```

returns three schemas: `configuration_schema` (the route fields — path, method, optional route gating, per-route middleware), `request_schema` (the request envelope your handler receives), and `response_schema` (the response envelope your handler must return). Build to all three. Use `engine::triggers::list` to discover trigger types in the first place.

## Set the right HTTP status

YOUR handler chooses the HTTP status code — it is a field on the response envelope whose exact name is in the `response_schema` from `engine::triggers::info { id: "http" }` (read it; do not guess the field name from memory). The status is NOT inferred from the body, so a handler that just returns a value or an error object yields `200` by default. For error cases, set the matching status explicitly: a not-found path returns `404`, a bad request `400`, and so on. Returning `200` with an `{ error: ... }` body is a bug — it "isn't a 500," but it also isn't the status the requirement asked for, and callers (and tests) will read it as success. Avoiding a crash is not the same as returning the right status.

## When to Use

- Expose a function as a REST endpoint without standing up a separate HTTP server.
- You need URL path parameters or query strings parsed and handed to the handler.
- You want condition-gated routes or per-route preHandler middleware.
- You need a single not-found handler for unmatched routes.

## Boundaries

- No callable functions — the worker is never reached through an `http::*` id; routes are bound with `iii.registerTrigger`, not called.
- Two routes on the same path + method conflict; the router resolves to the most recently registered one.
- Query-string parsing is single-valued — a repeated key keeps only the last value; pre-encode multi-valued params yourself.
- Middleware runs BEFORE body parsing, so it sees headers, path, and query but not the body — use it for preconditions, not response logic (that belongs in the handler).

## Binding a route

Register your handler as a normal function, then bind an `http` trigger to it with `iii.registerTrigger` using the `http` trigger type. Shape the trigger config to what `engine::triggers::info { id: "http" }` describes — the route path and method, an optional gating function evaluated before the handler, and per-route middleware. Path segments you declare in the route arrive as string path parameters on the request; the gating function, if set, can veto a request before the handler runs.

## Where your route is served

Your bound route is served by the `iii-http` worker on its configured host and port — NOT on a port you pick. The default port is `3111` (host `0.0.0.0`), so a route with path `/example` is reachable at `http://localhost:3111/example` on a default deployment. Do NOT guess other ports (3000, 8080, the engine's WS port): if the deployment overrides the default, read the live host/port from the `iii-http` worker's configuration (`config.yaml`) or its status rather than probing. Verify a live endpoint with `web::fetch` against that base URL, never `curl`.

## Middleware

Register a middleware function when the same logic must run before multiple routes — authentication, rate limiting, request logging, header normalization. A middleware is a regular registered function that receives a subset of the request (without the body) and either lets the request proceed (optionally enriching its context) or short-circuits with an immediate response. Target middleware two ways:

- Globally — via the worker's middleware config in `config.yaml`; runs on every matched route, in priority order.
- Per-route — via the trigger's middleware list; runs after the global chain, in the order given.

The full preHandler order per request: route match → global middleware → route gating function → per-route middleware → body parsing → handler.

## Configuration

The worker is configured in `config.yaml`: the listen host and port, request timeout, concurrency and body-size limits, proxy trust and request-id header, trailing-slash handling, a not-found handler function, CORS, and the global middleware chain. For the full set of keys and their defaults, read the worker's configuration rather than hardcoding values from memory.