Docs

LoopOps Quickstart

11 min read · markdown (agent-readable twin)

# LoopOps Quickstart

Connect your app to LoopOps and run the improvement loop: keys, telemetry in over OTLP, your coding agent on over MCP, first diagnosis. First accepted signal takes under a minute; reads roll ~60-90s behind ingest. Prefer sectioned, per-stack pages? /docs — every page has an .md twin. (Packages ship as @getloopops/* on npm.)

Human? Don't work through this file by hand — https://loopops.dev/start hands you one prompt to paste into your coding agent, and the agent does all of it: required plugin/skill + MCP installation for that harness, keys, wiring, first trace, first diagnosis, your console link. 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? Fetch https://loopops.dev/agent-setup/prompt.md instead — it is this file's content in execution order and makes harness installation the hard first gate before provision, wire, first span, first diagnosis, and handoff.

Claude Code: install the LoopOps plugin

Run these commands — the plugin registers the hosted MCP server and bundles the loop skill:

claude plugin marketplace add codeyogi911/loopkit-plugin
claude plugin install loopkit@loopkit --scope user

Verify with claude plugin list, then run /reload-plugins or restart Claude Code so the newly installed skill and MCP load into the session.

First MCP use opens a one-time email sign-in. A project created by the onboarding runbook (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 LoopOps repo marketplace installs the hosted MCP server and workflow guardrails together:

codex plugin marketplace add codeyogi911/loopkit-plugin --ref main
codex plugin add loopkit@loopkit
codex mcp login loopops

Verify with codex plugin list --json and codex mcp list --json, complete the browser sign-in, then 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.

Cursor, Copilot, Windsurf, OpenCode — and other MCP clients

Installing only the MCP is incomplete: install the shared LoopOps skill first, then add the hosted server to the client's native config:

npx -y skills add codeyogi911/loopkit-plugin --skill loopkit --yes --global

Cursor uses .cursor/mcp.json:

{
  "mcpServers": {
    "loopops": { "type": "http", "url": "https://mcp.loopops.dev" }
  }
}

GitHub Copilot / VS Code uses .vscode/mcp.json with a top-level servers object; Windsurf uses mcpServers.loopops.serverUrl in ~/.codeium/windsurf/mcp_config.json; OpenCode uses a remote entry under mcp.loopops in ~/.config/opencode/opencode.jsonc followed by opencode mcp auth loopops. The canonical, execution-ordered spelling is in /agent-setup/prompt.md.

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 keys for a 14-day sandbox (claim it to keep it):

curl -sS -X POST https://loopops.dev/v1/provision \
  -H 'content-type: application/json' \
  -d '{"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 for a 14-day sandbox — claim the project to keep it. 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 converges 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.

POST /v1/signup still exists as a DEPRECATED, non-idempotent alias — it mints a fresh project on every call with the same terms (a 14-day sandbox; claim it to keep it). Prefer provision:

curl -sS -X POST https://loopops.dev/v1/signup \
  -H 'content-type: application/json' \
  -d '{"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 free (ingest is unlimited; they expire if unclaimed) — 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

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):

npm i @getloopops/sdk
export LOOPOPS_URL=https://loopops.dev
export LOOPOPS_INGEST_KEY=<producer_key>   # from step 0
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, 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 sourceLoopOps wireNotes
Vercel AI SDK 7@getloopops/sdk/airegisterTelemetry(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/otelregisterLoopOps() + telemetry()Uses OTel OTLP exporter; set sampleRate or LOOPOPS_SAMPLE_RATE.
Plain Node OpenTelemetry@getloopops/otelinitLoopOps()One-call OTLP exporter for any spans your process already emits. Node-only — not Workers.
Cloudflare WorkersCloudflare automatic tracing → Traces destination → https://ingest.loopops.dev/v1/traces; human-approved Logs destination → https://ingest.loopops.dev/v1/logsNo NodeSDK or request wrapper. Destinations are created in the Cloudflare dashboard, then named in observability.traces.destinations / observability.logs.destinations. Never self-point a Worker at its own receiver.
Browser or product UX events@getloopops/sdk/browser or OTLP logs → /v1/logsUse the browser-safe feedback_key only for targeted feedback/events.
Any language, collector, or frameworkOTLP/HTTP JSON or protobuf → /v1/tracesKeep service.name, service.version, and deployment.environment.name on the resource.
Mastra or other agent frameworksExisting OTel exporter/bridge → https://loopops.dev/v1/tracesKeep 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": "0af7651916cd43dd8448eb211c80319c",
        "spanId": "b7ad6b7169203331",
        "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. Fastest check, any stack: GET /v1/ingest/health with any project key — status: "receiving" means ingest accepted your batch (immediate, ahead of the ~60-90s read roll). Or in code (needs BOTH env keys — LOOPOPS_INGEST_KEY for emit and LOOPOPS_AGENT_KEY for the read-back):

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_*): browser-safe — it can submit feedback and targeted events, plus read its own project's ingest health, 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

project window; use a returned name as the optional environment selector on compatible reads. Environments never create a separate key scope.

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.