Skip to content

HTTP API reference

Every route is registered under the /api prefix (createServer mounts aiRoutesPlugin with prefix: '/api'). Request bodies and responses are validated against Zod schemas derived from your providers; validation failures return the structured error described at the end.

Method Path Purpose
GET /api/health Liveness check.
GET /api/client/schema The client manifest — every provider’s schemas and metadata.
POST /api/providers/:providerId/actions/:actionId Invoke an action.
POST /api/providers/:providerId/entities/:entityId Search an entity.
GET /api/providers/:providerId/entities/:entityId/:recordId Fetch one record by id.
GET /api/buckets List the exposed buckets.
GET /api/buckets/:bucketId List objects in a bucket.
GET /api/buckets/:bucketId/* Read an object’s bytes.
HEAD /api/buckets/:bucketId/* An object’s size, content type, and metadata.
PUT /api/buckets/:bucketId/* Write an object.
DELETE /api/buckets/:bucketId/* Delete an object.
POST /api/nlp/classify Match a phrase against declared intents.
POST /api/nlp/execute Run an intent returned by classify.
POST /api/eval Run a script in the sandbox.
GET /api/eval/actions List actions callable from scripts.
GET /api/eval/actions/:functionName One action’s detail and typings.
GET /api/docs Interactive Scalar/OpenAPI reference.

Provider routes exist only for providers registered on the host; the action, entity, and record ids are those the provider defines. Bucket routes exist only for buckets a provider registered and marked exposed — see Store blobs in a bucket.


Liveness check. No parameters, no body.

Response 200

{ "status": "ok" }

The client manifest — the host’s machine-readable self-description. For every registered provider it lists the actions, entities, and events, each with metadata and JSON Schemas, and each action’s allocated functionName. This is the source @grundlag/client compiles into a typed client and a runtime tool set.

Request body — none.

Response 200

{
"providers": {
"notes": {
"actions": {
"create": {
"id": "create",
"name": "Create note",
"functionName": "notesCreate",
"input": { "type": "object", "properties": { "title": { "type": "string" } } },
"output": { "type": "object", "properties": { "id": { "type": "string" } } }
}
},
"entities": {},
"events": {}
}
}
}

Each entity entry carries searchInput and data schemas; each event entry carries payload and filter schemas.


POST /api/providers/:providerId/actions/:actionId

Section titled “POST /api/providers/:providerId/actions/:actionId”

Invoke a provider action.

Path params

Param Description
providerId Registered provider id.
actionId Action id within that provider.

Request body

Field Type Required Description
input the action’s input schema yes The action input.
resumption { data, state? } no Answer to a prior interrupt; state is the checkpoint the interrupt carried.
items array of item snapshots no The caller’s current conversation items, seeding the action’s latest lookups.

Response 200 — an envelope covering both outcomes:

Field Type Description
status "completed" | "interrupted" Whether the action finished or paused for a decision.
result the action’s output schema Present when completed.
message string Present when interrupted — the interrupt’s message.
data unknown Present when interrupted — what the caller must decide on.
state unknown Present when interrupted — opaque checkpoint; pass back as resumption.state.
items array of item snapshots The items the call emitted.

An interrupt is returned as a normal 200 with status: "interrupted" — it is not an error status.

Errors400 validation error if the body does not match the request schema; the action’s execute never runs. A 500 serialization error if the action returns a value that does not match its output schema.


POST /api/providers/:providerId/entities/:entityId

Section titled “POST /api/providers/:providerId/entities/:entityId”

Search an entity.

Path params

Param Description
providerId Registered provider id.
entityId Entity id within that provider.

Request body

Field Type Required Description
input entity’s searchInput schema yes The search criteria.
cursor string no Opaque pagination cursor from a prior response.
limit number no Maximum results to return.

Response 200

Field Type Description
results array of the entity’s data schema The matching records.
nextCursor string (optional) Pass back as cursor for the next page; absent when there are no more pages.

Errors400 validation error if the body does not match the schema.


GET /api/providers/:providerId/entities/:entityId/:recordId

Section titled “GET /api/providers/:providerId/entities/:entityId/:recordId”

Fetch a single record by id.

Path params

Param Description
providerId Registered provider id.
entityId Entity id within that provider.
recordId The record’s id.

Request body — none.

Response 200 — one record, shaped by the entity’s data schema.


List the buckets exposed over HTTP.

Request body — none.

Response 200

{ "buckets": [{ "id": "reports" }] }

List the objects in a bucket, ordered by key.

Path params

Param Description
bucketId Exposed bucket id.

Query params

Param Type Required Description
prefix string no Only keys starting with this prefix.

Response 200

{
"objects": [
{
"key": "monthly/june.md",
"size": 6,
"contentType": "text/markdown",
"metadata": { "author": "morten" },
"updatedAt": "2026-06-30T12:00:00.000Z"
}
]
}

Read an object’s bytes. The key is the whole trailing path, so /api/buckets/reports/monthly/june.md addresses the key monthly/june.md.

Response 200 — the raw bytes, with Content-Type set to the object’s content type (application/octet-stream when it has none) and Last-Modified to its write time.

Errors404 when no object is stored under the key; 400 when the key is malformed (see key rules).


An object’s metadata, without its body.

Response 200 — no body. Content-Length is the object’s size, Content-Type its content type, Last-Modified its write time, and each metadata entry is returned as x-bucket-meta-<name>.

Errors404 when the object does not exist.


Write an object, replacing anything already stored under the key.

Request body — the bytes to store, whatever the content type. Bodies are capped at 32 MiB.

Request headers

Header Description
Content-Type Becomes the object’s content type.
x-bucket-meta-<name> Becomes the metadata entry <name>.

Both are replaced wholesale by each write: a write that sends no metadata clears what the previous write stored.

Response 200 — the resulting object, same shape as one entry of the listing above.

Errors400 when the key is malformed.


Delete an object.

Response 204 — no body. Idempotent: deleting a key that holds nothing succeeds.

Errors400 when the key is malformed.


Match a phrase against the utterances every registered provider declared — the cheap first pass, so an unambiguous request need not reach a model. See Match spoken phrases for declaring them.

The corpus is trained on first use from the providers’ live slot values and rebuilt whenever a slot reports a change, so this reflects the current state of the system rather than what was true at boot.

Request body

{ "text": "turn off the kitchen light", "minScore": 0.9 }

minScore is optional and overrides the host’s own floor for this call — lower it to inspect weak matches, raise it to be stricter.

Response 200

{
"match": {
"score": 0.99,
"text": "turn off the kitchen light",
"intent": {
"id": "5f6d…",
"name": "Turn lights off",
"providerId": "home-assistant",
"entities": {
"a1b2…": [{ "value": { "id": "room-1", "name": "Kitchen" }, "text": "the kitchen", "accuracy": 1 }]
}
}
}
}

match is null whenever nothing cleared the floor — the ordinary case, and not an error. A caller should read it as “ask the model”.

The split is deliberate. score and text describe how the match was arrived at; intent is the executable part, and it is exactly what /api/nlp/execute takes back. Deciding whether a match is strong enough happens once, here.

score is not calibrated, and a threshold does not transfer between hosts: unrelated input scores around 0.5 against a corpus of two slot-bearing intents but above 0.9 once a short slot-free intent joins it. Probe your own corpus with phrases that should be rejected before settling on a floor.

id is the matched intent’s generated id and name its display name. Ids are regenerated each time the host starts, so persist name rather than id if a record has to outlive the process.

entities is keyed by slot id, and every key holds an array — one phrase can fill a single slot several times over (“the kitchen and the hallway lights”). Each entry’s value is the provider’s own object for the match, not the words that matched it; text is those words. A slot the phrase left unfilled is absent.

Errors400 when text is empty.


Run an intent that /api/nlp/classify returned.

Request body — the match.intent from a classify response, unchanged:

{
"intent": {
"id": "5f6d…",
"name": "Turn lights off",
"providerId": "home-assistant",
"entities": {
"a1b2…": [{ "value": { "id": "room-1", "name": "Kitchen" }, "text": "the kitchen", "accuracy": 1 }]
}
}
}

No score is asked for, and sending one changes nothing. Whether the match was good enough is settled against the score classify returned, before calling this.

Response 200

{ "status": "completed" }

An intent that raises an interrupt answers { "status": "interrupted", "message": "…" } with a 200, because needing a human is an outcome rather than a failure. Unlike an action, there is no resumption channel — the intent cannot be continued once answered.

Errors404 when the intent id names nothing, which is what a match held across a restart looks like, since ids are minted per process. 400 when the intent is malformed. Anything the intent itself throws surfaces on its own terms — an action reached through it answers 400 with the offending fields if the mapping does not fit its input schema.


Run a TypeScript script in a sandbox built from every registered provider’s actions.

Request body

Field Type Required Description
script string yes The TypeScript source to run.
data object (record of unknown) yes Initial data made available to the script.
callLog array of call-log entries (each nullable) no A call log from a prior interrupted run, to replay deterministically.
resumption { position, data, state? } no The answer to a prior interrupted run. position and state come from that run’s interrupt object; data is the answer handed to the paused call. Pass it together with the prior callLog.

Call-log entries may be null in a resumed log: an interrupted call reserves its position without recording while concurrent calls complete behind it.

Response 200

Field Type Description
status "completed" | "interrupted" "completed" if the script finished; "interrupted" if an action paused the run.
data object (record of unknown) The script’s resulting data.
callLog array of call-log entries (each nullable) The recorded call log; pass it back to resume.
console array of console entries Everything the script logged.
interrupt { position, data?, state? } (optional) Present only when status is "interrupted". position is the paused call’s slot in the call log; data is the action’s caller-facing payload (what to decide on); state is its opaque checkpoint. Pass all three back under resumption to resume.

An interrupt is returned as a normal 200 with status: "interrupted" — it is not an error status. To resume, call again with the same script, the returned callLog, and a resumption built from the interrupt object (position, state) plus your answer (data).


List the actions callable from eval scripts.

Request body — none.

Response 200 — array of:

Field Type Description
functionName string The name the script calls the action by.
name string Human-readable action name.
description string (optional) Action description.

Detail for one callable action, including generated TypeScript typings.

Path params

Param Description
functionName The action’s function name (from the list route).

Response 200

Field Type Description
functionName string The name the script calls the action by.
name string Human-readable action name.
description string (optional) Action description.
typings string Generated TypeScript declaration for the function’s input and output.

Errors500 if no action matches functionName.


Interactive Scalar/OpenAPI reference for the host, served in the browser. The OpenAPI document is generated from the same Zod schemas that validate the routes above.


Request-validation and response-serialization failures return a structured JSON body rather than an opaque payload.

Request validation — 400

{
"error": "Validation Error",
"message": "Request does not match the expected schema — `title`: Required",
"statusCode": 400,
"details": {
"location": "request",
"method": "POST",
"url": "/api/providers/notes/actions/create",
"issues": [{ "path": "title", "message": "Required", "code": "invalid_type" }]
}
}

Response serialization — 500 — same shape with error: "Response Serialization Error", statusCode: 500, and details.location: "response". This means the server produced a value that did not match its declared response schema.

issues entries

Field Type Description
path string Dotted path to the offending field; (root) for the top level.
message string Why it failed.
code string The validation issue code.
expected unknown (optional) Expected value or type, when applicable.
received unknown (optional) Received value or type, when applicable.

Two storage failures carry their own shape, since neither is the server’s fault: a missing object returns 404 with error: "Not Found" and details: { bucket, key }, and a malformed object key returns 400 with error: "Invalid Key" and details: { key, reason }.

Other errors that are not schema failures propagate as thrown errors and surface as a standard 500.