GNL
Docs menu
Core · Free@gnldev/server

Automatic REST API + OpenAPI + SSE

Turns a createGnl configuration into a durable HTTP API in one line: /agents/:name/run|resume|stream (SSE), /workflows, /runs, /usage, and a generated /openapi.json.

What it's for / when to use it#

If you already have a createGnl configuration (agents + workflows + storage), you don't need to hand-write endpoints to expose it as an HTTP API. createRestApi reads the config and automatically generates run, resume, SSE streaming, run history, and usage endpoints — since it all flows through runDurable, exactly-once and durability guarantees come for free.

Typical scenario: wire it into a Hono app with app.route('/api', createRestApi(config, opts)) and point a client (web, CLI, another service) directly at these endpoints. Auth, multi-org isolation, and budget/quota are optional — if you don't pass them, the endpoints are open and single-org (as shown below).

Setup / import#

The package is @gnldev/server; for journal/storage it uses @gnldev/durable and its sub-export @gnldev/durable/sqlite.

npm install
npm install @gnldev/server @gnldev/durable hono
create config
import { Hono } from 'hono';
import { createRestApi } from '@gnldev/server';
import { createGnl } from '@gnldev/durable';
import { SqliteStorage } from '@gnldev/durable/sqlite';

const storage = new SqliteStorage('runs.db');
const config = {
  storage,
  agents: {
    starwars: { model: buildModel(), system: SYSTEM, tools: makeSwapiTools(), maxSteps: 4 },
  },
};

Step-by-step usage#

Once the config is ready, createRestApi(config, opts) returns a Hono router; mount it at any path you want (e.g. /api).

app.ts
const app = new Hono();
app.route('/api', createRestApi(config, { title: 'SWAPI Free', auth }));

// POST /api/agents/starwars/run     {"runId":"demo-1","prompt":"..."}
// POST /api/agents/starwars/stream  -> SSE (text-delta/tool-call/tool-result/interrupt/done)
// GET  /api/usage                   -> token/maliyet raporu

A run request requires runId — it's the idempotency key for the exactly-once guarantee; POSTing again with the same runId won't re-run tool calls that already completed.

curl — run
curl -s -X POST http://localhost:3001/api/agents/starwars/run \
  -H 'content-type: application/json' \
  -d '{"runId":"demo-1","prompt":"Who is Luke Skywalker?"}'

For streaming, POST the same body to /agents/:name/stream — the response is an SSE stream (text-delta, tool-call, tool-result, interrupt, done events). If a tool is suspended pending human approval (interrupt), continue it by POSTing { runId, approvals } to /agents/:name/resume with the same runId — the input (prompt/messages) is read from the journal, so you don't need to pass it again.

For a usage/cost report, use GET /usage:

curl — usage
curl -s http://localhost:3001/api/usage
# {"org":null,"usage":{...},"limit":null,"exceeded":false}

The generated OpenAPI 3.1 schema is served at GET /openapi.json — run/resume/stream paths are automatically added for every registered agent and workflow; you can feed this directly into tools like Swagger/Redoc.

API reference#

fncreateRestApi

Generates a Hono router from a createGnl config: /agents/:name/run|resume|stream, /agents, /workflows/:name/run, /workflows, /runs, /runs/:id, /usage, /openapi.json.

typeRestApiOptions

Second argument to createRestApi: title (OpenAPI title), auth (optional AuthProvider/ReadWriteAuth), org (opt-in multi-org isolation), budgets (fallback budget/quota limits).

fnbuildOpenApi

Generates an OpenAPI 3.1 schema from the list of agent and workflow names; createRestApi calls this automatically at the /openapi.json endpoint, and it can also be imported directly.

fnpipeAgentStream

Pumps a streamDurable result (AI SDK StreamTextResult) into SSE: text-delta/tool-call/tool-result/error/interrupt/done events.

fninterruptsFromSteps

Extracts suspended tool calls (Interrupt[]) from a list of completed steps — the same logic as the suspend handling in runDurable; pipeAgentStream uses this for the interrupt event at the end of the stream.

fnstreamDurable

Comes from @gnldev/durable (used indirectly inside createRestApi.gnl.stream()): a durable + streaming run — the same exactly-once/suspend guarantees, with the result streamed as fullStream.

Tip
If you don't pass auth, all endpoints are OPEN (existing behavior is preserved). In production it's recommended to pass at least an auth via roleAuth (@gnldev/auth) — GET endpoints require read, POST endpoints require write permission.
Idempotency
Every run request requires runId — this is the key to the exactly-once guarantee. Retrying with the same runId is NOT a new job — it counts as a continuation/retry of the existing one (completed tools won't re-run; if a job is suspended, the budget gate is skipped too).