Runs onWrit CloudDesktopSelf-hosted
On this page
Workflows.
A workflow is an ordered list of steps that runs in a real browser and returns structured data. Author it once — by recording or by describing — then run it on demand, on a schedule, from a webhook, or as a published endpoint and MCP tool.
Writ runs on your own accounts, with your own credentials and data, on sites you are authorized to use.
object ▸ the shape
The workflow object.
A workflow is plain JSON: a name, an ordered steps array, and the declared-input defaults its placeholders resolve against. Each step is a small object with a type and a config.
{
"name": "Product extractor",
"description": "Prices from the catalog",
"workflow_type": "recorded",
"steps": [
{ "type": "navigate", "config": { "url": "{{url}}" } },
{ "type": "extract", "config": { "fields": {
"title": ".product .title",
"price": ".product .price"
} } }
],
"form_data": { "url": "https://example.com/catalog" },
"timeout_ms": 120000,
"headless": true
} steps ▸ the vocabulary
30+ step types.
Steps run in order, and each one can read what earlier steps produced. The vocabulary spans navigation, interaction, waiting, extraction, tabs, AI, authentication and flow control — every type is listed in the step reference with its fields, a real example and its behavior.
Navigation
Interaction
Waiting
Extraction
Tabs
AI
Authentication
Flow
io ▸ inputs and outputs
Inputs in, structured data out.
A run carries its inputs in the form_data body field. Inside step values, placeholders resolve at run time — so the recipe stays generic and nothing sensitive is stored in it:
| Placeholder | Resolves to |
|---|---|
{{key}} | The matching key from the run’s form_data, falling back to the workflow’s saved defaults. |
{{vault:name}} | A secret from your vault, injected at run time. Secrets are interpolation inside a step value — never a step of their own. |
{{extracted:key}} | A value a previous step extracted in this same run — for chaining api_call requests. |
{{file:slot}} | A stored file bound to the named slot (uploads, captured downloads). |
On the way out, extract steps fill extracted_data and the run settles with result_data — the structured payload your caller reads.
run ▸ the api
Running a workflow.
One endpoint starts a run. By default the call waits for the verdict; turn wait off to get a task handle back immediately.
POST /api/v1/workflows/{workflow_id}/runs?wait=true&timeout=120
Authorization: Bearer wt_xxxxxxxxxxxx
{ "form_data": { "url": "https://example.com/catalog" } }
# wait=true (default) — the call blocks until the run settles:
{
"status": "success",
"success": true,
"result_data": { "title": "…", "price": "…" },
"extracted_data": { "title": "…", "price": "…" },
"error": null,
"duration_ms": 8412
}
# wait=false — returns immediately with a task handle:
{ "task_id": "…", "status": "pending", "workflow": { "…": "…" } } | Query param | Role |
|---|---|
wait | Default true — the HTTP call blocks until the run settles. |
timeout | How long to wait, in seconds. Default 120, accepted range 10–300. |
With wait=false the response is {"task_id", "status": "pending", "workflow"} — poll the run, or subscribe to its events.
From the SDKs
On your own machine, the published SDKs discover the local agent and run the same workflow with no compute charge:
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); Where a run executes decides what it costs: your local agent runs it for free; a cloud run is metered from your plan’s included usage — see billing.
lifecycle ▸ eight statuses
The run lifecycle.
Every run reports one of eight normalized statuses:
| Status | Meaning |
|---|---|
queued | A cloud run waiting for a slot — it exposes its place in line and an ETA. |
pending | Dispatched toward a desktop agent and waiting to be picked up. No queue position — the agent pulls when ready. |
running | Steps are executing in a live browser. |
repairing | AI repair is working on the workflow. An overlay state while the repair holds the workflow, not a stored status. |
success | The run settled and its outputs are available. |
failed | The run settled with an error — the error field says why. |
cancelled | Stopped on request before it settled. |
skipped | Not executed — for example held back by its own configuration. |
queued vs pending: queued is cloud-side (a slot will open; you can see how far back you are). pending is desktop-bound (your agent picks the run up when it connects) — it has no place-in-line to show.
The runs feed unifies five run types in one stream — workflow, check, ai_session, automation and crawl — so everything that executed shows up in one place with the same statuses.
Live events
While a run executes, step-by-step progress streams over SSE — each SDK exposes it in its native idiom:
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}"),
_ => {}
}
} limits ▸ per plan
How long a run may take.
Every plan sets a maximum run duration. A run that reaches its cap is stopped and settles as failed — it cannot bill open-endedly.
| Plan | Max run duration |
|---|---|
| Free | 2 min |
| Starter | 4 min |
| Pro | 5 min |
| Growth | 10 min |
| Scale / Enterprise | 15 min |
Cloud recording sessions have their own cap: 10 minutes on Free, up to 60 minutes on Scale and Enterprise. Streaming sessions are capped separately — see the streaming reference.
repair ▸ opt-in ai
AI repair.
Sites change. With ai_repair_enabled on a workflow (off by default), a run that breaks on a stale selector triggers repair instead of just failing. Repair works in two tiers:
| Selector repair | The selector is re-derived on the live page; a validated candidate replaces the stale one and the step is retried in place. |
| Grounded re-record | For structural changes, a live browser is driven through the flow again and the recipe is re-recorded from what actually works now. |
Repair always runs on the managed cloud AI service and is metered by the tokens it uses — it never runs on a BYO key.
While a workflow is repairing it is locked: other queued runs of the same workflow are held until the repair clears, so they don’t all fail on the same broken step.
Each workflow keeps its last 50 repair entries, each tagged repair_type selector or rerecord — you can audit exactly what was changed and why.
Honest failure by default. With the flag off, a broken selector fails the run and says so. There is no silent fallback-selector chain and no non-AI “self-heal” — a run either replays the recipe as recorded, or repair (opted in) fixes it in the open.
next ▸ where to go
Keep going.
- Step reference — every step type, its fields, and a real example.
- AI sessions — the describe path: goal in, recorded workflow out.
- Managed endpoints — turn this workflow into a REST endpoint; MCP makes it an agent tool.
- Billing & usage — exactly how a cloud run is metered.