API reference

API reference (v1)

Base URL: your deployment, e.g. https://lgrcloud.example.com. JSON in and out. Errors look like {"error": {"type": "not_found", "message": "run run_x not found"}}. Starting runs, sending messages, launching or starting instances and scaling fleets up return 402 insufficient_credit when the organisation has no credit left (see Billing).

Authentication

  • API key: Authorization: Bearer lgr_.... Keys belong to one project. GET endpoints that stream (SSE) also accept ?token=.
  • Browser session: the lgr_session cookie set after signing in. Pick the project with lgr-project: proj_... (or ?project= for SSE); otherwise the user's first project is used. Account management endpoints require a session.

Runs

MethodPath
POST/v1/runsStart a run. Returns 201 with the run plus events_url, trace_url, console_url.
GET/v1/runsList runs, newest first. Query: status, agent, instance, limit (≤1000), before (cursor). Returns data, has_more and next_before (pass it as before for the next page).
GET/v1/runs/:idRun with usage, result, error, signals and callbacks (deliveries of its result).
GET/v1/runs/:id/eventsEvents as JSON (?after=<seq>), or SSE with Accept: text/event-stream (resumes from Last-Event-ID; closes after the terminal event).
GET/v1/runs/:id/traceSpan tree for the waterfall view.
GET/v1/runs/:id/artifactsArtifact list.
GET/v1/runs/:id/artifacts/:artifactIdArtifact content.
POST/v1/runs/:id/cancelCancel a queued or running run (409 if it already finished).
POST/v1/runs/:id/approvals/:approvalId{"approved": true, "reviewer": "..."} for a pending tool call.
POST/v1/runs/:id/signals{"kind": "test" | "rating" | "outcome", "value": -1..1, "source": "..."}

Run body:

{
  "task": "Fix the failing tests in tests/api and explain the cause",
  "agent": "billing-service@v3",
  "harness": "lagrange",
  "instructions": "Do not change public interfaces.",
  "workspace": { "repo": "github.com/acme/app", "ref": "main" },
  "model": { "name": "claude-sonnet-4-6" },
  "tools": ["Bash", "Read", "Edit"],
  "mcp_servers": [{ "name": "linear", "url": "https://mcp.linear.app/mcp", "secret": "LINEAR_TOKEN" }],
  "limits": { "max_minutes": 20, "max_cost_usd": 2, "max_turns": 80 },
  "approval": "ask",
  "profile": "dev",
  "continue": true,
  "persist_profile": true,
  "metadata": { "ticket": "ACME-142" },
  "callback_url": "https://example.com/hooks/lagrange"
}

Only task is required. Set at most one of profile, instance, fleet. continue resumes the harness conversation stored on the profile or instance. persist_profile: false runs on a profile without saving a new version. callback_url gets the result when the run finishes; see Callbacks.

A run on a fresh sandbox needs something to call a model with: a platform model (model.name such as tensorblock/gpt-5), or a key for the harness's provider in the project secrets or on the API host (OPENAI_API_KEY for codex, ANTHROPIC_API_KEY for claude-code and lagrange, or Claude Code's Bedrock and Vertex switches). With neither, codex and claude-code runs, on any target, use the platform model named after the harness's default, such as tensorblock/gpt-6.1-sol for codex (see Billing). When the platform doesn't have it, the run is refused with 400 and a message saying which to set. Runs on a profile or instance are not refused, since the harness state stored there may hold its own login.

Statuses: queued, running, succeeded, failed, cancelled.

Agents and improvements

MethodPath
POST/v1/agents/draft{"prompt": "..."}: a platform model drafts an agent from a description, without deploying it; see below.
GET/v1/agents/draft/:idA draft started with POST /v1/agents/draft: {id, status, draft}.
POST/v1/agents/importRead an uploaded agent folder (.tar.gz or .zip, up to 50 MB) without deploying it; see below.
POST/v1/agentsCreate an agent from a spec, or add a version if the name exists. A spec without a harness is stored with the current default harness. ?preview=true adds the version without making it live.
GET/v1/agentsList agents.
GET/v1/agents/:id/mcp-suggestionsServices the agent's last 50 runs called over HTTP at least 3 times (curl, wget, HTTPie or a web fetch), each with up to two MCP servers from the MCP Index that could replace those calls: {data: [{host, calls, runs, candidates: [{id, name, description, profile_url, server}]}]}. Only install-ready entries that need no browser sign-in are offered; hosts the agent already reaches through an MCP server are left out.
GET/v1/agents/:idAgent (id or name) with its current spec and API endpoint.
PATCH/v1/agents/:id{"api_paused": true} stops new calls through the agent's API; false resumes. {"current_version": 3} makes that version live.
GET/v1/agents/:id/keysCaller keys for the agent's API.
POST/v1/agents/:id/keys{"name": "Customer A"}; returns the key once.
DELETE/v1/agents/:id/keys/:keyIdRevoke a caller key.
POST/v1/agents/:id/playgroundA session token for trying the agent through its endpoint, on the live version or {"version": 3}; {"session_id"} renews one. Returns token, session_id, expires_at (one hour), version and endpoint.
GET/v1/agents/:id/versionsAll versions with their specs, newest first.
POST/v1/agents/:id/adjust{"prompt": "...", "version": 3}: a platform model drafts a change to a version (the live one by default) from a description; see below.
POST/v1/agents/:id/adjust/save{version, instructions, skills: [{name, description, instructions}], secrets: [names]}: saves them on top of version as a new version; ?preview=true as with POST /v1/agents.
POST/v1/agents/:id/versionsPublish a new immutable version; it becomes the current one unless ?preview=true. To roll back, post an earlier version's spec.
POST/v1/agents/:id/improvement-jobsLook for an optimization in the agent's runs on its live version (at least 5 finished runs; with fewer, only a workflow audit is tried) and save it as a preview version with a pending candidate. 422 nothing_to_propose says why there is none; 409 while another candidate is pending or in canary. When only a workflow audit (a platform model reading the agent's logic and its most expensive runs) can find one, answers 202 with {id, status: "running"} to poll instead. The platform also does this on its own every hour, at most once per live version, unless the spec sets improvement.mode: off.
GET/v1/agents/:id/improvement-jobs/:jobA workflow audit's job: status is running or done with its improvement; a failed audit answers with its error, 422 nothing_to_propose when it found nothing.
GET/v1/agents/:id/improvementsCandidates for one agent.
GET/v1/improvementsAll candidates; ?state=pending|canary|promoted|rejected|rolled_back.
GET/v1/improvements/:idCandidate: base_version, candidate_version, changes, estimate and metrics measured on each version's finished runs (runs, success_rate, cost_per_run_usd, p50_seconds, failed_calls_per_run).
POST/v1/improvements/:id/promote{"traffic": 0.1} sends that share of runs naming the agent (without a version) to the candidate; 1 makes it the live version. 409 once the base version is no longer live, and for a workflow audit's candidate until a replay has no result judged worse and costs less than the originals.
POST/v1/improvements/:id/reject{"reason": "..."}
POST/v1/improvements/:id/replayRun up to 5 recent inputs of the base version on the candidate (succeeded runs in a fresh sandbox, without approvals; the replay runs on the candidate's own harness and model), at the platform's cost. 202; the candidate's replay.items fill in with each run's cost and time next to the original's and a model's verdict (better, same, worse) with a reason. Replay runs carry metadata.kind: "eval_replay" and don't count in metrics. They run for real, so an agent that changes things elsewhere does so again.
POST/v1/improvements/:id/rollbackMake the version a promoted candidate was built from live again.

While a candidate serves runs, in a canary or in its first 50 runs after promotion, the platform compares it with the version it was built from once it has 10 finished runs. If its success rate is more than 10 points lower, or its cost per run more than 10% higher, a canary is stopped (rejected) and a promotion rolled back (rolled_back), with stopped_reason saying which.

Agent spec (the body of POST /v1/agents; everything but name is optional):

{
  "name": "support-bot",
  "harness": "lagrange",
  "model": { "name": "claude-sonnet-4-6" },
  "instructions": "Answer from the ticket history. Escalate refunds.",
  "bundle": "bnd_5d02c7a3b81e4f0a9c6d7e21",
  "skills": ["triage"],
  "setup": ["pip install --user -r requirements.txt"],
  "mcp_servers": [
    { "name": "linear", "url": "https://mcp.linear.app/mcp", "secret": "LINEAR_TOKEN" },
    { "name": "crm", "command": "python3", "args": ["scripts/crm_mcp.py"], "env": { "CRM_TOKEN": "${CRM_TOKEN}" } }
  ],
  "secrets": ["LINEAR_TOKEN", "CRM_TOKEN"],
  "egress": { "allow": ["api.zendesk.com", "*.githubusercontent.com"] },
  "limits": { "max_minutes": 30 }
}

harness is a built-in harness id, or {"command": "python3", "args": ["harness/main.py"]} to run the agent's own harness from its files; see the harness protocol. skills names skills in the bundle and defaults to all of them. Before the harness starts, each run unpacks the bundle's skills where the harness loads them, copies its workspace/ files into the working directory and runs the setup commands there; a failing command fails the run with its output. Instances and profiles keep what they were prepared with and run setup again only when the bundle or setup changes. An MCP server has either a url (with an optional bearer token secret) or a command that runs inside the sandbox; ${NAME} in its env is replaced with the project secret.

POST /v1/agents returns the agent with the new version, its endpoint, its console_url and warnings: what would make calls to it fail, such as secrets the spec uses that the project hasn't set, a platform model that isn't available, or a harness with no model to call. The deploy still succeeds, since secrets can be set afterwards and an agent served on an instance may use a login stored there. POST /v1/agents/:id/versions returns the same warnings. An unknown harness id is rejected.

POST /v1/agents/import is how the console deploys a folder. The body is the folder as a .tar.gz (Content-Type: application/gzip) or a .zip (application/zip); ?name= names the agent when the archive has no single top folder. The folder's lgr.yaml is used when it has one. Otherwise the spec is built as lgr import builds it, with every visible file and folder in the workspace (dot-files, .git, node_modules and the like are left out). The skills and files are stored as a bundle, and the response has the spec ready for POST /v1/agents (with bundle set), source (lgr.yaml or detected), bundle, the workspace files, the secrets the spec uses with whether each is set, and warnings.

POST /v1/agents/draft is how the console creates an agent from scratch. Drafting takes the model a minute or more, so it answers 202 with {id, status: "running"} at once; poll GET /v1/agents/draft/:id until status is done and read draft. A failed draft answers that poll with its error. A platform model (through Forge, like every model call) turns the description in prompt into {name, instructions, skills: [{name, description, instructions}], secrets: [{name, description, used_by}]}: the AGENTS.md text, up to a few skills, and the secrets those skills read, each with what it is for and where to get it, and the skills that read it. A secret that no skill or instruction reads is dropped. Outside services are called from skills over their HTTP APIs, with the secrets as environment variables, so a drafted agent needs no MCP servers to run. The console shows the draft for editing, then packs it as a folder and sends it to /v1/agents/import, so it is reviewed and deployed like an upload. Needs credit left, like a run; answers 503 when platform models are unavailable and 502 when the model's reply can't be read.

POST /v1/agents/:id/adjust is how the console adjusts a deployed agent. The model gets the version's instructions, its skills (each SKILL.md, and the names of the other files in the skill's folder) and its secrets, and the change in prompt. Like a draft, it answers 202 and is polled at GET /v1/agents/draft/:id; the finished draft has the whole agent after the change plus a summary, and the poll also returns base, the version as it was, so the change can be shown as a diff. Secrets the version already had are kept even when nothing the model sees reads them. POST /v1/agents/:id/adjust/save writes a new bundle from the version's: skills whose text changed get a new SKILL.md (keeping any other front matter), removed skills lose their folder, and everything else (workspace files, a skill's other files, harness, model, limits) is kept as it was.

With ?preview=true, a new version of an existing agent is added without going live: runs and API calls naming the agent keep the current version, while the Playground (or a run naming agent@v3) can try it. PATCH /v1/agents/:id with current_version makes it live. A new agent's first version is always live.

egress.allow limits the hosts the harness can reach while it works: example.com matches that host, *.example.com its subdomains. The model API (including a custom ANTHROPIC_BASE_URL or OPENAI_BASE_URL) and the agent's remote MCP servers are always allowed; setup commands may reach any public host. Blocked connections show up as egress blocked log events on the run. Without egress, any public host is allowed. Private and loopback addresses are never reachable. This is enforced when the server runs the egress proxy and sandboxes have no other route out (see the README's Sandboxes section).

Bundles

A bundle is a .tar.gz with skills/<name>/SKILL.md (plus any files the skill uses) and workspace/.... lgr deploy builds and uploads it from skills and files in lgr.yaml. Bundles are content-addressed per project: uploading the same archive again returns the same id.

MethodPath
POST/v1/bundlesUpload an archive (Content-Type: application/gzip, up to 50 MB). 201 with the bundle, or 200 if the project already has it. Links, absolute paths and paths outside skills/ and workspace/ are rejected.
GET/v1/bundles/:idThe bundle with its skills and file entries.

Examples

Agents that ship with the platform, kept as agent folders under examples/. Deploying one creates an ordinary agent in the project, on the example's suggested harness and model when this deployment offers both, otherwise on the default harness. Its harness and model can be changed like any agent's.

MethodPath
GET/v1/examplesThe examples, each with an id, title, description, a sample task and a suggested_config ({harness, model}) it is known to work with.
POST/v1/examples/:id/deployDeploy the example into the project: 201 with the agent. If the project already has an agent by that name, 200 with that agent, unchanged.

Compute

MethodPath
POST/v1/profiles{"name": "dev", "config": {"env": {...}, "setup": ["..."]}}
GET/v1/profilesList.
GET/v1/profiles/:idProfile (id or name) with its versions.
PATCH/v1/profiles/:id{"config": {...}}
POST/v1/profiles/:id/rollback{"version": 3}: makes that snapshot the newest version.
POST/v1/instances{"name", "size", "profile"?, "config"?, "agent"?}. Returns 201 in provisioning.
GET/v1/instancesList; ?fleet=, ?include_terminated=true.
GET/v1/instances/:idInstance with agent, uptime_seconds, cost_usd, active_runs, state_reason.
PATCH/v1/instances/:id{"agent": "oncall" | null}: change or detach the agent (409 for fleet members; change the fleet).
POST/v1/instances/:id/messagesSend a message to the instance's agent. Returns 201 with the run.
POST/v1/instances/:id/stopStop (409 while runs are active).
POST/v1/instances/:id/startStart a stopped instance.
POST/v1/instances/:id/snapshot{"profile": "golden"}: save the instance as a profile version (creates the profile if needed).
DELETE/v1/instances/:idTerminate and delete its disk.
POST/v1/fleets{"name", "desired", "size"?, "profile"?, "agent"?}
GET/v1/fleetsList with member instances.
GET/v1/fleets/:idFleet.
PATCH/v1/fleets/:id{"agent": "oncall" | null}: change the agent of the fleet and its members.
POST/v1/fleets/:id/messagesSend a message to the least busy running member. Returns 201 with the run.
POST/v1/fleets/:id/scale{"desired": 4}. Scale-down removes idle members, newest first.
DELETE/v1/fleets/:idTerminate all members and delete the fleet.

Instance states: provisioning, running, stopped, failed, terminated. Sizes: lgr.small (1 vCPU, 2 GiB), lgr.medium (2, 4), lgr.large (4, 8), lgr.xlarge (8, 16).

Agent services

An instance or fleet with an agent (name, or name@v3 to pin a version) serves that agent. Messages become runs on the instance with the agent's current version, and the message source is recorded in the run's input.metadata.source (message, webhook or schedule).

Message body:

{ "message": "Check the error rate on the checkout service", "conversation": "continue", "metadata": { "ticket": "OPS-9" }, "callback_url": "https://example.com/hooks/lagrange" }

conversation is continue (the default for instances: resume the conversation stored on the instance) or new (the default for fleets). Messaging an instance that is not running returns 409.

MethodPath
POST/v1/triggersCreate a trigger (below). Webhooks return webhook_url once.
GET/v1/triggersList; ?instance= or ?fleet= to filter.
GET/v1/triggers/:idTrigger (id or name) with next_fire_at, last_fired_at, last_run_id, last_error.
PATCH/v1/triggers/:id{"message"?, "schedule"?, "callback_url"?, "enabled"?}; "callback_url": null stops sending results.
POST/v1/triggers/:id/fireSend the message now; {"body"?} fills {{body}} to try a webhook.
DELETE/v1/triggers/:idDelete. Triggers are also deleted with their instance or fleet.
POST/hooks/:tokenPublic webhook endpoint; the token in the URL is the credential. Any content type, up to 256 KB. Returns 202 with run_id.

Trigger body:

{ "name": "standup", "kind": "schedule", "instance": "desk", "schedule": "0 9 * * 1-5", "message": "Post the daily summary" }

Set exactly one of agent, instance, fleet; an agent gets a new run of its live version each time. kind is webhook or schedule. Schedules are five-field cron expressions in UTC and need a message. A webhook's message defaults to {{body}}; {{body}} is replaced with the request body, pretty-printed when it is JSON. Unknown and paused webhooks both return 404. A schedule missed while the API was down fires once when it comes back. A trigger with a callback_url sends the result of every run it starts there.

Callbacks

A run started with a callback_url, directly, as a message, or by a trigger, has its result POSTed to that URL when it finishes:

POST /hooks/lagrange HTTP/1.1
content-type: application/json
user-agent: LagrangeCloud-Callbacks/1.0
lgr-delivery: dlv_3c9e01b2f7aa
lgr-event: run.completed
lgr-signature: t=1791403200,v1=5f1d…

{"id": "dlv_3c9e01b2f7aa", "type": "run.completed", "created_at": "2026-10-07T20:00:00.000Z", "run": { "id": "run_…", "status": "succeeded", "result": "…", "usage": { … }, "input": { … }, "console_url": "https://…/console/#run=run_…" }}

type is run.completed, run.failed or run.cancelled, and run is the run as POST /v1/runs returns it, with absolute links when LGR_PUBLIC_URL is set. Answer with any 2xx status. Anything else, including redirects, and no answer within 10 seconds count as a failure, and the same delivery is sent again after 30 seconds, 2 minutes, 10 minutes and 1 hour. Use lgr-delivery (the payload's id) to drop duplicates.

lgr-signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> keyed with the project's signing secret. Recompute it over the raw body, compare in constant time, and reject timestamps more than five minutes old. The SDK exports verifyCallback(secret, header, body); the console's Settings → Callbacks has the same check for Node.js to copy.

Callback URLs must be http or https without credentials. Hosts that are, or resolve to, loopback, private or link-local addresses are refused, checked when the run starts and again on every attempt (LGR_ALLOW_PRIVATE_CALLBACKS=1 lifts this for self-hosted installs).

MethodPath
GET/v1/callbacksDeliveries, newest first. Query: run, status (pending, delivered, failed), limit (≤500), before.
POST/v1/callbacks/:id/retrySend now and return the delivery. A pending one keeps its schedule; a delivered or failed one gets one more attempt.
GET/v1/callbacks/secret{"secret": "whsec_…"}, created on first use.
POST/v1/callbacks/secret/rotateReplace the secret. Callbacks are signed with the new one from then on.

Billing

Projects in an organisation spend its prepaid credit: runs pay for sandbox time, plus model tokens when they use the platform's model key rather than the project's ANTHROPIC_API_KEY / OPENAI_API_KEY secret (GET /v1/harnesses lists each harness's credential); instances pay for uptime, charged every minute and when they stop. A run on the platform's key gets limits.max_cost_usd capped at the credit left. Without credit, running instances are stopped once idle with a state_reason; their disks are kept.

MethodPath
GET/v1/billing{"metered", "credit_usd", "charges"}: the organisation's credit and this project's recent charges (kind run or instance; uptime is one line per instance per day). metered is false for static-key projects.

Platform

MethodPath
GET/v1/overview?range=7d|30dKPIs, daily series against the base-harness baseline, attention items, agents.
GET/v1/harnessesAvailable harnesses, versions and capabilities.
GET/v1/benchmarksBenchmark results (mock data for now; see notice).
PUT/v1/secrets{"name": "LINEAR_TOKEN", "value": "..."} (write-only).
GET/v1/secretsSecret names and when they were last set.
DELETE/v1/secrets/:nameDelete a secret.

Accounts

MethodPath
GET/auth/providersEnabled sign-in providers, and email: true when email sign-in is on.
POST/auth/email/signup{"email", "password", "name"?}: create an account (password ≥ 8 characters) and start a session. 409 if the email is taken, 403 outside LGR_EMAIL_SIGNUP_ALLOW.
POST/auth/email/login{"email", "password"}: start a session. 401 on a wrong email or password, 429 after ten failures for one email within 15 minutes.
GET/auth/:provider?return_to=/console/...Start OAuth sign-in.
GET/auth/:provider/callbackOAuth callback; sets the session cookie.
POST/auth/logoutEnd the session.
POST/auth/deviceStart a CLI login: returns device_code, user_code, verification_uri_complete, interval.
POST/auth/device/tokenPoll with {"device_code"}: 400 authorization_pending until approved, then {"api_key", "project_id", "api_url"} once.
GET/v1/meCurrent user, organisations and projects (or the key's project).
POST/v1/orgsCreate an organisation (with a default project).
GET/v1/orgs/:id/membersMembers and pending invitations.
POST/v1/orgs/:id/members{"email", "role"}: add an existing user or invite by email (admin).
PATCH/v1/orgs/:id/members/:userId{"role"} (admin; owners for owner changes).
DELETE/v1/orgs/:id/members/:userIdRemove a member or leave.
DELETE/v1/orgs/:id/invitations/:emailRevoke an invitation.
POST/v1/projects{"org_id", "name"} (admin).
GET/v1/api-keysKeys of the current project (prefix only).
POST/v1/api-keys{"name"}; the response includes the key once.
DELETE/v1/api-keys/:idRevoke.
GET/v1/device/:codePending CLI login details.
POST/v1/device/approve{"user_code", "project_id"?}
POST/v1/device/deny{"user_code"}