S’exécute surWrit CloudAuto-hébergé
Sur cette page
Quatre SDKs. Un agent.
TypeScript, Python, Go et Rust — publiés, versionnés, légers. Chaque client découvre l’agent Writ qui tourne sur votre machine, et le même client atteint Writ Cloud quand vous lui confiez une clé wt_.
install ▸ premier run
Installer, découvrir, exécuter.
Chaque quickstart suit les trois mêmes temps : le client trouve l’agent en cours d’exécution (pas d’URL, pas de token à coller), liste vos workflows, en exécute un et relit les lignes extraites. Ces exemples sont les paquets publiés, mot pour mot.
| TypeScript | typescript/ | Depuis le dépôt · Node ≥ 18 · zéro dépendance runtime |
| Python | python/ | Depuis le dépôt · Python ≥ 3.10 · import writ_agent |
| Go | github.com/usewrit/writ-sdks/go | go get · Go ≥ 1.23 · stdlib uniquement |
| Rust | rust/ | Dépendance git · async, tout runtime compatible 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"}' surfaces ▸ deux
Un client, deux surfaces.
Les SDKs parlent à deux endroits différents, et la doc ne les confond jamais. L’agent local est le logiciel sur votre machine : loopback uniquement, gratuit, avec ses propres familles de tokens. Writ Cloud est la surface hébergée qu’une clé wt_ déverrouille — avec un palier sans clé qui ne demande aucun compte.
| Surface | URL de base | Auth |
|---|---|---|
| Agent local (writ-agentd) | http://127.0.0.1:8131 · https://127.0.0.1:8132 | token runtime wlt_ · clé restreinte wlk_ · OAuth wlo_ |
| Writ Cloud | https://api.usewrit.app | clé API wt_ (facturée) · X-Writ-Client-Id (sans clé) |
Parlez à 127.0.0.1, pas à localhost — le daemon applique une garde anti DNS-rebind sur le Host et l’Origin acceptés. Le jumeau HTTPS sur :8132 utilise une CA locale par installation dans ~/.writ/tls/ca.pem.
Variables d’environnement
La découverte lit d’abord l’environnement, puis les runtime.json du dossier Writ, en sondant chaque candidat. Noms identiques dans les quatre SDKs :
| Variable | Rôle |
|---|---|
WRIT_API_URL | Remplace l’URL de base du daemon local |
WRIT_TOKEN | Remplace le token bearer du daemon local |
WRIT_HOME | Premier dossier candidat pour runtime.json |
WRIT_API_KEY | Clé API Writ Cloud facturée (wt_) |
WRIT_CLOUD_URL | Remplace l’URL de base de Writ Cloud |
WRIT_CLIENT_ID | Remplace l’identifiant d’appareil sans clé |
runs ▸ trois façons d’attendre
Exécutez, puis attendez à votre façon.
Chaque SDK expose les trois mêmes postures pour le même run :
- Handle async — run() rend la main immédiatement avec un id de run — interrogez ou streamez quand vous voulez.
- Attente côté serveur — run avec wait — l’appel HTTP lui-même bloque jusqu’au verdict (timeout en secondes, borné côté serveur).
- runAndWait — Le SDK s’abonne au flux d’événements en direct avec un polling de secours, et rend le run une fois réglé.
Un run en échec est un résultat, pas une erreur : vous récupérez le run avec son statut. Seul un budget d’attente expiré lève — et l’erreur porte encore l’id du run, rien n’est perdu.
Les éléments du fil de runs portent un id composite comme workflow-3. Chaque appel runs.* prend l’id numérique de ligne — extrayez-le avec l’assistant du langage : runRowId(run) (TS), run_row_id(run) (Python), item.RowID() (Go), item.row_id() (Rust).
Événements en direct via SSE
La progression étape par étape est diffusée par le daemon ; chaque langage a son idiome natif — itérateur async, générateur, 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}"),
_ => {}
}
} surface ▸ services
Tout l’agent, par espaces de noms.
Un seul objet client porte toute la surface : agent, workflows, runs, moniteurs, selectors, extractors, automations, personas, secrets, vault, files, data, crawl, datasets, keys — plus cloud. Les noms sont identiques d’un langage à l’autre ; les idiomes sont natifs :
| TypeScript | Espaces de noms en Promises ; enveloppes Page<T> ; run() surchargé pour wait et dry-run. |
| Python | Jumeaux WritAgent sync et AsyncWritAgent ; réponses en dicts ; Page itérable. |
| Go | Chaque méthode prend ctx en premier ; erreurs typées compatibles errors.As ; zéro dépendance. |
| Rust | Async uniquement ; listes filtrées via variantes *_with ; Cloud = CloudClient séparé ; un seul enum WritError. |
cloud ▸ facturé + sans clé
Le palier cloud est intégré.
Confiez une clé wt_ au client et scrape, map et crawl cloud sont décomptés de votre pool de crédits. Sans clé, le palier keyless scrape des pages publiques identifié par un simple id d’appareil — avec un endpoint de quota qui dit ce qui reste.
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" Le client expose son palier (« metered » ou « keyless ») pour que votre code puisse bifurquer. Le sans-clé répond 429 quand l’allocation est épuisée ; le facturé répond 402 quand le pool est vide — deux erreurs typées.
clés ▸ erreurs
Clés restreintes, échecs typés.
Fabriquer une clé wlk_ restreinte (scopes : read, run, admin) exige le token runtime à accès complet — une clé CI qui fuite ne peut donc jamais s’élargir elle-même.
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"}' Taxonomie des erreurs
Le même échec est le même type dans chaque langage — attrapez ce que vous savez gérer, le reste porte status, code et body :
ApiError | Tout non-2xx avec un code stable : bad_request, unauthorized, forbidden, not_found, vault_locked (423), too_many_requests, internal. |
RunTimeout | Un budget d’attente a expiré — porte l’id du run, toujours valide. |
RateLimited | Allocation sans clé épuisée — porte l’heure de reset et les compteurs restants. |
InsufficientCredits | Pool facturé vide (402). Rechargez ou repassez en local. |
ApiKeyRequired | Appel cloud facturé sans clé wt_. |
Connection / Discovery | Aucun agent vivant trouvé, ou daemon injoignable. |
rest ▸ sans sdk
Pas de SDK ? L’endpoint est du REST pur.
Un endpoint de workflow publié est un POST HTTPS ordinaire avec un Bearer wt_ — ces wrappers sont toute l’intégration si vous préférez posséder le HTTP vous-même.
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 référence ▸ suite
Construire l’intégration
Toute la surface locale + cloud, endpoint par endpoint.
→ AuthentificationFamilles de tokens, scopes, rotation.
→ Managed endpoints/v1/{slug}/{path} — vos portes publiées.
→ Clés consommateurDistribuer l’accès à des partenaires.
→ WebhooksLivraisons signées et vérification.
→ MCPLes mêmes workflows comme outils pour tout client MCP.
→faq
Questions SDK, répondues.
Ai-je besoin d’un SDK pour utiliser Writ ?
Quels langages sont publiés ?
Comment les SDKs trouvent-ils mon agent ?
Comment gérer les workflows longs ?
fin ▸ livrer
Installez-en un et faites le premier appel.
Le quickstart exécute votre premier workflow en quelques minutes, en local et gratuitement.