Press / to search

All documentation
docs Call it from your software SDKs

Runs onWrit CloudSelf-hosted

sdk ▸ four clients

Four SDKs. One agent.

TypeScript, Python, Go and Rust — published, versioned, and thin. Each client discovers the running Writ agent on your machine, and the same client reaches Writ Cloud when you hand it a wt_ key.

The SDKs drive software on your machine, on your accounts. Nothing phones home.

install ▸ first run

Install, discover, run.

Every quickstart is the same three beats: the client finds the running agent (no URL, no token to paste), lists your workflows, runs one and reads the extracted rows back. These samples are the published packages, verbatim.

TypeScript typescript/ From the repo · Node ≥ 18 · zero runtime deps
Python python/ From the repo · Python ≥ 3.10 · import writ_agent
Go github.com/usewrit/writ-sdks/go go get · Go ≥ 1.23 · stdlib only
Rust rust/ Git dependency · async, any reqwest-compatible runtime

run.ts

import { WritAgent, runRowId } from "@usewrit/agent-sdk";

const client = new WritAgent();              // discovers the running agent + token
const { data: workflows } = await client.workflows.list();
const run = await client.workflows.runAndWait(workflows[0].id, {
  inputs: { city: "Paris" },
});
const { data: rows } = await client.runs.data(runRowId(run));
console.log(run.status, rows);

surfaces ▸ two

One client, two surfaces.

The SDKs speak to two different places, and the docs never blur them. The local agent is the software on your machine: loopback only, free, with its own token families. Writ Cloud is the hosted surface a wt_ key unlocks — and it has a keyless tier that needs no account at all.

SurfaceBase URLAuth
Local agent (writ-agentd)http://127.0.0.1:8131 · https://127.0.0.1:8132wlt_ runtime token · wlk_ scoped key · wlo_ OAuth
Writ Cloudhttps://api.usewrit.appwt_ API key (metered) · X-Writ-Client-Id (keyless)

Talk to 127.0.0.1, not localhost — the daemon enforces a DNS-rebind guard on the Host and Origin it accepts. The HTTPS twin on :8132 uses a per-install local CA at ~/.writ/tls/ca.pem.

Environment variables

Discovery runs env-first, then reads runtime.json from the Writ home, probing each candidate for liveness. Identical names across all four SDKs:

VariableWhat it does
WRIT_API_URLLocal daemon base URL override
WRIT_TOKENLocal daemon bearer token override
WRIT_HOMEFirst runtime.json candidate directory
WRIT_API_KEYWrit Cloud metered API key (wt_)
WRIT_CLOUD_URLWrit Cloud base URL override
WRIT_CLIENT_IDKeyless device id override

runs ▸ three ways to wait

Run it, then wait your way.

Every SDK exposes the same three postures for the same run:

  1. Async handle — run() returns immediately with a run id — poll or stream when you choose.
  2. Server-side wait — run with wait — the HTTP call itself blocks until the run settles (timeout in seconds, clamped server-side).
  3. runAndWait — The SDK subscribes to the live event stream and polls as a fallback, returning the settled run.

A failed run is a result, not an error: you get the run back with its status. Only an expired wait budget raises — and the error still carries the run id so nothing is lost.

Run-feed items carry a composite string id like workflow-3. Every runs.* call takes the numeric row id — extract it with the per-language helper: runRowId(run) (TS), run_row_id(run) (Python), item.RowID() (Go), item.row_id() (Rust).

Live events over SSE

Step-by-step progress streams from the daemon; each language gets its native idiom — an async iterator, a generator, a range-over-func, a Stream.

events.ts

for await (const ev of client.runs.events(runRowId(run))) {
  console.log(ev.type, ev);
}

surface ▸ services

The whole agent, namespaced.

One client object carries the full surface: agent, workflows, runs, monitors, selectors, extractors, automations, personas, secrets, vault, files, data, crawl, datasets, keys — plus cloud. The names are identical across languages; the idioms are native:

TypeScriptPromise-based namespaces; Page<T> envelopes; run() overloads for wait and dry-run.
PythonSync WritAgent and AsyncWritAgent twins; responses are plain dicts; Page is iterable.
GoEvery method takes ctx first; errors are errors.As-friendly typed pointers; zero dependencies.
RustAsync-only; filtered lists via *_with variants; Cloud is a separate CloudClient; one flat WritError enum.

cloud ▸ metered + keyless

The cloud tier is built in.

Hand the client a wt_ key and cloud Scrape, Map and Crawl calls are metered from your credit pool. Without any key, the keyless tier reads public pages identified only by a device id — with a quota endpoint that tells you what remains.

cloud.ts

const cloud = new CloudApi({ apiKey: process.env.WRIT_API_KEY }); // wt_…
const page = await cloud.scrape("https://example.com");
const site = await cloud.map("https://example.com", { search: "pricing", limit: 20 });
console.log(cloud.tier); // "metered" | "keyless"

The client exposes its tier ("metered" or "keyless") so your code can branch. Keyless answers 429 when the allowance is spent; metered answers 402 when the pool is empty — both as typed errors.

keys ▸ errors

Scoped keys, typed failures.

Minting a scoped wlk_ key (scopes: read, run, admin) requires the full-access runtime token — so a leaked CI key can never mint broader access for itself.

const key = await client.keys.create({ name: "ci-runner", scopes: "read,run" });

Error taxonomy

The same failure is the same type in every language — catch what you can handle, let the rest carry status, code and body:

ApiErrorAny non-2xx with a stable code: bad_request, unauthorized, forbidden, not_found, vault_locked (423), too_many_requests, internal.
RunTimeoutA wait budget expired — carries the still-valid run id.
RateLimitedKeyless allowance spent — carries reset time and remaining counters.
InsufficientCreditsMetered pool empty (402). Top up or fall back to local.
ApiKeyRequiredA metered cloud call was made without a wt_ key.
Connection / DiscoveryNo live agent found, or the daemon is unreachable.

rest ▸ no sdk

No SDK? The endpoint is plain REST.

A published workflow endpoint is an ordinary HTTPS POST with a wt_ Bearer token — these wrappers are the whole integration if you would rather own the HTTP yourself.

writ.py

import os, requests

WRIT_BASE = "https://api.usewrit.app"

def run_workflow(slug: str, path: str, inputs: dict) -> dict:
    res = requests.post(
        f"{WRIT_BASE}/v1/{slug}/{path}",
        headers={"Authorization": f"Bearer {os.environ['WRIT_CONSUMER_KEY']}"},
        json=inputs,
        timeout=120,
    )
    res.raise_for_status()
    return res.json()

payload = run_workflow("acme", "price-check", {"url": "https://example.com/product/42"})
print(payload["data"])

faq

SDK questions, answered.

Do I need an SDK to use Writ?
No. Published endpoints are plain REST with a wt_ Bearer token, and the keyless cloud tier is a curl call. The SDKs earn their place when you drive the local agent: discovery, SSE events, typed errors and the full service surface without hand-rolling HTTP.
Which languages are published?
TypeScript, Python, Go and Rust, all in github.com/usewrit/writ-sdks. Go installs with go get; the others are installed from the repo today — registry packages are not published yet. Generated clients for other languages build from the same OpenAPI spec.
How do the SDKs find my agent?
Environment first (WRIT_API_URL / WRIT_TOKEN), then runtime.json candidates under your Writ home, each probed for liveness with a two-second budget. You can always pass a base URL and token explicitly.
How do I handle long-running workflows?
Three ways: take the async run id and come back; ask the server to wait (the call blocks until the run settles); or runAndWait, where the SDK streams live events with polling as a fallback. A failed run comes back as a result with its status, not as an exception.
Do the SDKs work in a browser?
Discovery is filesystem-based, so it is desktop-side only. In a browser, pass baseUrl and token explicitly — or call your published cloud endpoint, which is plain REST built for exactly that.

end ▸ ship

Install one and make the first call.

The quickstart runs your first workflow in a few minutes, locally and for free.