Se ejecuta enWrit CloudAutoalojado
En esta página
Cuatro SDKs. Un agente.
TypeScript, Python, Go y Rust — publicados, versionados y ligeros. Cada cliente descubre el agente Writ que corre en tu máquina, y el mismo cliente llega a Writ Cloud cuando le das una clave wt_.
install ▸ primer run
Instala, descubre, ejecuta.
Cada quickstart sigue los mismos tres tiempos: el cliente encuentra el agente en marcha (sin URL, sin token que pegar), lista tus workflows, ejecuta uno y lee las filas extraídas. Estos ejemplos son los paquetes publicados, al pie de la letra.
| TypeScript | typescript/ | Desde el repo · Node ≥ 18 · cero dependencias runtime |
| Python | python/ | Desde el repo · Python ≥ 3.10 · import writ_agent |
| Go | github.com/usewrit/writ-sdks/go | go get · Go ≥ 1.23 · solo stdlib |
| Rust | rust/ | Dependencia git · async, cualquier runtime compatible con reqwest |
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); run.py
from writ_agent import WritAgent, run_row_id
with WritAgent() as client: # discovers the local daemon
run = client.workflows.run_and_wait(3, inputs={"city": "Paris"})
print(run["status"], run["rows_extracted"])
print(client.runs.data(run_row_id(run))["data"]) # extracted rows run.go
client, err := writ.Discover(ctx) // find the running agent
page, _ := client.Workflows.List(ctx, nil)
item, _ := client.Workflows.RunAndWait(ctx, page.Data[0].ID, nil)
rowID, _ := item.RowID()
csv, _ := client.Runs.DataCSV(ctx, rowID) // extracted rows as CSV
fmt.Println(item.Status, "
", csv) run.rs
use writ_client::{RunOptions, WritAgent};
let agent = WritAgent::discover().await?; // find the running daemon
let workflows = agent.workflows().list().await?;
let wf = &workflows.data[0];
let outcome = agent.workflows().run_and_wait(wf.id, &RunOptions::default()).await?;
let rows = agent.runs().data(outcome.run.row_id().unwrap()).await?;
println!("{} → {}: {}", wf.name, outcome.run.status, rows.data); call.sh
curl -X POST https://api.usewrit.app/v1/acme/price-check \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/product/42"}' superficies ▸ dos
Un cliente, dos superficies.
Los SDKs hablan con dos lugares distintos, y la doc nunca los mezcla. El agente local es el software en tu máquina: solo loopback, gratis, con sus propias familias de tokens. Writ Cloud es la superficie alojada que una clave wt_ desbloquea — con un nivel sin clave que no pide cuenta.
| Superficie | URL base | Auth |
|---|---|---|
| Agente local (writ-agentd) | http://127.0.0.1:8131 · https://127.0.0.1:8132 | token runtime wlt_ · clave acotada wlk_ · OAuth wlo_ |
| Writ Cloud | https://api.usewrit.app | clave API wt_ (medida) · X-Writ-Client-Id (sin clave) |
Habla con 127.0.0.1, no con localhost — el daemon aplica una guardia anti DNS-rebind sobre el Host y el Origin que acepta. El gemelo HTTPS en :8132 usa una CA local por instalación en ~/.writ/tls/ca.pem.
Variables de entorno
El descubrimiento lee primero el entorno y luego los runtime.json del directorio Writ, sondeando cada candidato. Nombres idénticos en los cuatro SDKs:
| Variable | Qué hace |
|---|---|
WRIT_API_URL | Sustituye la URL base del daemon local |
WRIT_TOKEN | Sustituye el token bearer del daemon local |
WRIT_HOME | Primer directorio candidato para runtime.json |
WRIT_API_KEY | Clave API medida de Writ Cloud (wt_) |
WRIT_CLOUD_URL | Sustituye la URL base de Writ Cloud |
WRIT_CLIENT_ID | Sustituye el id de dispositivo sin clave |
runs ▸ tres formas de esperar
Ejecuta, y espera a tu manera.
Cada SDK expone las mismas tres posturas para el mismo run:
- Handle async — run() vuelve de inmediato con un id de run — consulta o streamea cuando quieras.
- Espera en el servidor — run con wait — la propia llamada HTTP bloquea hasta que el run se resuelve (timeout en segundos, acotado en el servidor).
- runAndWait — El SDK se suscribe al stream de eventos en vivo con polling de respaldo, y devuelve el run resuelto.
Un run fallido es un resultado, no un error: recibes el run con su estado. Solo un presupuesto de espera agotado lanza — y el error aún lleva el id del run, nada se pierde.
Los elementos del feed de runs llevan un id compuesto como workflow-3. Cada llamada runs.* toma el id numérico de fila — extráelo con el ayudante del lenguaje: runRowId(run) (TS), run_row_id(run) (Python), item.RowID() (Go), item.row_id() (Rust).
Eventos en vivo por SSE
El progreso paso a paso llega en stream desde el daemon; cada lenguaje tiene su idioma nativo — iterador async, generador, range-over-func, Stream.
events.ts
for await (const ev of client.runs.events(runRowId(run))) {
console.log(ev.type, ev);
} events.py
for ev in client.runs.events(run_row_id(run)):
print(ev["type"], ev) events.go
for ev, err := range client.Runs.Events(ctx, rowID) {
if err != nil { break }
fmt.Println(ev.Type, ev)
} events.rs
use futures_util::StreamExt;
use writ_client::RunEvent;
let mut events = agent.runs().events(run_id).await?;
while let Some(ev) = events.next().await {
match ev? {
RunEvent::Step { index, step_type, status, .. } => println!("{index} {step_type} {status}"),
RunEvent::Finished { status, .. } => println!("done: {status}"),
_ => {}
}
} superficie ▸ servicios
Todo el agente, por espacios de nombres.
Un solo objeto cliente lleva toda la superficie: agent, workflows, runs, monitors, selectors, extractors, automations, personas, secrets, vault, files, data, crawl, datasets, keys — más cloud. Los nombres son idénticos entre lenguajes; los idiomas, nativos:
| TypeScript | Espacios de nombres con Promises; sobres Page<T>; run() sobrecargado para wait y dry-run. |
| Python | Gemelos WritAgent síncrono y AsyncWritAgent; respuestas como dicts; Page iterable. |
| Go | Cada método toma ctx primero; errores tipados compatibles con errors.As; cero dependencias. |
| Rust | Solo async; listas filtradas con variantes *_with; Cloud es un CloudClient aparte; un único enum WritError. |
cloud ▸ medido + sin clave
El nivel cloud viene integrado.
Dale al cliente una clave wt_ y scrape, map y crawl en la nube se descuentan de tu fondo de créditos. Sin clave alguna, el nivel keyless scrapea páginas públicas identificado solo por un id de dispositivo — con un endpoint de cuota que dice cuánto queda.
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" cloud.py
cloud = Cloud(api_key=os.environ["WRIT_API_KEY"]) # wt_… — no daemon needed
page = cloud.scrape("https://example.com")
site = cloud.map("https://example.com", search="pricing", limit=20)
print(cloud.tier) # "metered" | "keyless" metered.sh
# Metered — wt_ API key, billed from your credit pool
curl -X POST https://api.usewrit.app/api/crawl/scrape \
-H "Authorization: Bearer $WRIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' keyless.sh
# Keyless — no account, no key: a stable device id is the only identity.
# 429 keyless_rate_limited when the allowance is spent.
curl -X POST https://api.usewrit.app/v1/keyless/scrape \
-H "X-Writ-Client-Id: $WRIT_CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
curl https://api.usewrit.app/v1/keyless/quota -H "X-Writ-Client-Id: $WRIT_CLIENT_ID" El cliente expone su nivel ("metered" o "keyless") para que tu código pueda bifurcar. El sin-clave responde 429 cuando la asignación se agota; el medido responde 402 cuando el fondo está vacío — ambos como errores tipados.
claves ▸ errores
Claves acotadas, fallos tipados.
Acuñar una clave wlk_ acotada (scopes: read, run, admin) exige el token runtime de acceso completo — una clave de CI filtrada nunca puede ampliarse a sí misma.
const key = await client.keys.create({ name: "ci-runner", scopes: "read,run" }); key = client.keys.create("ci-runner", scopes="read,run") key, err := client.Keys.Create(ctx, "ci-runner", "read,run") let key = agent.keys().create("ci-runner", Some("read,run")).await?; # Minting keys requires the full-access runtime token (wlt_)
curl -X POST http://127.0.0.1:8131/v1/keys \
-H "Authorization: Bearer $WRIT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "ci-runner", "scopes": "read,run"}' Taxonomía de errores
El mismo fallo es el mismo tipo en cada lenguaje — captura lo que sepas manejar; el resto lleva status, code y body:
ApiError | Cualquier no-2xx con un código estable: bad_request, unauthorized, forbidden, not_found, vault_locked (423), too_many_requests, internal. |
RunTimeout | Un presupuesto de espera expiró — lleva el id del run, aún válido. |
RateLimited | Asignación sin clave agotada — lleva la hora de reinicio y los contadores restantes. |
InsufficientCredits | Fondo medido vacío (402). Recarga o vuelve a local. |
ApiKeyRequired | Llamada cloud medida sin clave wt_. |
Connection / Discovery | Ningún agente vivo encontrado, o daemon inalcanzable. |
rest ▸ sin sdk
¿Sin SDK? El endpoint es REST puro.
Un endpoint de workflow publicado es un POST HTTPS corriente con Bearer wt_ — estos wrappers son toda la integración si prefieres poseer el HTTP tú mismo.
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"]) writ.ts
const WRIT_BASE = "https://api.usewrit.app";
export async function runWorkflow<T>(slug: string, path: string, inputs: unknown): Promise<T> {
const res = await fetch(`${WRIT_BASE}/v1/${slug}/${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WRIT_CONSUMER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(inputs),
});
if (!res.ok) throw new Error(`Writ ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
} writ.go
package writ
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const Base = "https://api.usewrit.app"
func RunWorkflow(slug, path string, inputs any) (map[string]any, error) {
body, err := json.Marshal(inputs)
if err != nil {
return nil, err
}
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/%s/%s", Base, slug, path), bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("WRIT_CONSUMER_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return nil, fmt.Errorf("writ %d", res.StatusCode)
}
var out map[string]any
return out, json.NewDecoder(res.Body).Decode(&out)
} writ.rs
use serde::Serialize;
use serde_json::Value;
pub const BASE: &str = "https://api.usewrit.app";
pub async fn run_workflow<T: Serialize>(
slug: &str,
path: &str,
inputs: &T,
) -> Result<Value, Box<dyn std::error::Error>> {
let key = std::env::var("WRIT_CONSUMER_KEY")?;
Ok(reqwest::Client::new()
.post(format!("{BASE}/v1/{slug}/{path}"))
.bearer_auth(key)
.json(inputs)
.send()
.await?
.error_for_status()?
.json()
.await?)
} run.sh
# WRIT_CONSUMER_KEY must be exported (csk_...)
curl -sS -X POST https://api.usewrit.app/v1/acme/price-check \
-H "Authorization: Bearer $WRIT_CONSUMER_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/product/42"}' | jq .data referencia ▸ siguiente
Construye la integración
Toda la superficie local + cloud, endpoint por endpoint.
→ AutenticaciónFamilias de tokens, scopes, rotación.
→ Managed endpoints/v1/{slug}/{path} — tus puertas publicadas.
→ Claves de consumidorDistribuir acceso a socios.
→ WebhooksEntregas firmadas y verificación.
→ MCPLos mismos workflows como herramientas para cualquier cliente MCP.
→faq
Preguntas de SDK, respondidas.
¿Necesito un SDK para usar Writ?
¿Qué lenguajes están publicados?
¿Cómo encuentran los SDKs mi agente?
¿Cómo manejo workflows largos?
fin ▸ enviar
Instala uno y haz la primera llamada.
El quickstart ejecuta tu primer workflow en pocos minutos, en local y gratis.