Runs onWrit CloudDesktopSelf-hosted
On this page
Inside your network. Callable from outside.
An internal system with no API becomes one HTTPS endpoint. The agent dials out and stays connected; a call to your endpoint is handed down that connection, and the workflow runs on your machine against a system only it can see.
connection ▸ one direction
The agent dials out. Nothing dials in.
Every part of this rests on one property: the connection is made from inside your network, outward, and it stays open. Work travels down a connection you already opened.
- 01 The agent connects outward
You install the agent on a machine that can already reach the internal system. It opens an outbound connection to Writ and holds it.
- 02 The call is handed down it
A request to your published endpoint is passed down that existing connection. Writ does not open a connection into your network.
- 03 The workflow runs inside
The browser drives the internal system from that machine, so the run sees exactly what someone sitting at that desk would see.
There is no inbound port to open and no public address to give the internal system. What the agent needs is the outbound HTTPS your network already allows.
parts ▸ four
What it takes.
Four things, each documented on its own page. This page is the order to assemble them in.
| A machine inside the network | Writ Desktop or a self-hosted agent, on a host that can already reach the system you want to call. |
| A workflow | Recorded once against that system — or described in words — and pointed at your own agent so it runs there. |
| A published endpoint | The front door on Writ Cloud: a method and a path you choose, served under /v1/{slug}/{path}. |
| A consumer key | What your callers present. Scope it to one endpoint, give it a rate limit, rotate it with a grace window. |
setup ▸ in order
Put it together.
Each step is ordinary product work — nothing here is specific to reaching an internal system except where the agent sits.
- 01 Install and link the agent
On a machine inside the network. Once it is linked it appears in your agent list and holds its outbound connection.
- 02 Teach the workflow
Record the task against the internal system, or describe it and let the AI session record it for you. Credentials resolve from the vault at run time.
- 03 Point it at your agent
Set the workflow to run on your own agent rather than the cloud, so every run happens on a machine that can see the system.
- 04 Publish the endpoint
Choose the method and path. That path becomes the API the internal system never had.
- 05 Hand out a consumer key
One key per caller. Limit it to the endpoints it needs, and revoke or rotate it without touching the workflow.
call ▸ from anywhere
Call it like any API.
Callers do not know or care where the run happens. They post to your path with their key and read the result.
call.sh
curl -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"}' call.py
import os, requests
res = requests.post(
"https://api.usewrit.app/v1/acme/price-check",
headers={"Authorization": f"Bearer {os.environ['WRIT_CONSUMER_KEY']}"}, # csk_...
json={"url": "https://example.com/product/42"},
timeout=120,
)
res.raise_for_status()
payload = res.json()
print(payload["run_id"], payload["data"]) call.ts
const res = await fetch("https://api.usewrit.app/v1/acme/price-check", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WRIT_CONSUMER_KEY}`, // csk_...
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com/product/42" }),
});
if (!res.ok) throw new Error(`Writ call failed: ${res.status}`);
const { run_id, data } = await res.json();
console.log(run_id, data); call.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]string{"url": "https://example.com/product/42"})
req, _ := http.NewRequest("POST", "https://api.usewrit.app/v1/acme/price-check", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("WRIT_CONSUMER_KEY")) // csk_...
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
RunID string `json:"run_id"`
Data json.RawMessage `json:"data"`
}
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out.RunID, string(out.Data))
} call.rs
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = std::env::var("WRIT_CONSUMER_KEY")?; // csk_...
let res: Value = reqwest::Client::new()
.post("https://api.usewrit.app/v1/acme/price-check")
.bearer_auth(key)
.json(&json!({ "url": "https://example.com/product/42" }))
.send()
.await?
.error_for_status()?
.json()
.await?;
println!("{} {}", res["run_id"], res["data"]);
Ok(())
} Calls are synchronous by default. Ask for 202 and a run handle with Prefer: respond-async, and serve a recent result instead of a fresh run with Cache-Control: max-age=N. Response shapes, status codes and the polling path are on the managed endpoints page.
split ▸ who does what
What runs where.
The division is worth being precise about, because it is what makes this safe to expose.
| Side | What it holds |
|---|---|
| Your machine | The browser, the sign-in, the credentials and every byte read from the internal system. The run happens here. |
| Writ Cloud | The front door: authentication, rate limits, quotas and the run record. It holds the caller’s request while your agent works. |
Runs on your own agent are free and unmetered. The cloud front door still applies your plan’s rate limits and monthly call quota.
limits ▸ what caps
Limits worth knowing.
These are the numbers that decide how many doors you can open and how hard callers may knock.
| Linked agents | Free, Starter and Pro include one. Growth allows 5, Scale 20, Enterprise is uncapped. |
| Published endpoints | Capped per plan. Publishing a path consumes one. |
| Rate limit | Per consumer key, 60 requests a minute unless you set your own, plus your organization’s monthly call quota. |
| Timeout | 5–300 seconds per endpoint, 120 by default. Over budget returns 504 with a run handle you can poll. |
reference ▸ the detail
Where each part is documented.
questions ▸ asked
Uplink, answered.
Do I have to open a port?
Does the internal system need an API?
Can two machines serve one endpoint?
Is any of this metered?
end ▸ open one door
Put one internal system behind one endpoint.
Start with the endpoint mechanics, then decide where the run should happen.