Lark Docs

Run AI agents from your code

The Lark API starts agents, follows them live, talks to them while they work, and gets their results, from any language. Each agent works on its own cloud computer, with a terminal, a browser and your tools, for minutes or days. The official SDKs for TypeScript and Python wrap it, with no dependencies.

The base URL is your Lark address: https://dashboard.larkbot.app. All requests and responses are JSON, and timestamps are ISO 8601 in UTC.

Quick start

  1. Create an API key. In the dashboard, go to Settings → API keys (admins only), name it, and copy it. It's shown once. Store it as LARK_API_KEY.
  2. Have an agent. Create one in the dashboard (or ask the assistant), or with the API. You refer to it by name.
  3. Start a run and wait for the result:
curl -X POST https://dashboard.larkbot.app/api/agents/researcher/runs \
  -H "Authorization: Bearer $LARK_API_KEY" \
  -H "content-type: application/json" \
  -d '{"prompt": "Research Acme Inc. and write a one-page brief with sources."}'

# → {"id": "run_8kQ2…", "status": "queued", …}
curl https://dashboard.larkbot.app/api/runs/run_8kQ2… -H "Authorization: Bearer $LARK_API_KEY"
import { Lark } from "lark-agents";

const lark = new Lark({ apiKey: process.env.LARK_API_KEY, baseUrl: "https://dashboard.larkbot.app" });

const run = await lark.runs.create({
  agent: "researcher",
  prompt: "Research Acme Inc. and write a one-page brief with sources.",
});
const done = await lark.runs.wait(run.id);
console.log(done.status, done.result);
from lark_agents import Lark

lark = Lark(base_url="https://dashboard.larkbot.app")  # reads LARK_API_KEY

run = lark.runs.create(agent="researcher",
                       prompt="Research Acme Inc. and write a one-page brief with sources.")
done = lark.runs.wait(run["id"])
print(done["status"], done["result"])

That's it: the agent got its own machine, did the research in a browser, and returned a report. The rest of this page covers following it live, talking to it, automating it, and what it costs.

Authentication

Send your key on every request as a bearer token: Authorization: Bearer lark_…. A key belongs to one organization and acts with the role you gave it:

RoleCan
memberRun agents, read runs and events, manage agents, triggers, skills and secrets.
adminEverything a member can, plus organization settings, notifications, members, API keys and the webhook signing secret.
  • Keys never expire. Revoke one in Settings → API keys and it stops working immediately. Rotate by creating the new key first, deploying it, then revoking the old one.
  • Keep keys on a server, never in a browser or mobile app, and never in source control.
  • Keys can't approve runs the assistant proposed, drive an agent's browser, or change billing. Those need a signed-in person.

Costs & limits

Runs started with an API key are billed to the key's organization exactly like any other run. A run costs its AI model tokens plus its machine time. It's costed as it works (every 30 seconds) and shows on the run and in Usage & billing, broken down by agent and by API key.

Your plan's included usageFree includes $1 a month and stops there. Paid plans include more and continue past it, billed as you go.
The monthly budgetWhen the month's spend reaches it, new runs are refused and running ones stop, wherever they were started.
A cap per keyGive any key its own monthly cap, so a leaked or runaway integration can't spend the whole budget.

When a run can't start, the API answers 402 with a code that says why (see Errors). A run that hits a limit while it's working ends as failed, with the reason in error.

SDKs

Both SDKs have the same shape, no dependencies, and typed errors. Set LARK_API_KEY and LARK_URL, or pass them in.

# Node 18+, Deno, Bun or workers
npm install lark-agents
# Python 3.9+
pip install lark-agents
AreaMethods
Runscreate get list events stream wait message cancel
Agentslist get create update delete
Triggerslist create update delete fire
Skillslist get create update delete draft
Secretslist set delete
Otherusage(), request() for any endpoint, and verifyWebhook / verify_webhook

Runs

A run is one task for one agent. It moves through queued → running → succeeded, failed or cancelled. A run lasts up to the agent's limit: 24 hours by default, up to 7 days.

POST/api/agents/{agent}/runsStart a run
GET/api/runs/{id}Status, result, error, cost
GET/api/runs?agentId=&status=&limit=&before=List, newest first
GET/api/runs/{id}/events?after=The activity log
GET/api/runs/{id}/stream?after=The same, live (Server-Sent Events)
POST/api/runs/{id}/messagesMessage it while it works
POST/api/runs/{id}/cancelStop it

The body for starting a run:

FieldType
promptstring, requiredThe task, in plain words. Be specific about what "done" looks like.
repostringA git URL to clone before it starts. Private repos need a GITHUB_TOKEN secret on the agent.
refstringBranch, tag or commit to check out.
webhookUrlURLWhere to POST the run when it finishes (see Run webhooks).
const run = await lark.runs.create({
  agent: "coder",
  prompt: "Fix the failing tests and open a pull request.",
  repo: "https://github.com/acme/web.git",
  webhookUrl: "https://acme.dev/hooks/lark",
});

// Later, or somewhere else:
const latest = await lark.runs.get(run.id);
if (latest.status === "failed") console.error(latest.error);
console.log(latest.cost); // { billableUsd, modelUsd, vmSeconds, … }
run = lark.runs.create(
    agent="coder",
    prompt="Fix the failing tests and open a pull request.",
    repo="https://github.com/acme/web.git",
    webhook_url="https://acme.dev/hooks/lark",
)

latest = lark.runs.get(run["id"])
if latest["status"] == "failed":
    print(latest["error"])
print(latest["cost"])
curl -X POST https://dashboard.larkbot.app/api/agents/coder/runs \
  -H "Authorization: Bearer $LARK_API_KEY" -H "content-type: application/json" \
  -d '{"prompt":"Fix the failing tests and open a pull request.","repo":"https://github.com/acme/web.git"}'

Streaming a run

Follow an agent as it works: every command, page and message arrives as an event. The stream is Server-Sent Events. Each event's text is a readable line, and an end event closes it when the run finishes. Reconnect with after=<last seq> (or Last-Event-ID) and you won't miss or repeat anything. The SDKs do this for you.

for await (const event of lark.runs.stream(run.id)) {
  if (event.text) console.log(event.text);         // "$ npm test", "⌁ browser goto …"
  if (event.type === "question") console.log("It's asking:", event.data.question);
}
for event in lark.runs.stream(run["id"]):
    if event.get("text"):
        print(event["text"])
    if event["type"] == "question":
        print("It's asking:", event["data"]["question"])
curl -N https://dashboard.larkbot.app/api/runs/run_8kQ2…/stream \
  -H "Authorization: Bearer $LARK_API_KEY" -H "Accept: text/event-stream"
EventWhen
statusThe run's status changed (data.status, and data.error on failure).
assistantThe agent said something (data.text).
tool.call / tool.result / tool.outputA command, a file edit, a browser action, and what came back.
screenIts browser has a new screenshot (data.url, data.title).
question / answerIt asked you something and is waiting; it got the answer.
user.messageSomeone sent it a message.
costThe final cost, when it ends.
resultThe final report was recorded.

Talking to a running agent

Add to the task while it works, or answer a question it asked. The agent reads your message before its next step. When an agent is stuck (it needs a login, or a decision only you can make), it emits a question event, notifies the people who follow it, and waits for a message.

await lark.runs.message(run.id, "Also include their pricing page.");

for await (const e of lark.runs.stream(run.id)) {
  if (e.type === "question") await lark.runs.message(run.id, "Use the staging account.");
}
lark.runs.message(run["id"], "Also include their pricing page.")

for e in lark.runs.stream(run["id"]):
    if e["type"] == "question":
        lark.runs.message(run["id"], "Use the staging account.")
curl -X POST https://dashboard.larkbot.app/api/runs/run_8kQ2…/messages \
  -H "Authorization: Bearer $LARK_API_KEY" -H "content-type: application/json" \
  -d '{"text":"Also include their pricing page."}'

Agents

An agent is a model, a harness (Lark's built-in agent, Claude Code, Codex or Aider), a machine, and its instructions, secrets, skills and tools. Refer to agents by name or id.

GET/api/agentsList
POST/api/agentsCreate
GET/api/agents/{agent}Get
PATCH/api/agents/{agent}Change some fields
DELETE/api/agents/{agent}Delete
await lark.agents.create({
  name: "researcher",
  description: "Researches companies on the web",
  model: { provider: "lark", model: "accounts/fireworks/models/glm-5p3" },
  browser: true,                             // a real browser in its machine
  sandbox: { type: "fly", persistent: true }, // keeps files, logins and notes between tasks
  systemPrompt: "Cite every source. Prefer primary sources.",
  secrets: ["CRM_TOKEN"],
  skills: ["company-research-brief"],
});
lark.agents.create(
    name="researcher",
    description="Researches companies on the web",
    model={"provider": "lark", "model": "accounts/fireworks/models/glm-5p3"},
    browser=True,
    sandbox={"type": "fly", "persistent": True},
    systemPrompt="Cite every source. Prefer primary sources.",
    secrets=["CRM_TOKEN"],
    skills=["company-research-brief"],
)

List the models and harnesses this server offers with GET /api/providers and GET /api/harnesses.

Triggers & inbound webhooks

A trigger starts an agent on its own: on a schedule (cron, in your time zone) or when another service calls its webhook URL. What a webhook receives becomes part of the task: {{payload}} is the whole body, and {{payload.field}} is one value from it. Triggered runs are billed like any run, and stop at the trigger's daily cap and your budget.

const hook = await lark.triggers.create("researcher", {
  name: "new-lead",
  kind: "webhook",
  prompt: "New lead: {{payload.name}} at {{payload.company}}. Research the company and write a brief.",
});
console.log(hook.url, hook.secret); // give these to the sending service; the secret is shown once

const nightly = await lark.triggers.create("coder", {
  name: "nightly-tests", kind: "schedule", schedule: "0 2 * * *", timezone: "America/New_York",
  prompt: "Run the test suite and fix anything that's red.",
});
hook = lark.triggers.create("researcher", name="new-lead", kind="webhook",
    prompt="New lead: {{payload.name}} at {{payload.company}}. Research the company and write a brief.")
print(hook["url"], hook["secret"])

lark.triggers.create("coder", name="nightly-tests", kind="schedule",
    schedule="0 2 * * *", timezone="America/New_York",
    prompt="Run the test suite and fix anything that's red.")
# The sending service signs its body with the trigger's secret:
BODY='{"name":"Ada Lovelace","company":"Analytical Engines"}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$TRIGGER_SECRET" -hex | sed 's/.* //')"
curl -X POST "$TRIGGER_URL" -H "content-type: application/json" -H "X-Lark-Signature: $SIG" -d "$BODY"
# → 202 {"runId": "run_…", "status": "queued"}

If the sender can't sign, call <url>/<secret> instead. Lark answers 202 with the run id, 401 for a bad signature, 429 past the daily cap, and 402 past your budget.

Run webhooks

Pass webhookUrl when starting a run, and Lark POSTs the run there when it finishes, retrying with backoff if your server fails. The body is {"event": "run.succeeded" | "run.failed" | "run.cancelled", "run": {…}}. Every delivery is signed with your organization's webhook signing secret, which admins see in Settings → API keys: X-Lark-Signature: sha256=<hex HMAC-SHA256 of the raw body>.

import { verifyWebhook } from "lark-agents";

app.post("/hooks/lark", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = await verifyWebhook(req.body, req.get("x-lark-signature"), process.env.LARK_WEBHOOK_SECRET!);
  if (!ok) return res.sendStatus(401);
  const { event, run } = JSON.parse(req.body.toString());
  if (event === "run.succeeded") await saveReport(run.id, run.result);
  res.sendStatus(200);
});
from lark_agents import verify_webhook

@app.post("/hooks/lark")
def lark_hook():
    raw = request.get_data()
    if not verify_webhook(raw, request.headers.get("X-Lark-Signature"), os.environ["LARK_WEBHOOK_SECRET"]):
        abort(401)
    payload = json.loads(raw)
    if payload["event"] == "run.succeeded":
        save_report(payload["run"]["id"], payload["run"]["result"])
    return "", 200

Always verify against the raw body, before parsing it. Answer with a 2xx quickly; do slow work afterwards.

Skills

A skill is reusable know-how (how you deploy, a research method, a checklist) that agents load when a task calls for it. Draft one with AI from a description or from a run that went well, review it, then save it and attach it to agents.

const { draft } = await lark.skills.draft({ runId: run.id });  // learn from a run that went well
await lark.skills.create(draft);
await lark.agents.update("researcher", { skills: ["company-research-brief", draft.name] });
draft = lark.skills.draft(run_id=run["id"])["draft"]
lark.skills.create(**draft)
lark.agents.update("researcher", skills=["company-research-brief", draft["name"]])

Secrets

API keys and tokens your agents need (a GitHub token, a CRM key) live encrypted in your organization and are only put into the machines of the agents you attach them to, as environment variables. Values are write-only: you can list names and replace values, never read them back. They're also removed from logs.

await lark.secrets.set("GITHUB_TOKEN", process.env.GITHUB_TOKEN!);
await lark.agents.update("coder", { secrets: ["GITHUB_TOKEN"] });
lark.secrets.set("GITHUB_TOKEN", os.environ["GITHUB_TOKEN"])
lark.agents.update("coder", secrets=["GITHUB_TOKEN"])

Usage & billing

GET /api/org/billing (or lark.usage()) returns the month's spend, by agent, by day and by API key, plus what's running now, the plan, its limits and the budget. Use it to watch an integration's spend from your own code.

Errors

Errors are JSON: {"error": "what went wrong", "code": "…"}. The SDKs raise them as LarkError, with status and code.

StatusCodeMeaning
400The request isn't valid. details says which field.
401Missing, wrong or revoked key.
402budget_reachedThe organization's monthly budget is used up.
402key_capThis key's own monthly cap is used up.
402plan_limitThe plan doesn't include this, or its included usage is used up (Free).
402payment_failedThe last payment failed; update the card in Usage & billing.
402suspendedThe organization is suspended. Contact support.
403The key's role can't do this, or only a signed-in person can.
404No such agent, run or resource in this organization.
409Not now: for example, messaging a run that already finished.
429rate_limitedToo many requests. Wait and retry with backoff.

Rate limits

  • Starting runs: 30 per minute per API key, by default.
  • Inbound trigger webhooks: 120 requests per minute per trigger, plus each trigger's daily run cap.
  • Agents running at once: set by your plan. Extra runs queue and start as others finish.

A 429 means slow down. Retry after a short pause, with exponential backoff.

Objects

Run

idstringrun_…
statusstringqueued, running, succeeded, failed, cancelled
agentobjectThe agent as it was when the run started.
prompt, repo, refstringWhat it was asked to do.
resultstring | nullThe final report (Markdown).
errorstring | nullWhy it failed.
costobject | nullbillableUsd, modelUsd, computeUsd, vmSeconds, updated while it runs.
usageobjectinputTokens, outputTokens, steps.
createdBystring | nullWho started it: an email, key:<prefix> for an API key, or a trigger.
triggerIdstring | nullThe trigger that started it.
createdAt, startedAt, finishedAtstringISO 8601, UTC.

Need something these docs don't cover? Every endpoint the dashboard uses is in the full API reference.