Agent protocol
Every Stackbone agent must serve POST /invoke, GET /health and GET /schema.
Every Stackbone agent must serve these three HTTP endpoints. They are the contract between your container and the rest of the platform — they're what
stackbone devexercises locally and what the production runtime expects in the cloud. The contract is framework-agnostic: the official templates use Hono, but you can use Express, Fastify, plain Node, or any other server.The full wire spec for the Studio API (the broader surface orchestrator + cloud serve, e.g.
/api/runs,/api/contract) lives indocs/arquitectura/specs/stackbone-agent-protocol-v1.md. This page covers only the three endpoints the agent itself must implement.
Why these three?
| Endpoint | Purpose |
|---|---|
/invoke |
The agent's main entrypoint — every external trigger (webhook, scheduled run, manual run, queue) lands here. |
/health |
Liveness probe. The runtime waits on this during boot; if it never returns 200 the deploy is rolled back. |
/schema |
Self-description: input, output, optional configSchema. The runtime uses it to validate inputs and Studio uses it to render forms and config editors. |
Minimal agent
The hello-world template is the canonical reference:
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
const app = new Hono();
app.post('/invoke', async (c) => {
const body = await c.req.json().catch(() => ({}));
return c.json({ hello: body.who ?? 'world' });
});
app.get('/health', (c) => c.json({ status: 'ok' }));
app.get('/schema', (c) =>
c.json({
input: { type: 'object', properties: { who: { type: 'string' } } },
output: { type: 'object', properties: { hello: { type: 'string' } } },
}),
);
const port = Number(process.env.PORT ?? 8080);
serve({ fetch: app.fetch, port });POST /invoke
The agent's main entrypoint.
- Method:
POST. - Content-Type:
application/jsonfor synchronous results, ortext/event-streamfor SSE streams (see below). - Body: any JSON shape — your agent decides. Validate against your
/schemainput. - Response: any JSON shape — again, your agent decides. Match it to
your
/schemaoutput.
Errors should use HTTP status codes. The Studio UI surfaces both
the status and the body, so a 4xx with a structured body is more useful
to the operator than a 200 with { "error": "..." }.
Streaming (SSE)
For long-running invocations (LLM streams, multi-step reasoning), respond with SSE:
app.post('/invoke', (c) => {
return streamSSE(c, async (stream) => {
await stream.writeSSE({ event: 'token', data: '...' });
await stream.writeSSE({ event: 'done', data: '...' });
});
});TODO — formal event taxonomy (
token,tool_call,tool_result,done,error) once Epic 5 (official streaming templates) lands.
Auth
In production the runtime ensures only the platform can reach /invoke
(network policy + JWT). The emulator does not enforce auth — anything
with the agent port can hit it. Don't expose the emulator's agent port
publicly.
GET /health
Liveness probe. Return 200 { "status": "ok" } (any extra fields are
fine; the runtime only checks the status code).
app.get('/health', (c) => c.json({ status: 'ok' }));The runtime polls /health during startup and during steady-state
operation. If the probe fails for too long the container is recycled.
TODO — formal definition of the failure window (timeout, retries, recycling threshold) lands with Epic 5 of the runtime component.
GET /schema
Self-description used by the runtime (input validation) and by Studio (form rendering). Minimum shape:
{
"input": {
/* JSON Schema for /invoke request body */
},
"output": {
/* JSON Schema for /invoke response body */
},
}Optional fields:
configSchema— JSON Schema for the value Studio's "Config dinámica" panel writes intoAGENT_CONFIG. When present, Studio renders a typed form. When absent, free-form JSON is accepted. Seedocs/arquitectura/componentes/11-stackbone-studio.mdfor howconfigSchemaflows through the platform.
{
"input": {
"type": "object",
"required": ["goal"],
"properties": { "goal": { "type": "string" } },
},
"output": { "type": "object", "properties": { "answer": { "type": "string" } } },
"configSchema": {
"type": "object",
"properties": {
"tone": { "type": "string", "enum": ["formal", "casual"] },
"maxIterations": { "type": "integer", "minimum": 1, "maximum": 20 },
},
},
}Optional endpoints
| Endpoint | Purpose | Status |
|---|---|---|
POST /checkpoint/save, /checkpoint/load |
LangGraph-style checkpointing for resumable workflows. The runtime persists the blob between calls. | TODO (V1) |
Webhook receivers (e.g. POST /approvals/...) |
Verify HMAC-signed callbacks from the platform (HITL approvals, queue jobs, …). Owned by the agent code, not the platform. | Stable (in @stackbone/sdk) |
Port and binding
The runtime injects PORT into the container. Bind your server to it:
const port = Number(process.env.PORT ?? 8080);
serve({ fetch: app.fetch, port });Bind to 0.0.0.0 inside the container — the platform's network
policy controls which traffic can actually reach it. Locally, stackbone dev passes the agent process the auto-picked port and proxies /invoke
through the Studio API at http://127.0.0.1:4242.