LoopOps Quickstart
# LoopOps Quickstart
LoopOps turns production telemetry into an approval-gated improvement loop. (Packages ship as @getloopops/* on npm; loopkit survives only as the internal codename.)
Human? Take the guided path instead of this file: https://loopops.dev/start creates a project, sends your first trace, and opens your console in about two minutes — no email needed. Returning humans enter at https://loopops.dev/dashboard; the bookmarkable signed link from signup still uses /console?t=... so it can be opened without signing in. Sign in with email when you want to keep projects under an account.
Agent? You are the first-class reader. Read a bounded window, synthesize an improvement packet, and stop before any external write. Onboarding an app right now? Follow https://loopops.dev/onboard.md instead — it is this file's content in execution order: provision, wire, first span, first diagnosis, human handoff.
Claude Code: install the LoopOps plugin
Two slash commands inside Claude Code — the plugin registers the hosted MCP server and bundles the loop skill:
/plugin marketplace add codeyogi911/loopkit-codex-plugin
/plugin install loopkit@loopkit
Prefer the pieces separately (or scripting the install)? Same result — the state port and the procedure layer:
claude mcp add --transport http loopops https://mcp.loopops.dev
curl -fsS https://loopops.dev/skill.md --create-dirs -o ~/.claude/skills/loopops/SKILL.md
First MCP use opens a one-time email sign-in. A project created at /start (or via POST /v1/signup) belongs to its keys, not your account — bind it once from inside the agent:
Claim project <project_id> with recovery code <recovery_code>, then diagnose the last hour.
Codex: install the LoopOps plugin
The fastest Codex path is the LoopOps repo marketplace. It installs the hosted MCP server and the workflow guardrails together:
codex plugin marketplace add codeyogi911/loopkit-codex-plugin --ref main
codex
/plugins
Install LoopOps from the plugin browser, start a new thread, and ask:
Use LoopOps to diagnose the latest telemetry window.
On first use, Codex opens a one-time email sign-in for https://mcp.loopops.dev. The MCP read tools belong to your signed-in account; runtime telemetry is still produced with keys over POST /v1/traces.
If you only want the MCP server without the plugin instructions:
[mcp_servers.loopops]
url = "https://mcp.loopops.dev"
0. Get a key (no operator needed)
Agents should prefer the idempotent provision lane — one re-runnable call that converges to the same project for a given name and returns a permanent key:
POST /v1/provision
Content-Type: application/json
{ "project_name": "my-app" }
The response is the signup shape plus resumed and graduated booleans. The first call (no auth) MINTS the project and returns fresh keys, self-graduated to a permanent key. To re-run later, send one of that project's keys to RESUME it — resume is NON-ROTATING, so you keep the key you sent (no new key is returned):
POST /v1/provision
Authorization: Bearer <your producer or agent key>
Content-Type: application/json
{ "project_name": "my-app" }
Resume graduates the project in place; it never rotates or deletes keys. Resuming an existing project WITHOUT one of its own keys is rejected — only the owner can resume it, so a name cannot be hijacked by a caller who merely knows it.
If you just want a throwaway sandbox project each call, use /v1/signup instead:
POST /v1/signup
Content-Type: application/json
{ "project_name": "my-app" }
The response gives you producer_key (ingest), agent_key (read + diagnose), and feedback_key (browser-safe, write-only end-user feedback — see section 3), plus your project_id, quota_spans, expires_at, a recovery_code, and a bookmarkable dashboard_url. Sandbox projects are isolated and span-capped — request a production tenant via /contact.json when you outgrow it.
Save your recovery_code and project_id. Keys are shown once and stored only as hashes — they can't be shown again. If you lose them, rotate fresh ones:
POST /v1/recover
Content-Type: application/json
{ "project_id": "my-app-1a2b3c4d", "recovery_code": "<the code from signup>" }
This mints new keys, revokes the old ones, and returns a fresh dashboard_url. The recovery_code is an opaque, per-project proof-of-ownership token and does not change when keys rotate. Humans can also recover in the browser at /console via the "Lost your key?" form.
If an operator gives you a signed upgrade code, make the sandbox permanent while keeping its span cap:
POST /v1/signup/upgrade
Authorization: Bearer <agent_key>
Content-Type: application/json
{ "code": "<signed hex code>" }
The code is a signed, project-scoped upgrade token issued out-of-band by an operator. Older sandbox projects that predate the token index can include producer_key and agent_key in that body once.
1. Orient
GET /llms.txt— one-line orientation.GET /agents.md— operating instructions and the default loop.GET /openapi.json— the full HTTP surface.
2. Send telemetry
On the Vercel AI SDK? Register ONE integration and every generateText / streamText / tool call flows — no per-call flag, no OpenTelemetry setup, runs anywhere the AI SDK runs (Node, Cloudflare Workers, Edge, Bun, Deno).
AI SDK 7 (recommended):
import { registerTelemetry } from "ai";
import { loopOps } from "@getloopops/sdk/ai";
registerTelemetry(loopOps({ agent: "my-app" })); // reads LOOPOPS_URL + LOOPOPS_INGEST_KEY
Producer policy lives in that one call. Set deploy identity for regression joins, privacy capture flags, and sampling without changing the read-side loop:
registerTelemetry(loopOps({
agent: "my-app",
serviceVersion: process.env.GIT_SHA,
environment: process.env.NODE_ENV,
recordInputs: false,
recordOutputs: true,
sampleRate: 0.25 // also accepts sampling: { type: "ratio", probability: 0.25 }
}));
Fold a conversation into one session via runtimeContext:
await generateText({ model, prompt,
runtimeContext: { thread_id: conversationId },
telemetry: { functionId: "my-app", includeRuntimeContext: { thread_id: true } } });
AI SDK 6 (still the majority), or you already run OpenTelemetry: @getloopops/sdk/otel stands up an OTLP exporter the way Sentry.init() does — Node-only, keeps the per-call flag:
// instrument.mjs -> node --import ./instrument.mjs agent.mjs
import { registerLoopOps } from "@getloopops/sdk/otel";
await registerLoopOps({ agent: "my-app" });
// per call:
import { telemetry } from "@getloopops/sdk/otel";
await generateText({ model, prompt, experimental_telemetry: telemetry({ thread_id: conversationId }) });
Peer deps for the v6 path: npm i @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions.
Adapter and exporter matrix
| Runtime or source | LoopOps wire | Notes |
|---|---|---|
| Vercel AI SDK 7 | @getloopops/sdk/ai → registerTelemetry(loopOps()) | Best default: pure fetch lifecycle hooks, operation-level sampling, runs on Workers/Edge/Node. |
| Vercel AI SDK 6 or existing Node OTel | @getloopops/sdk/otel → registerLoopOps() + telemetry() | Uses OTel OTLP exporter; set sampleRate or LOOPOPS_SAMPLE_RATE. |
| Plain Node OpenTelemetry | @getloopops/otel → initLoopOps() | One-call OTLP exporter for any spans your process already emits. |
| Browser or product UX events | @getloopops/sdk/browser or OTLP logs → /v1/logs | Use the browser-safe feedback_key only for targeted feedback/events. |
| Any language, collector, or framework | OTLP/HTTP JSON or protobuf → /v1/traces | Keep service.name, service.version, and deployment.environment.name on the resource. |
| Mastra or other agent frameworks | Existing OTel exporter/bridge → https://loopops.dev/v1/traces | Keep their native observability; send a bounded copy to LoopOps when you want MCP-readable improvement loops. |
Anything else? POST OTLP JSON span batches straight to /v1/traces with the producer key — any language, any framework:
POST /v1/traces
Authorization: Bearer <producer_key>
Content-Type: application/json
{
"resourceSpans": [{
"scopeSpans": [{
"spans": [{
"traceId": "trace_1",
"spanId": "span_1",
"name": "tool_call_failed",
"startTimeUnixNano": "1781856000000000000",
"status": { "code": 2, "message": "tool failed" }
}]
}]
}]
}
LoopOps accepts OTLP JSON span batches at POST /v1/traces with the same producer key.
First-event proof
After wiring a producer, prove the path before waiting for real traffic:
import { emit, waitForTelemetry, diagnose } from "@getloopops/sdk";
await emit({ name: "loopops.first_event", sessionId: "smoke", status: "ok" });
await waitForTelemetry({ minRecords: 1, timeoutMs: 180_000 });
const packet = await diagnose({ sinceMinutes: 60 });
console.log(packet.telemetry_read, packet.window_metrics);
The lake read path can lag ingest by about 60-90 seconds. waitForTelemetry() polls the bounded diagnose window until the first record is readable, so an agent can prove wiring without opening an unbounded telemetry read.
Make failures replayable (recommended). Units that also carry the capture contract — unit (llm_call | tool_call | graph_node | retrieval), input, output, model (llm_call), expected when known — become replayable evidence: diagnose clusters report replayable_count > 0, prompt fixes can be proven BEFORE shipping (loopops_replay), and tool/graph fixes get executable eval specs (loopops_eval_spec). Apps already emitting gen_ai.* semconv (or Vercel AI SDK telemetry) get this with zero producer changes — the worker derives the capture block. With @getloopops/sdk's emit(), just add the fields to the event.
3. Capture user feedback (typed or spoken)
Feedback is telemetry: it lands next to the session/trace it grades, and the same loop that clusters failures clusters bad feedback — negative feedback becomes bug candidates, directional asks become idea candidates whose text IS the spec. Signup/provision responses include a feedback_key (lk_feedback_*): write-only and browser-safe — it can submit feedback and nothing else, so shipping it client-side is fine.
Typed — target one of session_id/trace_id/span_id, plus at least one valence: score (0..1), polarity (up|down), rating (1..5), comment, or expected ("what it should have said" — a golden-answer seed):
POST /v1/feedback
Authorization: Bearer <feedback_key>
Content-Type: application/json
{ "session_id": "sess_abc", "polarity": "down", "labels": ["export"],
"comment": "the export button does nothing on mobile" }
Spoken — POST the raw recording bytes (audio/*, ≤ 4 MiB); the worker transcribes at the edge and the transcript enters the same lane. Metadata rides query params because the body IS the audio. Raw audio is not retained; the 201 ack echoes transcript so you can show the human what was heard:
POST /v1/feedback/audio?session_id=sess_abc
Authorization: Bearer <feedback_key>
Content-Type: audio/webm
<recording bytes>
How feedback reaches your product is your call — a MediaRecorder mic button, a phone app, a kiosk. @getloopops/sdk ships feedback() / feedbackAudio() plus a drop-in browser widget (feedback-widget.js: 👍/👎, comment panel, 🎤).
UI/product events use standard OTLP logs with EventName. They can use the same feedback_key when each event is targeted to a session/trace/message, or a producer key for ordinary application logs. Emit regenerations, retries, abandons, escalations, stream disconnects, or slow UX markers; actionable friction becomes a bug candidate, while positive actions stay context:
POST /v1/logs
Authorization: Bearer <feedback_key>
Content-Type: application/json
{
"resourceLogs": [{
"scopeLogs": [{
"logRecords": [{
"traceId": "tr_123",
"eventName": "chat.response_regenerated",
"body": { "stringValue": "user regenerated the response" },
"attributes": [
{ "key": "session.id", "value": { "stringValue": "sess_abc" } },
{ "key": "loopops.assistant_message_id", "value": { "stringValue": "msg_42" } },
{ "key": "loopops.event.action", "value": { "stringValue": "regenerated" } },
{ "key": "app.screen.name", "value": { "stringValue": "ops-room-web" } },
{ "key": "url.path", "value": { "stringValue": "/chat" } }
]
}]
}]
}]
}
4. Run diagnosis
POST /v1/runs/diagnose
Authorization: Bearer <agent_key>
Content-Type: application/json
{ "since_minutes": 1440 }
The response is a diagnosis packet wrapper: { "ok": true, "packet": { … } }. See the DiagnosisPacket schema in GET /openapi.json for the exact shape (packet_id, window_metrics, improvement_candidates, approval_required, …). Metrics are derived from the traces/logs/feedback in the bounded window: duration, slow records, failures, token burn, and cost. They exist to rank the agent's next action; dashboards are a secondary view. packet.status: ready_for_issue_diagnosis is the legacy ready state; new callers should read improvement_candidates as evidence-backed hypotheses to act on.
Each candidate carries a stable cluster_id and a server-computed status joined against the project's loop-run history — new (no run yet), in_progress (continue the linked run's run_id), verified (fix held — skip), or regression (failures recur after the verified fix — reopen with a fresh run_id). Carry cluster_id onto the loop events you record so the whole lifecycle joins on one key. No client-side dedup against prior runs is needed. (The CLI run-manifest packet at /schema/improvement-packet.json is a separate, file-oriented shape — do not validate the diagnose response against it.)
5. Read the evidence
GET /v1/dashboard— JSON: summary, improvement candidates, last run, artifacts.GET /v1/reports/latest.md— the same as a markdown report.GET /v1/telemetry— the bounded recent telemetry record window.GET /v1/sessions— the session index: agent, turns, outcome, timing (the JSON sibling of the console).GET /v1/sessions/{id}— one session's raw, time-ordered records;{id}is a session_id or trace_id.
6. Stop before external writes
LoopOps prepares the issue, eval candidate, and patch plan locally. External writes (GitHub issues, PRs, production changes) happen outside LoopOps for now. Never post on your own.
The default loop
1. Read a bounded telemetry window. 2. Surface evidence-backed improvement candidates. 3. Draft eval candidates and patch plans. 4. Stop before any external write — posting issues or opening PRs happens outside LoopOps for now. 5. Verify the next telemetry window after the change ships.