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.GETendpoints that stream (SSE) also accept?token=. - Browser session: the
lgr_sessioncookie set after signing in. Pick the project withlgr-project: proj_...(or?project=for SSE); otherwise the user's first project is used. Account management endpoints require a session.
Runs
| Method | Path | |
|---|---|---|
POST | /v1/runs | Start a run. Returns 201 with the run plus events_url, trace_url, console_url. |
GET | /v1/runs | List 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/:id | Run with usage, result, error, signals and callbacks (deliveries of its result). |
GET | /v1/runs/:id/events | Events 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/trace | Span tree for the waterfall view. |
GET | /v1/runs/:id/artifacts | Artifact list. |
GET | /v1/runs/:id/artifacts/:artifactId | Artifact content. |
POST | /v1/runs/:id/cancel | Cancel 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
| Method | Path | |
|---|---|---|
POST | /v1/agents/draft | {"prompt": "..."}: a platform model drafts an agent from a description, without deploying it; see below. |
GET | /v1/agents/draft/:id | A draft started with POST /v1/agents/draft: {id, status, draft}. |
POST | /v1/agents/import | Read an uploaded agent folder (.tar.gz or .zip, up to 50 MB) without deploying it; see below. |
POST | /v1/agents | Create 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/agents | List agents. |
GET | /v1/agents/:id/mcp-suggestions | Services 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/:id | Agent (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/keys | Caller keys for the agent's API. |
POST | /v1/agents/:id/keys | {"name": "Customer A"}; returns the key once. |
DELETE | /v1/agents/:id/keys/:keyId | Revoke a caller key. |
POST | /v1/agents/:id/playground | A 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/versions | All 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/versions | Publish 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-jobs | Look 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/:job | A 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/improvements | Candidates for one agent. |
GET | /v1/improvements | All candidates; ?state=pending|canary|promoted|rejected|rolled_back. |
GET | /v1/improvements/:id | Candidate: 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/replay | Run 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/rollback | Make 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.
| Method | Path | |
|---|---|---|
POST | /v1/bundles | Upload 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/:id | The 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.
| Method | Path | |
|---|---|---|
GET | /v1/examples | The 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/deploy | Deploy 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
| Method | Path | |
|---|---|---|
POST | /v1/profiles | {"name": "dev", "config": {"env": {...}, "setup": ["..."]}} |
GET | /v1/profiles | List. |
GET | /v1/profiles/:id | Profile (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/instances | List; ?fleet=, ?include_terminated=true. |
GET | /v1/instances/:id | Instance 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/messages | Send a message to the instance's agent. Returns 201 with the run. |
POST | /v1/instances/:id/stop | Stop (409 while runs are active). |
POST | /v1/instances/:id/start | Start 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/:id | Terminate and delete its disk. |
POST | /v1/fleets | {"name", "desired", "size"?, "profile"?, "agent"?} |
GET | /v1/fleets | List with member instances. |
GET | /v1/fleets/:id | Fleet. |
PATCH | /v1/fleets/:id | {"agent": "oncall" | null}: change the agent of the fleet and its members. |
POST | /v1/fleets/:id/messages | Send 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/:id | Terminate 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.
| Method | Path | |
|---|---|---|
POST | /v1/triggers | Create a trigger (below). Webhooks return webhook_url once. |
GET | /v1/triggers | List; ?instance= or ?fleet= to filter. |
GET | /v1/triggers/:id | Trigger (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/fire | Send the message now; {"body"?} fills {{body}} to try a webhook. |
DELETE | /v1/triggers/:id | Delete. Triggers are also deleted with their instance or fleet. |
POST | /hooks/:token | Public 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).
| Method | Path | |
|---|---|---|
GET | /v1/callbacks | Deliveries, newest first. Query: run, status (pending, delivered, failed), limit (≤500), before. |
POST | /v1/callbacks/:id/retry | Send 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/rotate | Replace 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.
| Method | Path | |
|---|---|---|
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
| Method | Path | |
|---|---|---|
GET | /v1/overview?range=7d|30d | KPIs, daily series against the base-harness baseline, attention items, agents. |
GET | /v1/harnesses | Available harnesses, versions and capabilities. |
GET | /v1/benchmarks | Benchmark results (mock data for now; see notice). |
PUT | /v1/secrets | {"name": "LINEAR_TOKEN", "value": "..."} (write-only). |
GET | /v1/secrets | Secret names and when they were last set. |
DELETE | /v1/secrets/:name | Delete a secret. |
Accounts
| Method | Path | |
|---|---|---|
GET | /auth/providers | Enabled 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/callback | OAuth callback; sets the session cookie. |
POST | /auth/logout | End the session. |
POST | /auth/device | Start a CLI login: returns device_code, user_code, verification_uri_complete, interval. |
POST | /auth/device/token | Poll with {"device_code"}: 400 authorization_pending until approved, then {"api_key", "project_id", "api_url"} once. |
GET | /v1/me | Current user, organisations and projects (or the key's project). |
POST | /v1/orgs | Create an organisation (with a default project). |
GET | /v1/orgs/:id/members | Members 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/:userId | Remove a member or leave. |
DELETE | /v1/orgs/:id/invitations/:email | Revoke an invitation. |
POST | /v1/projects | {"org_id", "name"} (admin). |
GET | /v1/api-keys | Keys of the current project (prefix only). |
POST | /v1/api-keys | {"name"}; the response includes the key once. |
DELETE | /v1/api-keys/:id | Revoke. |
GET | /v1/device/:code | Pending CLI login details. |
POST | /v1/device/approve | {"user_code", "project_id"?} |
POST | /v1/device/deny | {"user_code"} |