# Flue HTTP API reference

> Mounted routes (`/agents`, `/workflows`, `/runs`, `/channels`, `/openapi.json`), admission semantics, stream reads, and error envelopes.

- Repository: withastro/flue-with-vercel-eve
- GitHub: https://github.com/withastro/flue
- Human docs: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6
- Complete Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/llms-full.txt

## Source Files

- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/handle-agent.ts`
- `withastro-flue:packages/runtime/src/errors.ts`
- `withastro-flue:packages/runtime/test/workflow-runs.test.ts`
- `withastro-flue:packages/sdk/src/index.ts`
- `withastro-flue:examples/hello-world/src/app.ts`

---

---
title: "Flue HTTP API reference"
description: "Mounted routes (`/agents`, `/workflows`, `/runs`, `/channels`, `/openapi.json`), admission semantics, stream reads, and error envelopes."
---

`flue()` from `@flue/runtime/routing` returns a mountable Hono sub-app. Mount it at any prefix in `app.ts` (commonly `/` or `/api`); all paths below are relative to that prefix. The generated runtime calls `configureFlueRuntime()` before serving, which wires agent handlers, workflow handlers, channel routes, run stores, and error rendering.

| Route | Methods | Role |
| --- | --- | --- |
| `/openapi.json` | `GET` | OpenAPI 3 document for agent and workflow invocation |
| `/agents/:name/:id` | `POST`, `GET`, `HEAD` | Prompt an agent instance; read its Durable Streams event log |
| `/workflows/:name` | `POST` | Start a workflow run |
| `/runs/:runId` | `GET`, `HEAD` | Read workflow-run events; `?meta` returns run record JSON |
| `/channels/:name/*` | Per-channel | Verified webhook ingress (method + suffix dispatch) |

Only agents and workflows that opt into HTTP transport appear in the manifest and are invocable. Unmatched paths outside `flue()` fall through to your outer Hono app or `createDefaultFlueApp()`'s `RouteNotFoundError` envelope.

## Mounting

```typescript
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';

const app = new Hono();
app.route('/', flue()); // routes live at /agents/..., /workflows/..., etc.
export default app;
```

With a prefix mount (`app.route('/api', flue())`), admission responses derive absolute `streamUrl` values under that prefix — for example `http://host/api/runs/run_01…` for workflow runs admitted at `/api/workflows/daily-report`.

<Note>
`createDefaultFlueApp()` mounts `flue()` at `/` and applies canonical error envelopes for unmatched routes. Custom `app.ts` projects compose `flue()` alongside their own routes and middleware.
</Note>

## Invocation modes

Agent and workflow `POST` routes share one query contract:

<ParamField query="wait" type="'result'">
When set to `result`, the server waits for terminal completion and returns **200** with a synchronous JSON envelope. Omit for default **202** admission.
</ParamField>

Invalid `wait` values (for example `?wait=results`) return **400** `invalid_request`.

### Admission (202)

Default mode admits work and returns stream coordinates immediately. The handler continues in the background; the HTTP response does not wait for model output or workflow completion.

<ResponseField name="streamUrl" type="string">
Absolute URL for reading events. Agent prompts stream at the same URL; workflow runs stream at sibling `/runs/:runId` under the mount prefix.
</ResponseField>

<ResponseField name="offset" type="string">
Opaque Durable Streams offset captured at admission. Reading from this offset replays events from the start of the admitted interaction or run. First prompt on a fresh agent instance is typically `-1`.
</ResponseField>

<ResponseField name="runId" type="string">
Present on workflow admissions only. Format: `run_` + ULID (for example `run_01HJKMNP-TV-Z26CHARS`).
</ResponseField>

<ResponseField name="submissionId" type="string">
Present on agent admissions only. Correlates the admitted prompt with attached-agent events.
</ResponseField>

Admission responses also set DS stream-creation headers:

- `Location` — same value as `streamUrl`
- `Stream-Next-Offset` — same value as `offset`

<RequestExample>

```bash
curl -X POST http://localhost:3000/workflows/daily-report \
  -H 'Content-Type: application/json' \
  -d '{"report":"weekly"}'
```

</RequestExample>

<ResponseExample>

```json
{
  "runId": "run_01HJKMNP-TV-Z26CHARS",
  "streamUrl": "http://localhost:3000/runs/run_01HJKMNP-TV-Z26CHARS",
  "offset": "-1"
}
```

Status **202**. Headers include `Location` and `Stream-Next-Offset`.

</ResponseExample>

### Synchronous (`?wait=result`)

Waits for terminal completion, then returns **200** JSON:

| Field | Agents | Workflows |
| --- | --- | --- |
| `result` | Terminal prompt result (`null` if handler returns `undefined`) | Terminal workflow return value |
| `streamUrl` | Request URL (query stripped) | `/runs/:runId` under mount prefix |
| `offset` | Offset at admission | Offset at admission |
| `runId` | — | Generated run id |
| `submissionId` | Admission receipt | — |

Session-layer `FlueError` subclasses that escape during `?wait=result` (for example `model_not_configured`) render as typed **500** envelopes, not opaque `internal_error`.

Workflow admission requires a configured run store. Missing store returns **501** `run_store_unavailable` before the handler executes.

## Agents

:::endpoint POST /agents/:name/:id
Prompt a persistent agent instance. Body is required JSON. Default returns **202** admission; `?wait=result` blocks until the prompt completes.
:::

<ParamField path="name" type="string" required>
Agent module name from `agents/<name>.ts`. Must be HTTP-exposed in the build manifest.
</ParamField>

<ParamField path="id" type="string" required>
Caller-chosen instance id (for example a user or session key). Empty segments are rejected.
</ParamField>

<ParamField body="message" type="string" required>
Prompt text.
</ParamField>

<ParamField body="images" type="array">
Optional image attachments. Each item: `{ "type": "image", "data": string, "mimeType": string }`. `data` max length 14 MiB characters.
</ParamField>

<RequestExample>

```bash
curl -X POST http://localhost:3000/agents/assistant/customer-123 \
  -H 'Content-Type: application/json' \
  -d '{"message":"Summarize ticket #42"}'
```

</RequestExample>

<ResponseExample>

```json
{
  "streamUrl": "http://localhost:3000/agents/assistant/customer-123",
  "offset": "-1",
  "submissionId": "submission-abc"
}
```

</ResponseExample>

Allowed methods: `POST`, `GET`, `HEAD`. Other methods return **405** with `Allow: GET, HEAD, POST`.

`agentRouteMiddleware` runs for all three methods, so auth and rate limits apply to stream reads as well as prompts.

<Tip>
Use `dispatch()` from application code for asynchronous delivery to a continuing session without creating workflow-run history. HTTP prompts are attached interactions with durable admission on supported targets.
</Tip>

## Workflows

:::endpoint POST /workflows/:name
Start an HTTP-exposed workflow. Body is optional JSON (empty body treated as `{}`). Default **202** admission; `?wait=result` for synchronous completion.
:::

<ParamField path="name" type="string" required>
Workflow module name. Must be registered and HTTP-exposed.
</ParamField>

<ParamField body="payload" type="object">
Workflow-defined JSON object. Schema is workflow-specific; OpenAPI documents `additionalProperties: true`.
</ParamField>

Only `POST` is allowed. The server generates `runId`, persists run admission, creates the event stream, starts execution in the background, and returns coordinates.

Workflow handlers receive payload and request via `FlueContextInternal` (`ctx.payload`, `ctx.req`). Handler throws in `?wait=result` mode yield **500** `internal_error` to the client while the run record is finalized as `errored`.

## Runs

:::endpoint GET /runs/:runId
Read workflow-run events via the Durable Streams protocol. `HEAD` returns stream metadata without a body.
:::

:::endpoint GET /runs/:runId?meta
Return plain `RunRecord` JSON (`content-type: application/json`). Stream query params (`offset`, `live`, `tail`) are ignored.
:::

`runId` is opaque; owning workflow is resolved through the run store before the response is sent. Workflow `workflowRouteMiddleware` runs **before** existence is disclosed, so auth boundaries apply even for unknown ids.

| Condition | Result |
| --- | --- |
| Run pointer missing or workflow not in current manifest | **404** `run_not_found` |
| Run store not configured | **501** `run_store_unavailable` |
| Stream not yet created | **404** `run_not_found` (run streams) |

Non-`GET`/`HEAD` methods return **405** `Allow: GET, HEAD`.

Run ids match `^run_[0-9A-HJKMNP-TV-Z]{26}$`.

## Stream reads

Agent (`GET /agents/:name/:id`) and run (`GET /runs/:runId`) endpoints implement the [Durable Streams protocol](https://github.com/durable-streams/durable-streams/blob/main/PROTOCOL.md): catch-up, long-poll, and SSE. Streams are read-only over HTTP; writes happen internally during execution.

```text
POST /agents/:name/:id  ──admit──►  events appended to agents/:name/:id
POST /workflows/:name   ──admit──►  events appended to runs/:runId
GET  same agent URL     ──read──►   DS catch-up / long-poll / SSE
GET  /runs/:runId       ──read──►   DS catch-up / long-poll / SSE
```

<Warning>
Agent streams return **404** `stream_not_found` until the instance receives its first **admitted** prompt. Failed admission does not create a phantom stream.
</Warning>

### Query parameters

<ParamField query="offset" type="string">
Start position. Values: `-1` (beginning), `now` (tail; requires `live`), or `digits_digits` DS offset. Required when `live` is set.
</ParamField>

<ParamField query="live" type="'long-poll' | 'sse'">
Live tailing mode. `long-poll`: **200** with data or **204** empty (30s timeout). `sse`: `text/event-stream` with `event: data` and `event: control` frames.
</ParamField>

<ParamField query="tail" type="integer">
With `offset=-1`, return only the last N events. Must be ≥ 1.
</ParamField>

<ParamField query="cursor" type="string">
Client cursor for long-poll/SSE control events (`Stream-Cursor` header).
</ParamField>

### Response headers

| Header | Meaning |
| --- | --- |
| `Stream-Next-Offset` | Read cursor for the next request |
| `Stream-Up-To-Date` | `true` when client is at tail |
| `Stream-Closed` | `true` when stream is closed (workflow `run_end` finalized) |
| `Stream-Cursor` | Long-poll/SSE cursor token |
| `ETag` | Conditional caching for catch-up reads |
| `X-Content-Type-Options` | `nosniff` (all stream and error responses) |
| `Cross-Origin-Resource-Policy` | `cross-origin` |

Catch-up **GET** returns `application/json` body: JSON array of event objects. Long-poll abort returns **499** with security headers only.

Use `@flue/sdk` `createFlueClient()` for typed invocation and DS-backed `stream()` / `events()` helpers.

## Channels

:::endpoint ALL /channels/:name/:suffix
Dispatch inbound webhooks to channel-defined handlers. Not described in OpenAPI.
:::

Routes map from `channelHandlers[name]` keys shaped as `"METHOD /suffix"` (for example `"POST /events"`, `"HEAD /webhook"`). The HTTP path after `/channels/:name` must match the suffix.

| Request | Outcome |
| --- | --- |
| `/channels/:name` (no suffix) | **404** `route_not_found` |
| Unknown suffix | **404** `route_not_found` |
| Known suffix, wrong method | **405** with `Allow` listing permitted methods |
| Handler returns non-`Response` | **500** `internal_error` |

Handlers receive Hono `Context` and must return a `Response`. Multi-segment suffixes are supported (`/channels/custom/webhooks/retries` → suffix `webhooks/retries`).

## OpenAPI

:::endpoint GET /openapi.json
Machine-readable contract for public invocation routes. Generated from route metadata via `hono-openapi`.
:::

Documents:

- `POST /agents/{name}/{id}` — `invokeAgent`
- `POST /workflows/{name}` — `invokeWorkflow`

Both operations declare `x-flue-invocation-modes: ["accepted", "wait-result"]` and standard error responses (**400**, **404**, **405**, **415**, **500**, **501**). `info.version` reflects the built runtime version. Stream read and channel routes are not included.

## Error envelopes

Every Flue HTTP error returns JSON:

```json
{
  "error": {
    "type": "agent_not_found",
    "message": "Agent \"missing\" is not registered.",
    "details": "Verify the agent name is correct.",
    "dev": "Available agents: \"assistant\".\n..."
  }
}
```

<ResponseField name="error.type" type="string">
Stable machine-readable identifier (snake_case).
</ResponseField>

<ResponseField name="error.message" type="string">
One-sentence caller-safe summary. Always present.
</ResponseField>

<ResponseField name="error.details" type="string">
Longer caller-safe guidance. Always present; may be empty string.
</ResponseField>

<ResponseField name="error.dev" type="string">
Developer-only hints (available siblings, filesystem layout). Present only when `devMode` is enabled **and** the error class populated a non-empty `dev` field.
</ResponseField>

<ResponseField name="error.meta" type="object">
Structured data on select errors. Rare on HTTP routes.
</ResponseField>

Unknown thrown values log server-side and render **500** `internal_error` without leaking internals.

### HTTP error types

| `type` | Status | When |
| --- | --- | --- |
| `invalid_request` | 400 | Malformed path/query/body validation |
| `invalid_json` | 400 | Body present but not valid JSON |
| `agent_not_found` | 404 | Unknown or non-HTTP agent name |
| `workflow_not_found` | 404 | Unknown, non-HTTP, or internal-only workflow |
| `route_not_found` | 404 | Unmatched Flue route (including channels) |
| `run_not_found` | 404 | Unknown run id or missing run stream |
| `stream_not_found` | 404 | Agent stream before first admitted prompt |
| `method_not_allowed` | 405 | Wrong HTTP method; includes `Allow` header |
| `unsupported_media_type` | 415 | Body without `application/json` when body expected |
| `run_store_unavailable` | 501 | Run store not wired for workflow admission or lookup |
| `internal_error` | 500 | Unhandled non-Flue error or admission persistence failure |

Non-HTTP `FlueError` subclasses (session, submission, model configuration) that reach the HTTP layer render with their `type` and message at **500**.

Param/query validation failures from Valibot throw `invalid_request` with flattened issue messages — raw validation objects never reach the wire.

## Client SDK

`createFlueClient({ baseUrl, headers })` from `@flue/sdk` maps to the same routes:

| SDK method | HTTP |
| --- | --- |
| `agents.send(name, id, opts)` | `POST /agents/:name/:id` |
| `agents.prompt(name, id, opts)` | `POST /agents/:name/:id?wait=result` |
| `agents.stream(name, id, opts)` | `GET /agents/:name/:id` (DS client) |
| `workflows.invoke(name, opts)` | `POST /workflows/:name` |
| `workflows.invoke(name, { wait: 'result', ... })` | `POST /workflows/:name?wait=result` |
| `runs.get(runId)` | `GET /runs/:runId?meta` |
| `runs.stream(runId, opts)` | `GET /runs/:runId` (DS client) |
| `runs.events(runId, opts)` | Catch-up loop over `GET /runs/:runId` |

The SDK treats `streamUrl` and `offset` from admission responses as opaque — clients must not construct DS offsets themselves.

## Related pages

<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Run `flue dev`, invoke workflows, and connect to agent instances from the CLI.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents, HTTP route middleware, and `createAgent` definitions that back `/agents` routes.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define workflow modules and interpret run events streamed from `/runs/:runId`.
</Card>
<Card title="Flue channels" href="/flue-channels">
Discover channel modules and mount verified webhook routes under `/channels/:name`.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Flue sessions, workflow runs, and dispatch receipts with Eve session contracts.
</Card>
<Card title="Flue persistence reference" href="/flue-persistence-reference">
Run stores, event stream stores, and durable admission adapters behind HTTP execution.
</Card>
</CardGroup>
