Docs menu
Studio — inspector & control plane
A web UI + JSON API that observes the journal: run timeline/state/diff/trace/cost, a live Playground, approvals, metrics, threads, organizations, and budget management.
What it's for / when to use it#
Studio is an inspection + control plane built on top of the @gnldev/durable journal: it shows the timeline of runs, the step-by-step materialized state (state/diff), an OTel-like trace, and cost; lets you run agents from the browser (Playground) and review/approve pending tool approvals; and manages threads (memory), workflows, organizations, and budgets. It's used when you need things like "why was this run suspended", "how long did each model step take", or "let me try the agent in the browser before shipping it to prod" — instead of building this visibility from scratch in your own backend. Most views (Playground, Memory, Workflows, Scorers, Users, Organizations) are optional: each one only turns on if the matching option is provided, otherwise it's silently hidden (GET /capabilities is how the UI discovers which views to show).
Setup / import#
The core exports come from the @gnldev/studio root; converting the Playground's tool schema to JSON Schema needs a separate sub-export (this keeps the dependency on the ai package isolated from the studio core):
import { createStudioApp, createStudioRunner } from '@gnldev/studio';
import { aiToolSchema } from '@gnldev/studio/ai';Step-by-step usage#
Simplest setup: mount createStudioApp at a path on your own Hono/Node app. This combines both the JSON API (/api/*) and the compiled React interface (@gnldev/studio-ui) into a single app — apiBase tells the interface the API prefix it should fetch from (it must match the path you mounted it at):
const gnl = createGnl(config);
app.route('/studio', createStudioApp({
reader: toJournal(storage.runs),
apiBase: '/studio',
gnl: createStudioRunner(gnl, config, { toJsonSchema: aiToolSchema }),
auth,
org: {},
budgets: { default: { tokenLimit: 100 } },
users: userStore,
}));reader is required and drives the journal view (toJournal(storage.runs) derives it from your existing storage). If gnl is not provided, the Playground/Tools/Workflows views are hidden — when it's provided, createStudioRunner takes your createGnl instance and config and wraps them into a runner that can be executed from the browser (non-sensitive metadata such as the agent list, tool schemas, and model name leaks out; the model object itself never leaks). If auth is not provided, all endpoints are unrestricted; if provided (free roleAuth or paid @gnldev/auth-ee), read/write endpoints are gated. org: {} turns on multi-org read inspection (the default resolver is the x-gnl-org header); budgets sets the default/per-org limits editable in the Organizations panel; users turns on Studio's "Users" view.
If you only need the JSON API (no UI, you'll build your own interface) or only need a statically/separately served HTML UI, you can split the same options across two separate functions:
app.route('/studio/api', createStudioApi({ reader, gnl, auth })); // JSON only
app.route('/studio', createStudioAdmin({ apiBase: '/studio' })); // HTML UI only (it points at a remote API)Once mounted, the REST endpoints can also be used directly — e.g. for a run's cost or trace:
GET /studio/api/runs/:id/state?step=3 → the materialized state at step 3 (reconstructState)
GET /studio/api/runs/:id/cost → getRunCost(reader, id)
GET /studio/api/runs/:id/trace → waterfall span'leri (maliyet dahil)
GET /studio/api/approvals → pending tool approvals across ALL suspended runs (inbox)Dead-letter — what is parked, and putting it back#
Pass events and the Dead-letter view turns on, along with GET /dead-events/topics, GET /dead-events and POST /dead-events/release. It is the same wrap-your-own-store pattern as queue: StudioEvents is a host-supplied bridge, so Studio takes no dependency on @gnldev/events and works just as well over your own store.
Everything is addressed by the triple (topic, consumer, id). A topic fans out, so one event has one quarantine record per consumer — an id alone names several of them, which is why half a triple is a 400 rather than a lookup that quietly misses.
import { listDeadEvents, retryDeadEvent } from '@gnldev/events';
createStudioApp({
reader: toJournal(storage.runs),
events: {
orgScoped: true, // the host declaring it honours the ctx.orgId it is handed
topics: () => listTopics(storage.work),
listDead: (topic, consumer, ctx) => listDeadEvents(storage.work, topic, consumer),
release: (topic, consumer, id) => retryDeadEvent(storage.work, topic, consumer, id),
},
});Three things are part of the surface rather than bolted onto it. The event body is withheld unless the caller both asks for it and holds payloads:read; the handler's error text is withheld on the same permission and needs no asking, because it ships in the default answer. And the scan is expensive — it reads a whole topic log — so one runs at a time deployment-wide and the rest queue, with a Retry-After measured from this deployment's own completed scans rather than a constant.
Releasing is idempotent: handing back an already-released event is a documented operation, not a 409. Only a delivered event is terminal. Each release is written to the audit trail as event.release.
API reference#
createStudioAppAdmin HTML UI + JSON API (/api/*) in one app — the easiest backwards-compatible setup.
createStudioApiJSON API only (no UI); for embedding into your own mount/auth/programmatic flow.
createStudioAdminHTML UI only; points at a locally or remotely running Studio API via apiBase.
createStudioRunnerDerives a Playground/Tools/Workflows runner (StudioAgentRunner) from a createGnl instance.
aiToolSchemaThe @gnldev/studio/ai sub-export — converts a zod schema to JSON Schema (for the Tools view form).
deadEventScanTunes the dead-letter scan: timeoutMs (how long the host's scan may take), queueWaitMs and queueDepth (how many callers may wait), maxAbandonedScans (stop opening new reads against a store that is not answering).
StudioApiOptionsInput for createStudioApi/Runner: reader (required) + resume/chat/gnl/memory/workflows/scorers/datasets/mcp/queue/events/deadEventScan/cache/vectors/users/auth/org/budgets/retention/evalGate/alerts (all optional).
StudioAppOptionsStudioApiOptions + apiBase (the API prefix the admin HTML fetches from).
GET /capabilities returns true only for the options that were actually provided (e.g. if gnl is absent, playground: false). The UI adapts itself using this endpoint before the login screen; you can also use it in your own integration to verify which surfaces are enabled.users (the Users view + /users endpoints) is a paid feature — its implementation (createJournalUserStore) lives in @gnldev/auth-ee; if you don't provide this option on the free tier, the view is silently hidden. Similarly, Organizations/Budget management is only visible when multi-org is enabled (either org is provided, or the auth provider reports multiOrganization).