Writ Cloud
Server Details
Read, crawl and act on websites, signed in as the user, and turn any site into an API.
- Status
- Healthy
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-06-18
- URL
- Repository
- usewrit/writ-mcp
- GitHub Stars
- 1
- Server Listing
- writ-mcp
TDQS
Score is being calculated.
Available Tools
39 toolswrit_browser_actAct in a browser sessionDestructiveInspect
Runs one batch of actions on an open browser session and returns the fresh page. The caller is the session's brain: Writ runs no model here, and a batch performs exactly the navigations, clicks, fills, captures, probes and scripts it carries. It also composes, for clients without writ_browser_compose: actions=[{action:'define_function', name:'feed.list', from_index:3, ...}] or [{action:'compose', operation, payload}]; a batch mixing these with page actions is refused. Actions placed after a navigate, click or select that changes the page run against a page the caller has not seen yet. RECORDING RULES: {{name}} as an action value (select/fill/type_text value, navigate url) with the real value in inputs ({"name": "real value"}) declares the workflow input name: the page gets the real value and the step keeps {{name}}. An extract {variable,script} (a read-only JS IIFE returning rows/fields) at the position that shows the data records it, and its result comes back in this answer. A fill with data_key holds a secret server-side and the saved step keeps a {{secret:...}} placeholder. Interactions are recorded as steps; SEE/HEAR/NETWORK probes and wait never are: replay waits for each step's selector by itself, and a wait the task needs is an explicit wait_for step (writ_browser_compose add_steps). ACTIONS: DRIVE: navigate {url} · click {selector | field_index | button_index} · fill {selector,value,data_key?,human_layer?} · type_text {selector,value} · select {selector,value} · check {selector} · hover {selector} · submit {selector} · press_key {key} · scroll {direction,amount} · back · wait {seconds} · wait_for {selector,timeout}. SEE (granular first): query_dom {selector,limit,offset,attrs?,text_chars?,html_chars?} (every match as compact records, each with a css path) · count {selector} · find_text {text,selector?,exact?,limit?} (the deepest elements showing that text, with paths) · get_attributes {selector,index?} (one element: all attrs, value, box, options) · read_text {selector,all?,limit?,max_chars?} · inspect {selector,limit?,max_chars?} (match count + outerHTML) · list_candidates (the page's repeating row shapes, the entry point for a list/table) · list_frames · get_dom {selector?,depth?,max_chars?} (the real cleaned HTML, the most expensive read) · get_screenshot {x?,y?,width?,height?}. TABS / FILES / 2FA: list_tabs / switch_tab {index} · upload {selector,mode,file_slot} · wait_for_download {trigger_selector,output_key} · twofa {challenge_method,selector?,submit_selector?} (the persona's one-time code, minted server-side; covered by 2FA RULES). HEAR: get_console {level?,since?,query?,exclude?,limit?} (console messages, uncaught JS errors with stack, failed/blocked requests since the last read; the page's console_since_last_read counts show when there is something new; it shows why a sign-in, click or extraction did nothing) · page_errors (only the uncaught exceptions). NETWORK: capture_network {reload?} (the backend calls the page makes, i.e. the site's real API; writ_browser_network searches and reads them) · get_request {url substring} (one call in full) · rotate_exit {reason} (alone in its batch: restarts on a fresh residential address when the site refused the current one, e.g. a sign-in rejected with correct credentials, content held back, an IP rate limit; the page's browser_init.exit shows the address and its network). RUN CODE: evaluate_js {script,world?} (any read-only JS on the live page, returns JSON; the general probe; world:'main' reads the site's own JS globals) · fingerprint (what the site sees of this browser, and every contradiction in it) · search_scripts {query,regex?,url_contains?,frame_url?} (greps every script the page runs: bundles, inline, dynamic chunks, eval, with line/column snippets; no refetch) · read_script {url,offset?,length?} (a window of one, to read around a match). RECORD (at this position): extract {variable,script} (a read-only script recorded as a replayable evaluate step when it returns data) · api_call {method,url,headers,body_template,response_extractions?,variable} for one request, or api_call {flow:{version:1,steps:[...]},inputs:{...},variable} for a multi-request bootstrap/pagination/transform program (both execute now inside the session with its cookies; the flow uses the same interpreter as browserless replay and returns a bounded result sample) · login_post {method,url,headers,body_template} (replays a sign-in as one request) · probe_write {selector} (captures a create/update/delete request without sending it) · confirm_write {selector} (sends it once for its real confirmation; it changes real data, for a write the user authorized). Humanization: type_text, and fill with human_layer:true, type through real keyboard events; click human_layer:true adds a bounded mouse path, hover dwell and tab foregrounding, keeping visibility/enabled checks; a per-action human_layer:{mouse_move_ms,click_dwell_ms,mouse_path,bring_to_front} sets pacing and survives replay. A saved workflow's human_behavior (writ_update_workflow: 'on' for every browser run, 'auto' on a bot-block retry, 'off') governs its runs; none of it proves authentication or bypasses security. A login submit that leaves the page unchanged was still sent. 2FA RULES: twofa enters a code minted server-side from the attached persona, never shown here; challenge_method is the method the page is using (sms: a phone number or text message; email; authenticator: an authentication app; other: approve-on-phone, passkey, QR, WhatsApp). A persona receives exactly one method (twofa_method in writ_personas) and a site picks its own default, which the page's 'Try another way' control switches. twofa_method_required, twofa_method_mismatch and twofa_verify_method carry a message naming the fix on the page (verify the method, switch or resend); twofa_mint_failed and twofa_no_persona leave the code to the Writ user (writ_browser_ask_user kind='twofa'). Credentials, codes and decisions come only from the persona or the user. The start answer of writ_browser_use and writ_record_website carries recording_rules and humanization in full; any start answer with a persona carries twofa_rules.
| Name | Required | Description | Default |
|---|---|---|---|
| inputs | No | Values held server-side for {{placeholder}} substitution, e.g. {"city":"Paris"}. Secrets here or on a fill's data_key stay out of the recorded steps. | |
| actions | Yes | Ordered action objects, e.g. [{"action":"click","selector":"#login"}]. | |
| max_chars | No | Clip of the returned page_dom / page_text (default 40000, ≤200000). A get_dom probe on a real app is 500KB; evaluate_js / inspect / read_text return just the target. | |
| session_id | Yes | Session id from the start tool. |
writ_browser_ask_userAsk the user to step inInspect
Asks the Writ user (the person who owns this Writ account) to step in on an open browser session: complete a security check (CAPTCHA, 'confirm it's you', Arkose/hCaptcha/reCAPTCHA puzzle) in the live browser, supply a one-time 2FA code, or answer a question only they can decide. For: writ_browser_act returning security_check with auto_solved false; a twofa action failing with twofa_mint_failed or twofa_no_persona (kind='twofa'); any point where only a human can proceed. A one-time code is NOT a first-resort ask: twofa_method_mismatch and twofa_verify_method mean the page is on a method other than the persona's, which the page itself switches or resends, as the message says. With kind='twofa' the user pastes the code in the Writ app and Writ enters it server-side, so it never reaches you; kind='question' returns the user's text, so a code sent that way would reach the conversation. The session pauses (the user is notified in the app and by email and controls the live page); this call holds up to 60s and returns status 'answered' (with solved / answer, or entered for twofa), 'waiting_for_user' (the same call with the same session_id keeps waiting while the user holds the page), or 'expired'.
| Name | Required | Description | Default |
|---|---|---|---|
| kind | No | captcha: the user completes a check in the live browser. question: the user answers in text. twofa: the user supplies the one-time code the page is asking for; Writ types it server-side and returns `entered`, never the code. | |
| question | No | The question, in one short sentence (required for kind 'question'). | |
| session_id | Yes | ||
| wait_seconds | No | Hold up to this long (1-60, default 60). |
writ_browser_cancelClose a browser sessionDestructiveIdempotentInspect
Closes an open browser session. For: a finished task the user does not want to reuse, or an abandoned session. An open cloud browser keeps consuming execution time until it is closed. Unsaved work is kept: a session with defined functions, or a recording that did more than visit pages, is auto-saved as a workflow first (the reply names it; an inactive draft when it would not replay). discard=true closes without keeping anything. A session bound to a build is settled by that auto-save, or marked cancelled.
| Name | Required | Description | Default |
|---|---|---|---|
| discard | No | true = the session's unsaved steps and functions are thrown away instead of auto-saved. Default false. | |
| session_id | Yes |
writ_browser_composeCompose a workflow in a sessionDestructiveInspect
Authors the workflow being built in an open browser session: named functions, inputs and explicit steps that make what the session drove a real, complex, callable workflow rather than a replay of clicks.
WHEN IT IS NEEDED: a plain recording needs none of it (there a caller input is a {{name}} value + inputs on writ_browser_act, and the data is an extract action). It exposes named functions (an API), gives an input a description/default, and adds steps the recorder cannot see.
OPERATIONS: define_function {name, fn_type api|list|script|extraction, ...} · compile_function {name, from_index}: deterministic (no-LLM) capture->function that traces session tokens to an is_auth bootstrap, generates per-call ids ({{uuid()}}), and marks a write so the build never sends it (a real run does) · test_function {name, sample_inputs} · remove_function {name} · set_inputs {inputs:{name:{default?,description?,required?,example?}}} · add_steps {steps:[{type,...}], at?} · remove_step {id} · list (the draft: steps, data_steps, functions, inputs, build).
Fastest paths: (a) a list / table / search-results page: writ_browser_context section=lists returns a live-tested define_function payload, and with then_save:{name} one call here defines, tests and saves it. (b) a site endpoint: once capture_network and writ_browser_network locate the call, define_function {name:'quotes.list', from_index:, request:{url:'https://site/api/quotes?page={{page}}'}, input_variables:[{name:'page',example:'1'}], response_extractions:{quotes:{from:'json',path:'quotes'}, has_next:{from:'json',path:'has_next'}}, then_save:{name:'...'}}. Every function is live-tested as it is defined (an in-session request, or a DOM read, with sample_inputs={name: value}); a failed test returns feedback and keeps nothing, and a corrected definition reuses the same name. test=false skips the proof (a real run proves it later). Saved functions run through writ_run_workflow function_name.
DETAILS:
define_function: a named callable the saved workflow exposes. fn_type api (backed by one of the site's endpoints: from_index=<a captured call's index from writ_browser_network> seeds method/url/headers/body from the capture; overridden request fields take {{name}} placeholders, secrets {{secret:name}}, anti-CSRF echoes {{cookie:NAME}}), script (a read-only JS IIFE returning the data from the page), list (the generated-JS form for any list/table: row_selector + fields {name: sub-selector | {selector, attr}}; writ_browser_context section=lists returns this payload ready-made), or extraction (one selector's text). A list/script/extraction function reads the page it was defined on: page_url as a template (https://site/search?q={{query}}), or an
exampleon each input_variable from which the URL is templated. then_save:true (or {name, description}) saves the workflow the moment the function passes its live test. Names of the form . (orders.list, orders.create) group functions by surface. input_variables=[{name,description,required,example}], output_fields and response_extractions declare the fields callers pass and get back. Supported specs: JSON {from:'json',path:'data.items'}, embedded JSON {from:'embedded_json',kind:'array',has:['id']}, server HTML {from:'html_css',selector:'.row',attribute:'data-id',all:true}; withfieldsit returns row objects, which is how a server-rendered list becomes a BROWSERLESS function: {from:'html_css',selector:'tr.athing',all:true,base_url:'',fields:{title:{selector:'.titleline > a'},url:{selector:'.titleline > a',attribute:'href'}}} on anapifunction that GETs the page (no browser at replay, so cheaper than alist/scriptfunction whenever the rows are in the served HTML), regex {from:'regex',pattern:'...',group:1}, header {from:'header',name:'x-next'}, body {from:'body'}, or legacy '$.json.path'. The default shape is an Auphan-style named graph: ordered is_auth functions publish dynamic token/id/origin values consumed as {{extracted:name}}, while each data function remains independently callable. flow={version:1,steps:[...]} is for request loops, recursive mapping, cross-page dedupe, cursor pagination or a composite return; it is schema-validated and live-tested at once in the current browser session with its cookies, persona and egress, using the same interpreter as the saved HTTP lane, and the later saved run remains the final engine=http parity proof. is_auth=true marks the sign-in function: it runs first on every replay and its response_extractions publish values the others consume as {{extracted:}}.test_function {name, sample_inputs}: proves a defined function again.
remove_function {name}.
add_steps {steps:[...], at?}: explicit replayable steps the DOM recorder cannot see (navigate, click, fill, select, press, wait, wait_for, extract, evaluate, api_call, login_post, return, upload, wait_for_download), inserted at a position (default: append).
remove_step {id}.
set_inputs {inputs:{name:{default?,description?,required?,example?}}}: the parameters a caller passes at run time. A save is refused unless every {{name}} in a step or function is a declared input, a credential, a {{cookie:}}/{{extracted:}} runtime reference, or produced by an earlier step. Credentials are never inputs: they come from the persona or a data_key fill.
list: the draft so far. On writ_browser_save, api functions become api_call steps (auth first), the workflow becomes api_recorded when every step is a call, and each function is callable by name (writ_run_workflow function_name) and documented at GET /api/v1/workflows/{id}/api-docs.
| Name | Required | Description | Default |
|---|---|---|---|
| payload | No | The operation's arguments. define_function: {name, fn_type?, description?, surface?, from_index?, request?{method,url,headers,body_template}, flow?{version,steps}, script?, selector?, input_variables?, output_fields?, response_extractions?, is_auth?, order?, sample_inputs?, test?}. compile_function {name, from_index, sibling_index?, input_variables?, response_extractions?, page_url?}: the deterministic (no-LLM) way to turn a captured authenticated request (a GraphQL/RPC POST, a form submit) into a callable function. Writ decodes the body, keeps the static parameters, traces each session-minted token (csrf/xsrf/dtsg/lsd/etc.) to where a fresh session re-reads it and emits an is_auth bootstrap that publishes it as {{extracted:}}/{{cookie:}}, replaces per-call client values (idempotence token, session id, timestamp) with runtime generators ({{uuid()}}, {{uuid(session)}}, {{timestamp_ms()}}, {{counter()}}), templates the caller inputs, and classifies a write (create/post/send/delete): the build never sends it, a real run does (mutation_mode='dry_run' previews it). It suits any authenticated mutation or token-bound endpoint better than a hand-built api function. A second capture of the same request (sibling_index, else the nearest one Writ finds) tells the pagination inputs and per-call values from stable ids: a UUID unchanged between the two is kept as captured. test_function: {name, sample_inputs?}. remove_function: {name}. add_steps: {steps:[{type, config|flat fields, description?}], at?}. remove_step: {id}. set_inputs: {inputs:{name:{default?, description?, required?, example?}}}. | |
| operation | Yes | ||
| session_id | Yes | Session id from the start tool. |
writ_browser_contextRead a browser sessionRead-onlyIdempotentInspect
Reads context for an open browser session. section=page (default) re-reads the live page: url, form fields, buttons, links and the cleaned DOM. section=map reads the build this session is bound to: the pages, captured calls and candidate functions Writ's earlier rungs found (evidence, not yet verified), plus what has been composed so far. section=lists scans the live page at no AI cost: the repeating rows, their field selectors, which captured request carries them, and a live-tested define_function payload that writ_browser_compose accepts as is, so a guided session opened on a results URL becomes one list/search API with that payload and a save, without probing the page by hand. section=explorer pages through Writ's full recording policy; section=concierge_api pages through the API-builder policy. The policy sections are reference for unusual flows (logins, multi-request chains), not a prerequisite.
| Name | Required | Description | Default |
|---|---|---|---|
| offset | No | Paging offset for the policy sections. | |
| section | No | page (default) | lists | map | explorer | concierge_api | |
| max_chars | No | Characters per page (1000–10000, default 8000). | |
| session_id | Yes |
writ_browser_networkRead a session's network callsRead-onlyIdempotentInspect
Searches or reads the requests the live page has made: the site's real backend API, as opposed to its HTML. operation=search lists matching calls (filtered by query / method); operation=detail returns one call in full by index (method, url, request headers, request body, response body). Indices are stable for the session, so one from an earlier search still resolves later; only the oldest calls age out of the retained window, and asking for one of those says so rather than returning a different call. Calls come from the capture_network action of writ_browser_act, which reloads the page with capture armed; a POST (login/search/submit) is caught by performing the action that triggers it, then capturing. Any call's index feeds writ_browser_compose define_function as from_index=, turning it into a callable function. Held credential values are replaced with their placeholder in the output.
| Name | Required | Description | Default |
|---|---|---|---|
| index | No | Which call to read, for operation=detail. | |
| query | No | Substring filter across method, url, status, and bodies. | |
| method | No | Filter by HTTP method. | |
| offset | No | ||
| max_chars | No | Window size, 1000-10000 (default 8000; larger values are clamped); offset pages it. | |
| operation | No | search (default) | detail. list/get are aliases. | |
| session_id | Yes |
writ_browser_saveSave a session as a workflowDestructiveInspect
Saves the open browser session as a clean, replayable workflow and closes the browser. Everything composed with writ_browser_compose is materialized: named api functions become api_call steps with the auth function first, declared inputs become the workflow's parameters, explicit steps land at their position. The saved workflow is active immediately: it runs on demand through writ_run_workflow (function_name calls one function) or its own run_ tool (writ_pin_workflow_tool) at zero AI cost, can be scheduled with writ_set_schedule, exposed as a REST endpoint with writ_expose_workflow_api, and documented at GET /api/v1/workflows/{id}/api-docs. A session bound to a build (writ_website_to_api guided) settles that build. The save is refused, with the reasons, when a step or function references something no run could resolve; the browser then stays open. A workflow is only as sound as the task's run on the live page.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Short workflow name (defaults to the goal). | |
| keep_open | No | Leaves the browser open after saving (default false: saving closes it). | |
| session_id | Yes | ||
| description | No | ||
| allow_no_data | No | Saves a workflow that yields no data (navigation/actions only) on purpose. Off by default: a save with no data step and no defined function is refused for an API build (no define_function or extract yet) and warned for a task recording: such a workflow runs green and returns nothing. |
writ_browser_sessionsList open browser sessionsRead-onlyIdempotentInspect
Lists the cloud browser sessions this account has open. An open session is warm, parked on its current page, and keeps billing while it stays open; its session_id resumes it in writ_browser_act / writ_browser_context without a second browser beside it (a new writ_browser_use opens one). Returns each session's id, status, resumable flag, current url and goal.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max sessions to return (default 20). | |
| include_closed | No | Also lists recently closed sessions (not resumable) for reference. Default false: only open, resumable sessions. |
writ_browser_useOpen a browserDestructiveInspect
A REAL CLOUD BROWSER FOR A TASK ON A WEBSITE: the user's own signed-in account (email, social, shop, bank or work portal, through a persona_id from writ_personas: the password stays sealed in Writ, and sessions never ask for a password), a click, form, submit, search inside an app, setting change, booking or post, or a page a plain fetch cannot open (login wall, 403, CAPTCHA).
OPENS a real cloud browser and returns the first live page observation. It is not an autonomous agent: Writ runs no model here, the caller is the brain and the driver, and each writ_browser_act(session_id) batch performs exactly the actions it is given. One call opens one browser for one task and performs no step itself.
RECORDING IS ALWAYS ON, saving is on demand: every interaction in the session is recorded. A task recorded for reuse carries each value that changes as {{name}} with its real value in writ_browser_act inputs, and its data leaves through an extract action; writ_browser_save(name) then answers with the steps, inputs and a run_example, and the workflow replays at zero AI cost (writ_run_workflow). writ_browser_cancel closes an unsaved session; an open browser bills until it is closed.
In the session: navigate, click, fill, type, select, press keys, scroll, switch tabs, upload a file, sign in (a persona's 2FA code is minted server-side); see the page (read_text, get_dom for the real HTML, inspect a selector, list_candidates for repeating rows, get_screenshot); read every backend call the page makes (capture_network, then search/read them with writ_browser_network); run any JavaScript on the live page (evaluate_js) and call the site's backend from inside the session with its cookies (api_call); work a page whose content only appears after interaction.
NOT FOR: reading a page, a few pages, or the top N items of a listing (writ_scrape: one call, 2-10s, no browser); collecting a site into a dataset (writ_crawl_site); turning a site into an API (writ_website_to_api, which opens the same browser bound to a build). A browser costs execution time for as long as it is open.
The page comes back after every batch and on demand via writ_browser_context(section=page). A sensitive fill carries data_key: the value is held server-side and the saved step keeps a placeholder, never the raw value. writ_browser_compose adds what the recorder cannot see (named functions, explicit steps, an input's description/default). A saved workflow replays the same task without a browser (writ_list_workflows -> writ_run_workflow); the first observation lists this site's matching ones in saved_for_this_site. A browser's exit IP is fixed once it opens: past a bot wall or CAPTCHA, a new session with use_residential=true exits residential (writ_browser_cancel closes the blocked one). writ_browser_sessions lists open sessions; an open one is warm and cheaper to continue than a new one.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Starting URL to open (required). | |
| goal | No | Optional label describing the task, for the run log. A label only: the tool performs no step from it, and every step is a writ_browser_act batch. | |
| auth_mode | No | reuse verifies unknown saved authentication before adopting it; fresh_login starts without old cookies/storage while preserving the persona's device and network identity, the mode for recording a new login. | |
| fresh_exit | No | Residential only: skips the persona's usual exit for this site and draws a new address, for after the site refused it (an IP rate limit, a sign-in rejected with correct credentials). Ignored on server-IP egress. | |
| persona_id | No | Saved identity to sign in with (writ_personas lists them). Required for sites behind a login with 2FA. A desktop persona ('device:…') opens the browser on its own desktop, which fills {{secret:username}} / {{secret:password}} / 2FA itself; its values never reach the caller. | |
| human_layer | No | Native keyboard input, distance-adaptive mouse paths and pre-click dwell for this session. Fresh login defaults to enabled; explicit false disables it. Native actionability checks remain active. | |
| use_residential | No | Opens on the platform residential network (premium), for a site that blocks datacenter IPs or shows a bot wall / captcha. Default off (free). | |
| execution_target | No | Where the session runs: 'cloud' (managed cloud fleet), 'auto' (the user's own linked Writ desktop app when it is online, else cloud), or 'local' (only their own desktop app). On the user's own machine the session keeps their computer and IP and exposes nothing local to the cloud. No value means the account's default set in the Writ app. A 'local' run reports local_agent_offline when their app is closed; execution_target='cloud' re-runs it in the cloud, off the user's machine. | |
| residential_country | No | Two-letter ISO country the residential exit should be in (e.g. 'us', 'fr'), for a site that serves a different page per country or throttles foreign traffic. No value means an automatic exit. Ignored unless the session egresses residential. |
writ_crawl_filesList a crawl's captured filesRead-onlyIdempotentInspect
The original documents a crawl captured (PDFs, office docs, images, CSVs) as stored files: filename, size, version, source_url, and a short-TTL download_url fetchable with no further auth. The crawl's dataset holds the extracted text; this returns the files themselves. crawl_id selects one run; crawl (saved crawl slug/name/id) its most recent completed run(s).
| Name | Required | Description | Default |
|---|---|---|---|
| runs | No | With `crawl`: how many recent completed runs to aggregate (default 1 — the current version of every document). | |
| crawl | No | Saved crawl slug, name, or id (alternative to crawl_id). | |
| limit | No | Max files to return (default 100, cap 200). | |
| crawl_id | No | Crawl id from writ_crawl_site. |
writ_crawl_siteCrawl a site into a datasetDestructiveInspect
COLLECT A SITE (or a section of it) into a dataset: a distributed Dragnet crawl that discovers pages and stores each one as a queryable, change-tracked row. For: 'crawl ', 'every page of the docs', 'all products in this category', 'a dataset of ', or anything queried, exported, monitored or re-run later. NOT FOR: reading one or a few pages now, which is writ_scrape (url / urls / top_n answer in one call, no dataset); acting on a page, which is writ_browser_use.
Modes: all three fetch pages the same way and differ in who reads each page.
CLASSIC (default: extract_mode='markdown', executor='regular'): every page becomes clean markdown, no AI spent, fastest. Fits content, docs, articles, discussions (threads keep [top-level]/[reply · depth N] tags), and any ask the two modes below do not cover.
SCHEMA (extract_mode='schema' + extract_schema): every page holds the same structured record (a product, a listing row), returned as rows, not prose. Deterministic CSS extraction, no AI.
AI-ASSISTED (executor='ai' + extract_prompt): fields that need understanding and vary per page (sentiment, pros/cons, a classification, free-form values with no stable selector) across many pages. Each page waits on a model call (~10s) and bills 5x the page rate; on a few pages, or on fields the other modes capture, it adds cost and no accuracy.
Scope: an unscoped crawl of a real site collects hundreds of nav, tag and pagination pages and bills for each. Shapes:
A section ('the docs', 'the pricing and blog pages'):
intentin plain language (the server derives include/exclude paths and depth from the site's real URLs) plusrelevance_threshold≈0.3, which drops off-goal pages.Known pages as a dataset:
seed_urls(no discovery). The immediate answer is writ_scrape(urls).Top N of a listing as a dataset (re-run later, monitored):
rank_cap=N. The immediate answer is writ_scrape(url, top_n).Whole site ('every page'): the defaults;
page_budgetcaps the spend.
Delivery: a bounded crawl (rank_cap / seed_urls) waits and returns its pages in data.rows in this call; an open site crawl returns a crawl id for writ_crawl_status, and its results land as a workflow dataset (writ_workflow_data, writ_search_data, writ_export_data). Comment and discussion threads are kept by content_spec {"preset": "full", "include_comments": true} (rank_cap crawls set it already). Behind a login: persona_id, whose saved session the crawl uses. save_as keeps the crawl for re-running; writ_saved_crawls lists those, each answering only the asks its scope covers.
Response shape: by default every answer is Writ's envelope (definition + crawl status + a data table whose rows wrap fields in run bookkeeping, and whose records carry page metadata like content_kind/depth). output gives an API built on a crawl its consumer's shape: {shape:'record'} for one entity (a usage meter, a dashboard), {shape:'records'} for a list, fields to pick/rename ('percent_used as pct', dotted paths), exclude to drop, key to wrap. Page metadata is stripped unless include_meta=true. Saved with save_as, it becomes the API's default shape (overridable per call on writ_run_saved_crawl / writ_saved_crawl_data). The metadata is added after the extract_prompt model answers, so output removes it and the prompt cannot.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Seed URL (required). | |
| name | No | ||
| wait | No | Hold until the crawl converges and return the collected pages in this call (`data.rows`). Default true for a bounded crawl (rank_cap or seed_urls: a few pages, seconds) and false for an open site crawl (returns a crawl id for writ_crawl_status). Past the 75s ceiling the answer is a 504 that still carries the crawl id. | |
| limit | No | Rows of collected data to return when wait=true (default 50). | |
| speed | No | Throughput tier: slow | normal (default) | fast: the share of the parallel-agent allowance the crawl uses. slow ≈ ¼ at a discounted page rate, normal ≈ ½ at standard rate, fast = all of it at a premium. | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| intent | No | Plain-English goal. The server derives include/exclude paths and a depth from it against a sample of the site's real URLs, and ranks the frontier by relevance, so on an unfamiliar site it scopes better than guessed path regexes. | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| max_age | No | Only meaningful with `save_as`: if that saved crawl already completed within this many seconds, return its collected data instead of crawling again. 0 always crawls. | |
| save_as | No | Saves these settings as a named, callable crawl, for one that is re-run later (a recurring pull, an API the user asked for). Saved crawls are listed to every future session as 'already collected', so a saved one-off reads as reusable data to later sessions. Reusing the same name updates that saved crawl instead of creating a duplicate. | |
| delay_ms | No | Politeness delay between fetches per host (default 250). | |
| executor | No | regular (default) = deterministic crawl, no AI. ai = a fleet of AI agents reads every page against `extract_prompt` and returns structured records, for data with no clean CSS selector. Bills at 5x the page rate. | |
| ocr_mode | No | auto (default) | off | force | |
| rank_cap | No | Top N items of a listing (a front page, search results, a category). The server reads the seed page's link order (which is the ranking), seeds exactly those N item pages, and pins the crawl to them, one page per agent, in parallel. Without it the same request is a breadth crawl that mostly collects nav and pagination and does not answer a top-N ask. include_paths, when given, is the item-link shape. | |
| max_depth | No | ||
| seed_urls | No | Exact known pages to start from: the crawl collects these instead of discovering its own. The cheapest way to scrape a known set. | |
| persona_id | No | Saved identity to crawl as (writ_personas lists them), for pages behind a login. Every shard shares the persona's signed-in session and one sticky exit IP. 2FA is minted server-side. A desktop persona ('device:…') crawls on its own desktop, from that machine. | |
| shard_size | No | URLs fetched per shard batch (default 20). | |
| page_budget | No | ||
| render_mode | No | How each page is fetched, independent of `executor`, which decides who reads it. auto (default) = plain HTTP first, warm browser only for JS-challenge or near-empty pages; http = never opens a browser (fastest, static HTML); browser = warm-render every page (JS/SPA sites). executor=ai works on either lane. | |
| same_domain | No | ||
| content_spec | No | Which elements of each page to keep: {preset: 'full'|'main', include_comments: bool, exclude_selectors: [css], include_selectors: [css], keep: {images: bool}}. 'main' = article body only; 'full' = the whole page including comment and discussion threads. Comments, replies and discussion survive only with 'full' plus include_comments; otherwise they are stripped out. | |
| extract_mode | No | markdown (default) | schema (uniform records via extract_schema) | html (each page's raw HTML, for selectors or embedded JSON) | |
| exclude_paths | No | ||
| include_paths | No | ||
| preview_chars | No | Cut each inline page's text cells to this many characters (default 12000; 0 = full pages). Cut rows list the fields under `_truncated`; a full page comes from writ_workflow_data(workflow_id=<data_workflow_id>, refs=['<run_id>:<record_index>']). | |
| extract_prompt | No | Required with executor=ai: what each agent extracts from each page, in plain language (e.g. 'the product name, price and SKU'). | |
| extract_schema | No | ||
| respect_robots | No | Honor robots.txt (default true). | |
| timeout_seconds | No | Max seconds to hold when wait=true (≤75). | |
| use_residential | No | Route every shard through the platform residential network (premium), for sites that block datacenter IPs / show a bot wall; a persona crawl forces it on automatically. Costs residential bandwidth; default off. | |
| allow_subdomains | No | ||
| relevance_threshold | No | 0-1. Every discovered page is scored against `intent` and skipped below the bar, so a broad crawl collects only what the goal needs (≈0.3 for 'the pricing and docs pages'). Unset for a whole-site sweep. | |
| residential_country | No | Two-letter ISO country the residential exit should be in (e.g. 'us', 'fr'): pins the exit pool's geo for every shard. Omitted = an automatic exit. Ignored unless the session egresses residential. | |
| max_concurrent_shards | No | Explicit parallel-shard cap; overrides the `speed` allocation. |
writ_crawl_statusCheck a crawlRead-onlyIdempotentInspect
Status of a crawl by id: page counts, status, the dataset workflow id. With wait=true one call holds up to 75 s and returns when the crawl converges, with the collected rows inline (data, shaped by output); it resolves a crawl tool's 504 / crawl-id handle. A crawl still running at the ceiling answers with its current status, and the same call can be repeated.
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | Hold until the crawl is terminal and inline its rows (default false). | |
| limit | No | Rows to inline when it converged (default 50). | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| crawl_id | Yes | Crawl id from writ_crawl_site. | |
| preview_chars | No | Cut inline text cells to this many chars (default 12000; 0 = full). | |
| timeout_seconds | No | Ceiling for wait=true (≤75). |
writ_create_automationCreate an automationInspect
Creates an automation: on an event, it runs a workflow, sends a notification, and/or wakes an AI agent. It chains workflows (A completes → B runs), alerts on completion, or has an agent act on the event (ai_prompt). workflow_* events take a source workflow in on_workflow; at least one of run_workflow / notify / ai_prompt is required.
notify is a template over the event, so it carries the data, not just that it ran: after a workflow, {{result.extracted_data.0.title}} / {{result.extracted_data..0.url}} (the run's own rows); after a crawl, {{rows.0.}} … {{row_count}} (the records it collected, schema fields included) plus {{seed_host}} {{pages_done}}; after a monitor change, {{extracted.price}}. A missing path renders empty, so a digest of N rows is N numbered lines. Digest pattern: writ_set_schedule on the workflow, then this with when=workflow_completed; a crawl has no schedule of its own: when='scheduled' + a crawl block, then this with when=crawl_completed.
On a clock: when='scheduled' + schedule fires the actions at that time (a notify after run_workflow waits for that run, so {{result.extracted_data...}} is filled). run_functions + inputs call chosen functions of a multi-function workflow (what they need runs too). Anything else (conditions, scrape, extract, branches) is a raw blocks tree.
On a webhook: when='webhook_received' mints a signed inbound URL; the answer's webhook holds the url, its signing_secret (shown only then), the two headers every call signs, curl and Python examples and an example_body. Each top-level JSON field of a call becomes the run input of the same name; inputs, notify and ai_prompt templates read any field as {{payload.}}. A call is acknowledged at once; with ?wait=true it is held until the workflow runs the automation starts finish and answers their data. webhook_trigger_id reuses an existing URL (writ_list_webhooks).
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Name for the automation (required). | |
| when | No | Event: workflow_completed | workflow_started | crawl_completed | crawl_failed | ai_session_completed | ai_session_started | change_detected (with `target_id`) | webhook_received (mints a signed URL; `webhook_trigger_id` reuses one) | scheduled (a clock, with `schedule`). | |
| title | No | Notification title (with `notify`). | |
| blocks | No | Raw flow tree instead of the action arguments (max 50): each {id, type: event|condition|action, blockType, config, parentId}. The first block is the root event (its blockType is the event: one of `when`; scheduled config {mode, interval_ms | time, days, tz}); every other block names an earlier block as parentId. Actions: workflow {workflow_id, function_name | function_names, input_mapping}; notification {template, title, channels, recipients}; ai_session {goal, entry_url}; scrape {urls:[<=5, templates ok], format: markdown|html|both, on_error} -> {{scraped.content}} {{scraped.pages}}; extract {source:'{{scraped.content}}', fields:[{key, from: html_css|json|regex|embedded_json|body, selector, attribute, all, path, pattern, group, number, required}]} -> {{extracted.<key>}}; crawl {seed_url, intent, include_paths, exclude_paths, page_budget, max_depth, extract_mode: markdown|schema, extract_schema, render_mode: auto|http|browser, persona_id} starts a fresh crawl (its rows arrive with crawl_completed, not in this chain); condition {field, operator, value}. Awaiting a run: an event block workflow_completed {linked_to_block:<workflow block id>}. Every string setting takes {{placeholders}}. | |
| inputs | No | With run_workflow: its inputs {input_name: value}, each a literal or a {{template}} over the event (e.g. {{extracted.price}}, or {{payload.order.id}} from a webhook body); saved values fill the rest. Stored as plain JSON. | |
| notify | No | Send a notification with this message — a template over the event's data (see the tool description: {{result.extracted_data.0.title}} after a workflow, {{rows.0.title}} / {{row_count}} after a crawl, {{extracted.price}} on a change). | |
| enabled | No | ||
| channels | No | Notification channels for `notify`, e.g. ["pushover","email"] (required for delivery). | |
| priority | No | ||
| schedule | No | With when='scheduled': {kind:'interval', interval_minutes:N} or {kind:'daily', time:'HH:MM', tz:'<IANA zone>'} or {kind:'weekly', time, days:[1..7] (1=Mon .. 7=Sun), tz}. | |
| ai_prompt | No | Wake an AI agent with this task when the event fires. The agent gets the event context (page URL, diff, extracted values) and works the task in a cloud browser. Supports {{placeholders}}. | |
| target_id | No | With when='change_detected' (required there): the monitor whose changes fire this, i.e. the monitor_id writ_create_monitor returned. | |
| recipients | No | Notification recipients, e.g. ["email:3"]. Omitted = every enabled recipient on the channel; the answer names who the alert actually reaches, and warns when nobody is configured. | |
| description | No | ||
| on_workflow | No | Source workflow name whose event fires this (required for workflow_* events). | |
| ai_entry_url | No | Page the woken agent starts on. Defaults to the event's page (the monitored URL on change_detected); required in practice for workflow_*/webhook events. | |
| run_function | No | One function name (alias of run_functions). | |
| run_workflow | No | Workflow to run when the event fires (by name). | |
| ai_session_id | No | With when='ai_session_completed' / 'ai_session_started': only this AI session. | |
| run_functions | No | With run_workflow: call these functions of a multi-function workflow instead of the whole workflow — one run of the selection plus what it needs (sign-in, a token, the search whose ids another reads). Unset = the whole workflow. | |
| on_workflow_id | No | ||
| run_workflow_id | No | ||
| cooldown_minutes | No | Minimum minutes between AI wakes for `ai_prompt` (default 10; 0 disables). | |
| target_selector_id | No | With target_id: only changes of this one selector of that monitor. | |
| webhook_trigger_id | No | With when='webhook_received': fire on this existing inbound webhook (its id from writ_list_webhooks) instead of minting a new URL. Its senders keep signing with its secret; one that has no secret yet gets one, shown once in the answer. |
writ_create_http_extractionBuild an HTTP extractionDestructiveInspect
Creates or revises an advanced browserless HTTP extraction from plain language and real browser-network evidence (a browser experiment or capture_network). For: what one simple request or a named auth/function graph cannot express (request loops, GraphQL descriptor discovery, recursive JSON, cross-page dedupe, sorting, cursor pagination); ordinary login/bootstrap/data chains are writ_browser_compose define_function with is_auth/order/typed response_extractions. Generates a universal api_call.config.flow; site behavior stays inside the workflow. apply=false returns the draft and its validation; apply=true writes a valid draft. It is ready to expose once writ_diagnose_http_workflow shows a run with engine=http and records. Requests are flow steps (no fetch in evaluate_js); Writ-only controls never reach the site.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | Yes | Exact inputs, output fields, filters, ordering and pagination behavior wanted. | |
| apply | No | false (default) returns a reviewable draft; true writes a valid draft to the workflow. | |
| workflow | No | Existing workflow name (or workflow_id). | |
| step_index | No | api_call step to replace; defaults to the first, or appends one. | |
| workflow_id | No | ||
| requirements | No | Extra mapping, dedupe, filtering or cursor requirements. | |
| desired_inputs | No | Caller parameters such as query, min_price, max_price, limit and cursor. | |
| request_samples | No | Relevant calls returned by writ_browser_network/capture_network, including representative response bodies when available. | |
| response_sample | No | Optional representative JSON/HTML response when it is not in request_samples. |
writ_create_monitorWatch a page for changesInspect
Creates a monitor that watches a URL for changes/updates: Writ checks it on a schedule and fires a change_detected event when the page, a CSS selector's text, or a visual zone of the page changes. Returns the monitor id that writ_wire_monitor takes.
Selector proof: with the session_id of a writ_browser_use session open on the page, the selector is checked on the live page before saving. One that matches several elements (Amazon '.a-price' matches a dozen; the check would join them into one blob) is pinned to the one shown; one that matches nothing or only an image becomes a visual zone watch. With no selector at all, mode='visual' + zone_text=<the value exactly as the page prints it, e.g. '51,77 EUR'> watches that area's pixels (digits are compared, so 51.77 finds '51,77 EUR'). A zone fires on any visual change, so it suits a change alert, not a threshold. selector_check in the answer says what was done.
Interval and access: no interval → needs_input (this plan's options + a tell_user written for the user; nothing created); one the plan refuses is refused with the allowed ones. The page is read first: behind a sign-in → needs_persona; a bot check → an offer of use_residential.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | URL to monitor (required). | |
| mode | No | 'visual' watches the on-screen zone of `selector`'s element (or of `zone_text`) and diffs its pixels, for charts, images, badges, or a value no selector can read. | |
| watch | No | 'price' also rejects a selector whose text holds no number (it becomes a zone). Default 'content'. | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| enabled | No | Start the monitor enabled (default true). | |
| extract | No | Browserless alternative to `selector`: a response-extraction spec the check applies to a plain HTTP response: {"from":"json","path":"data.price"} (a JSON/XHR endpoint, with request_url), {"from":"html_css","selector":".a-offscreen"} (the page markup), {"from":"regex","pattern":"..."} (a value in a script/JSON blob). For a value readable without JavaScript (most prices and stock lines), no check opens a browser, so each is cheaper and harder to wall than a `selector`/requires_browser check. Structured data (an endpoint, JSON-LD, a JSON blob) survives a redesign best. Same grammar as api_call response_extractions. | |
| interval | No | How often to check: an option id from the needs_input answer ('5m', '15m', '1h', '6h', '24h') or a number of seconds (3600). Without it the answer is needs_input: this account's options (checks a day, how long the check allowance lasts, allowed or which plan) and a tell_user asking the user to pick; nothing is created. An interval the plan refuses is refused with the allowed ones; one that runs the allowance out before it renews is created with a `warning`. | |
| selector | No | CSS selector for content-change monitoring; unset = uptime/status monitoring. | |
| zone_text | No | The text the page prints where the value is (e.g. '51,77 EUR', 'Currently unavailable'). Locates the zone when no selector exists. | |
| persona_id | No | Every check carries this persona's live session (kept fresh by the persona's own sign-in), so a login or a bot wall it passed stays passed (writ_personas). Only for a page behind a sign-in: a public page is watched without one, and the answer says when one is needed. | |
| session_id | No | An open writ_browser_use session on this page. The selector is proved there before saving (pinned / switched to a zone); required for mode='visual' or zone_text. | |
| try_anyway | No | After a bot-check answer: create it on Writ's servers anyway (a check that meets the bot check reads nothing). | |
| request_url | No | With `extract`: the endpoint the value comes from (an XHR the page calls), when it is not `url` itself. | |
| use_residential | No | Check through a residential exit — for sites that wall datacentre traffic (Amazon, marketplaces). A bot check found on the page is answered with this offer, or with the alternatives when the plan cannot pay for one. | |
| interval_minutes | No | Legacy: minutes between checks; `interval` replaces it. | |
| requires_browser | No | Render with a real browser (JS) instead of plain HTTP, for JS-rendered/SPA pages and framed pages (framesets/iframes): the check matches the rendered, frame-flattened DOM, and selector validation is deferred to the first browser render instead of a raw-HTML fetch. Omitted, a page the access check could only read in a browser is checked in one. | |
| residential_country | No | ISO-2 exit country for use_residential (e.g. 'ca'); implies use_residential. |
writ_devicesChoose a linked desktopIdempotentInspect
The user's linked Writ desktops (there can be several): which are connected right now, and how many local workflows and personas each offers. action='list' (default) shows them; action='use' device= scopes this connection to one: runs (writ_run_workflow), browser sessions (writ_browser_use), desktop workflows (writ_list_workflows, runs_on='desktop') and desktop personas (writ_personas, source='device') then target it, and its run history (writ_workflow_runs), data (writ_workflow_data, writ_search_data) and monitors (writ_create_monitor, writ_wire_monitor; action='monitors' lists them) are read from and created on it. action='clear' removes the scope. A desktop's personas sign in from its own vault: the credentials never leave it. For: work 'on my laptop' or 'on my work computer', or in the user's own browser with their logins.
| Name | Required | Description | Default |
|---|---|---|---|
| action | No | list (default) | use | clear | monitors (that desktop's monitors) | datasets (what its exposed workflows collected). | |
| device | No | use: the desktop's agent_id (or its exact name) from list. |
writ_diagnose_http_workflowDiagnose a workflow's HTTP laneRead-onlyIdempotentInspect
Diagnoses HTTP-lane readiness for a saved workflow. Reports browser-only dependencies, invalid flow actions/expressions, unsafe internal query parameters, eligibility versus actual proof, and the next repair action. task_id (a representative run) confirms whether it really used engine=http, did not fall back, and returned records (an answer such as {ok:true,count:0,listings:[]} is not proof). With task_id it also returns the run's steps and, for a flow, what every request it made was answered with (status, sizes, a response sample): the first evidence when a run returned nothing or failed. A 200 with an empty feed means the site stopped serving this session (signed out, blocked, or a null written over a working request variable), not 'no matches'.
| Name | Required | Description | Default |
|---|---|---|---|
| task_id | No | Optional run to verify actual HTTP execution and output. | |
| workflow | No | ||
| workflow_id | No |
writ_discovery_statusCheck an API buildRead-onlyIdempotentInspect
Status of a build started by writ_website_to_api. Terminal states: succeeded, failed, cancelled. Resting state: needs_guidance — Writ's mechanical rungs are done and the build is the caller's to finish; it carries map (endpoints seen, specs, candidate functions), and writ_website_to_api mode=guided build_id= continues it. A rung the ladder replaced reports superseded=true with fallback_build_id; with wait=true this tool follows that pointer and answers with the rung now running (followed_from lists the ids it walked; the returned build_id is the current one from then on). escalations lists every earlier rung with the real reason it handed over (robots.txt refused the crawl, no pages fetched, nothing matched the goal, no structured list...), i.e. why a browser was needed. On success it returns the workflow_id, the surfaces mapped, and verified — false for a fast/browser map, whose functions are candidates until a real run proves them. Any function runs with writ_run_workflow (function_name); the generated API docs (OpenAPI 3, Markdown or a Postman collection, all pointing at the real Writ endpoint) are at GET /api/v1/workflows/{workflow_id}/api-docs.
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | One held call (≤75s) that follows escalations to the newest rung and returns when that build is terminal or parked as needs_guidance. Still building at the ceiling, it answers with the build_id to repeat it with. | |
| build_id | Yes | The build id from writ_website_to_api. | |
| timeout_seconds | No | Ceiling for wait=true (≤75). |
writ_export_dataExport collected dataRead-onlyIdempotentInspect
Exports a workflow's full extracted-data table as CSV or JSON (search/filter applied, unpaginated).
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | ||
| view | No | ||
| format | No | csv (default) or json | |
| workflow | No | ||
| workflow_id | No |
writ_expose_workflow_apiPublish a workflow as a REST APIIdempotentInspect
Publishes a saved workflow through Writ's managed REST gateway (the same resource as the app's REST endpoint switch). The returned POST URL waits for the workflow and returns its JSON result. Repeated calls reuse the existing endpoint.
| Name | Required | Description | Default |
|---|---|---|---|
| label | No | Optional name for the endpoint. | |
| workflow | No | ||
| workflow_id | No | ||
| wait_timeout | No | Deprecated alias of timeout_seconds. | |
| timeout_seconds | No | Managed run timeout. Defaults to 120, or 300 for AI navigation workflows. |
writ_list_webhooksList inbound webhook URLsARead-onlyIdempotentInspect
The account's inbound webhook URLs: for each, its webhook_trigger_id, the callable url, whether it is signed, the automations it fires and how often it was called. Signing secrets are stored encrypted and never shown. An automation reuses one with writ_create_automation webhook_trigger_id=; when='webhook_received' without it mints a new URL.
| Name | Required | Description | Default |
|---|---|---|---|
| webhook_trigger_id | No | Only this webhook. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, destructiveHint=false, so the safety profile is covered. The description adds value beyond them: signing secrets are stored encrypted and never returned, and it clarifies that reuse vs. minting of webhook URLs depends on supplying webhook_trigger_id.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with the resource and its returned fields, then the security caveat, then the integration rule. No filler or repetition; every clause carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description usefully enumerates the returned fields and the security constraint, and annotations cover the safety profile. What is missing is minor: no guidance on empty results, pagination, or ordering of the listed URLs.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single optional webhook_trigger_id is documented in the schema as 'Only this webhook.' The description adds no filter syntax or behavior beyond that, so the baseline 3 for a fully documented parameter set applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb+resource: it lists the account's inbound webhook URLs and enumerates exactly what is returned for each (webhook_trigger_id, callable url, signed status, firing automations, call count). An agent immediately knows this is a read of webhook configuration, not a crawler, workflow, or browser tool like its siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explains the relationship to writ_create_automation (reuse an existing webhook_trigger_id; when='webhook_received' without it mints a new URL), which supplies real context for the trigger id. However it never states when to call this listing tool rather than alternatives, nor any exclusion or precondition for the optional filter.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
writ_list_workflowsList saved workflowsRead-onlyIdempotentInspect
Lists the workflows saved in the Writ account; each runs on demand without live browsing. Returns id, name, declared inputs, schedule, and whether it is pinned as its own run_ tool.
| Name | Required | Description | Default |
|---|---|---|---|
| search | No | Optional name/description filter. |
writ_paymentPay with a card the user approvesDestructiveInspect
Pays on a website with one of the user's cards, without the card number ever reaching this conversation: the tool never sees or asks for card numbers. action='request' (site, max_amount, purpose) returns a tell_user and an open_url: a Writ window where the user picks or adds a card (or makes a virtual card) and approves it. A purchase needs that approval. action='wait' grant_id= is one held call (up to 60s) that answers when they approve, with the card's brand and last four digits only. Ordering: action='checkout' (session on the final checkout page, commit_selector = the place-order button, total_selector = the order total, grant_id, or max_amount when the store uses its own saved card) pauses the session and the user confirms (emailed link, the Writ app, or an auto-confirm rule they turned on); then Writ types the card, checks the total and clicks the order button itself: a real purchase. An order-button click sent by the assistant is refused. action='wait' confirmation_id= returns the outcome. action='fill' types an approved virtual card early (multi-page checkouts). Card use is limited to the site and amount granted; an unused grant expires after 15 minutes. action='list' shows the user's payment methods as handles (kind, brand, last4, id), never numbers; a handle goes into writ_wire_monitor buy.payment {kind, ref}.
| Name | Required | Description | Default |
|---|---|---|---|
| site | No | request: the store's domain, e.g. 'store.example.com'. The card is typed only on this site and its payment frames. | |
| steps | No | checkout: steps Writ runs after confirmation, before the order button, e.g. [{"type":"fill_card"}, {"type":"click","selector":"#continue"}, {"type":"wait","seconds":2}] for a card page followed by a review page. Default: fill the granted card, then the order button. | |
| action | Yes | request = the user approves a card for one purchase; wait = hold until they answer (grant_id) or until a checkout is settled (confirmation_id); checkout = pause at the order button for the user's confirmation, then Writ places the order; fill = type an approved virtual card early; list = the user's payment methods as handles. | |
| fields | No | request, for live browsing: card field -> CSS selector on the checkout page (writ_browser_context reads them), or {selector, frame_url} for a field inside a payment provider's frame (frame_url = part of the frame's URL, e.g. 'js.stripe.com'). Keys: number, exp (MM/YY), exp_month, exp_year, exp_year2, cvc, name, zip. Writ types into exactly these. | |
| purpose | No | request: one line the user reads when approving, e.g. 'Buy Nike Dunk Low, size 10'. | |
| session | No | checkout / fill (required) / request: the session_id of the writ_browser_use session on the checkout page. | |
| summary | No | checkout: one line the user reads when confirming, e.g. '1 x Nike Dunk Low, size 10, shipped to home'. | |
| currency | No | request / checkout: the store's currency, three-letter ISO code ('eur', 'usd', 'gbp'); max_amount is in it. Omitted: the user's card currency on request, the currency the page total shows on checkout. A card pays only in its own currency. | |
| grant_id | No | wait / fill / checkout: the grant_id action='request' returned. | |
| max_amount | No | request / checkout: the most this purchase may charge, tax and shipping included (e.g. 129.99). checkout without grant_id needs it. | |
| automation_id | No | request: the automation the card is for, when the purchase is a saved automation. | |
| total_selector | No | request / checkout: CSS selector of the order total on the checkout page. Writ reads it and never clicks on when it is above the maximum. Needed for an auto-confirm rule to apply. | |
| commit_selector | No | checkout (required): CSS selector of the button that places the order. Writ clicks it after the user confirms. | |
| confirmation_id | No | wait: the confirmation_id action='checkout' returned. | |
| payment_method_id | No | request: a handle id from action='list' to pre-select that card; the user still approves. |
writ_personasUse the user's saved sign-insInspect
The user's own accounts on websites. When a task needs the user signed in (email, social, shop, bank or work portal), a persona is how Writ signs in as them. No password passes through this tool or the conversation, and its answers never ask for one: credentials are typed only into a Writ window. A persona is a saved sign-in identity: a site's username plus credentials sealed server-side (never readable here), optional 2FA whose codes are minted server-side, and a warm signed-in session. Its persona_id is accepted by writ_browser_use, writ_crawl_site, writ_scrape and writ_run_workflow. action='list' (filter by domain) shows the personas usable on a site; action='get' inspects one (include_runs adds its recent runs); action='sign_in' runs its login workflow now (force=true re-logs-in even when the session looks usable); action='record_login' has a server-side AI sign in as it once and record the flow as its login workflow, so it can sign itself back in. This tool cannot create a persona. list with a domain and no match answers persona_needed: a tell_user (a message written for the user), a create_url (a minted link to a small Writ window with only the persona form, pre-filled for that site) and its link_id; action='request' domain= mints one on purpose (another account for a site). action='wait' link_id= is one held call that answers the moment the user saves the persona, with its persona_id. Also listed: the personas of the user's linked Writ desktop (source='device', id device:<agent>:<id>, name and site only). That id as persona_id on writ_run_workflow, writ_browser_use / writ_record_website, writ_scrape or writ_crawl_site sends the work to that desktop, which signs in from its own vault and only on the persona's own site: the credentials never leave it.
| Name | Required | Description | Default |
|---|---|---|---|
| why | No | request: one line the user sees in the window: why the account is needed. | |
| wait | No | list + domain: one call holds up to 75s for a persona for that site to appear. A link_id is awaited with action='wait'. | |
| force | No | sign_in: re-run the login even when the current session still looks usable. | |
| action | Yes | What to do (default list). request = mint the persona link for a site (domain); wait = hold until the user saves it (link_id). | |
| domain | No | list: only personas usable on this host (suffix match), e.g. 'github.com'. With no match the answer is `persona_needed`, a request worded for the user. request: the site the new persona is for. | |
| link_id | No | wait: the `link_id` of a persona_needed / request answer. Holds up to 75s and answers the moment the user saves the persona (persona_id), declines, or closes the window, else `still_waiting` (repeatable). | |
| login_url | No | record_login: exact sign-in page URL when known; defaults to the persona's domain root (the AI finds the form from there). list / request: the site's sign-in page, carried into `create_url` so the new persona can record it. | |
| persona_id | No | Which persona — required for get / sign_in / record_login. | |
| include_runs | No | get: include the persona's recent runs (which workflows acted as it, and whether they succeeded). |
writ_pin_workflow_toolPin a workflow as a toolIdempotentInspect
Pins (or unpins) a saved workflow as its own run_ tool on this server. Workflows are not exposed as individual tools by default; every one stays callable through writ_run_workflow, and the pinned list is capped. For: the few workflows the user runs often or wants to call as a tool from here.
| Name | Required | Description | Default |
|---|---|---|---|
| pinned | No | true (default) pins; false unpins. | |
| workflow | No | Workflow name (or workflow_id). | |
| workflow_id | No |
writ_record_websiteRecord a website taskDestructiveInspect
Records a repeatable website task as a workflow that replays on demand. For recording, capturing, teaching, automating or repeating actions on a site when the point is the task, not an API surface. Writ opens a real cloud browser, returns an observation and runs no model there; the caller is the brain: writ_browser_act drives the browser, writ_browser_compose authors what the recorder cannot see (inputs a caller passes, explicit steps, named functions), and writ_browser_save saves the finished task. The saved workflow replays at zero AI cost (writ_run_workflow, or its own run_ tool once pinned with writ_pin_workflow_tool) and can be scheduled. A goal that asks for an API ("turn into an API", "an endpoint for") is routed automatically to writ_website_to_api's intelligent ladder (the fast path, then Writ's AI browser rung), so the recording is a real build. That build first proposes the user's own matching workflows (existing_workflows) and ready-made marketplace APIs (marketplace_candidates); skip_existing / skip_marketplace bypass those.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Website URL to start on (required). | |
| goal | Yes | What should be recorded on the website, in plain language | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| auth_mode | No | Browser recordings only: reuse verifies unknown saved auth before adoption; fresh_login records a new login without restoring old cookies, headers or storage. Device identity stays the same. | |
| fresh_exit | No | Residential only: skip the persona's usual exit for this site and draw a new address — after the site refused it (an IP rate limit, a sign-in rejected with correct credentials). Ignored on server-IP egress. | |
| persona_id | No | Saved identity to sign in with (list them with writ_personas). Required for sites behind a login with 2FA — the one-time code is then minted server-side and never shown to the caller. A desktop persona ('device:…') records on its own desktop, which fills its values itself. | |
| human_layer | No | Browser recordings only: native keyboard input, distance-adaptive mouse paths and pre-click dwell. Fresh login enables it by default; false explicitly disables it. Native actionability checks remain active. | |
| skip_existing | No | API builds first propose the user's OWN matching workflows (replaying is instant and free); set true after the user declined those. | |
| use_residential | No | Open on the platform residential network (premium) for a site that blocks datacenter IPs or shows a bot wall. Default off (free datacenter egress). | |
| skip_marketplace | No | API builds then propose compatible ready-made marketplace APIs; set true to skip that and record fresh. | |
| residential_country | No | Two-letter ISO country the residential exit should be in (e.g. 'us', 'fr') — for a site that serves a different page per country, or throttles foreign traffic. Omit for an automatic exit. Ignored unless the session egresses residential. |
writ_run_saved_crawlRun a saved crawlInspect
Runs a saved crawl with its stored settings. With max_age, the data it already collected comes back when that run is recent enough (the cheap path); otherwise it re-crawls. The response's _cache.hit and _cache.age_seconds say which happened.
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | Hold until the crawl converges (default false: a crawl is slow). | |
| crawl | Yes | Saved crawl slug, name, or id (from writ_saved_crawls). | |
| limit | No | Rows of collected data to include (default 50). | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| max_age | No | Reuse the last completed crawl if it finished within this many seconds. 0 (default) always re-crawls. | |
| preview_chars | No | Cut each inline page's text cells to this many characters (default 12000; 0 = full). Full page: writ_workflow_data(workflow_id=<data_workflow_id>, refs=[...]). | |
| timeout_seconds | No | Max seconds to wait when wait=true. |
writ_run_workflowRun a saved workflowDestructiveInspect
Runs a saved workflow by id or name and, by default, waits for it to finish, returning the extracted data. Workflow inputs go as top-level fields or under inputs; file inputs go under files.
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | Wait for completion and return the data (default true). | |
| files | No | Optional file inputs for this run, as {slot: file_id}. Slot names come from the workflow's `file_slots` (writ_list_workflows); file_ids come from the account's file library. An upload step with a pinned file uses it when `files` is omitted; a value here swaps the file for this run only. | |
| device | No | Run on this linked Writ desktop (an agent_id from writ_devices) — overrides the desktop this connection chose with writ_devices action='use'. | |
| inputs | No | Run inputs (top-level fields work too). | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| max_age | No | Optional. Reuse a previous result if it is younger than this many seconds, instead of running the workflow again. 0 (the default) always runs fresh. A reused result answers at once and costs nothing. | |
| workflow | No | Workflow name (or workflow_id). | |
| persona_id | No | Run AS this saved identity (see writ_personas) — the run signs in with the persona's warm session. Omit to use the workflow's default persona, if it has one. A persona of the user's linked DESKTOP (`device:<agent>:<id>`, source='device' in writ_personas) sends the run to that desktop, which signs in from its own vault: the credentials never leave it. | |
| workflow_id | No | A number, or `local:<id>` for a workflow that lives on the user's linked Writ desktop (writ_list_workflows, runs_on='desktop') - it runs there, signed in as one of that desktop's personas when persona_id is `device:...`. | |
| function_name | No | One named function of a multi-function workflow (an API built with writ_website_to_api / writ_browser_compose define_function): only that function and the sign-in functions it depends on run. Omitted, the whole workflow runs. It is a control, never a workflow input. | |
| mutation_mode | No | How a write function (one that creates/posts/sends/deletes) runs on this run. Default 'live': a run is an explicit call, so its writes are sent. 'dry_run' previews the request without sending it; 'private_test' sends it with the function's safe overrides. (Separately, BUILDING a function — define/compile/test — never sends a write, whatever this is.) Reads ignore this. | |
| function_names | No | Several functions in one run, in place of `function_name`: the union of their steps runs once, in recorded order (a prerequisite they share runs once). Not for a desktop (`local:`) workflow. A control, never a workflow input. | |
| timeout_seconds | No | Max seconds to wait for completion (default 120). | |
| use_residential | No | Per-call network override for an owned workflow: true uses the platform residential network, false disables the workflow's residential default. For geo-sensitive sites and sites that block datacenter addresses. | |
| execution_target | No | Where this run executes: 'cloud' (managed fleet), 'auto' (prefer the user's OWN linked Writ desktop app when online, else cloud), or 'local' (require their own desktop app — keeps the run on their machine + IP). Omit to keep the workflow's own configured target. A 'local' run fails while that desktop app is offline; 'cloud' runs the same workflow on Writ's fleet, off the user's machine and IP. | |
| residential_country | No | Two-letter ISO-3166 exit country for this call (for example ca, us, fr). Keep it aligned with the requested storefront, coordinates or delivery market. It is applied when the run uses residential egress. |
writ_saved_crawl_dataRead a saved crawl's dataRead-onlyIdempotentInspect
Reads the data a saved crawl already collected on its most recent completed run, at any age. It never starts a crawl.
| Name | Required | Description | Default |
|---|---|---|---|
| crawl | Yes | Saved crawl slug, name, or id. | |
| limit | No | Rows to return (default 25). | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| preview_chars | No | Cut string cells (page markdown) to this many characters (default 2000; 0 = full cells). Full single pages: writ_workflow_data(refs=...) per the response hint. |
writ_saved_crawlsList saved crawlsRead-onlyIdempotentInspect
Lists the crawls the user saved for re-running (callable by API, each with a scope: seed_url, rank_cap, include_paths, extract_mode, executor). On a saved crawl whose scope matches an ask, writ_run_saved_crawl(max_age=…) returns recent data instantly at no cost. A saved crawl of a different page, or one using executor=ai, does not answer a fresh question; writ_crawl_site(rank_cap=N) does.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max saved crawls to return (default 50). |
writ_scrapeRead web pagesInspect
READ PAGE CONTENT NOW: one page, a list of pages, or the top N items of a listing, returned as clean markdown in this call (2-10s). For: 'what does say', 'summarize ', 'the top N posts/products/results of and what's on each', 'fetch these 3 links', including a page a plain fetch cannot read: blocked or empty (403, bot wall), rendered by JavaScript, or behind the user's sign-in (render_mode, use_residential, persona_id). NOT FOR: collecting a whole site or section into a dataset (writ_crawl_site); clicking, typing, signing in or any action on a page (writ_browser_use).
Three shapes, one call each:
url → that page.
urls=[...] (≤20) → all of them, fetched in parallel,
pagesin the order given.url= + top_n=N (≤20) → the listing (
listing) and the N top-ranked item pages it links to (pages, in rank order); e.g. url='https://news.ycombinator.com/', top_n=3 returns the front page and the 3 top stories' discussion pages. include_paths=['item\?id='] gives the item-link shape; without it the server detects it.
Discussion pages keep their comment threads; each comment is tagged [top-level] or [reply · depth N], so 'the top-level comments' is answerable from the text. Long pages are preview-cut at 12000 chars (_truncated); the hint says how to fetch a full page. The caller reads the markdown; no AI is spent here. Behind a login: persona_id. Bot wall: use_residential=true.
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | A page to read or, with `top_n`, the listing page whose top items are read. | |
| urls | No | Known pages to read together (max 20), fetched in parallel in one call. | |
| top_n | No | With `url` = a listing/front/search/category page: also read its N top-ranked item pages (the page's link order is the ranking). One call, parallel. | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| format | No | markdown (default): the page's clean main content. html: the raw HTML as fetched (same egress/persona/render) — for selectors, embedded JSON, anything the cleaned text drops; clipped to preview_chars (default 40000, 0 = whole page). both: html plus the markdown derived from it, one fetch. Single `url` only. | |
| persona_id | No | Saved identity to read as (writ_personas lists them), for pages behind a login. Forces the identity's own residential exit. A desktop persona ('device:…') reads on its own desktop, from that machine. | |
| render_mode | No | auto (default: plain HTTP, browser only if the page needs JS) | http | browser. | |
| include_paths | No | With top_n: regex(es) the item links match (e.g. 'item\\?id=', '/products/'). Optional: the server detects detail links when omitted. | |
| preview_chars | No | Cut each page's text to this many characters (default 12000; 0 = full pages). | |
| respect_robots | No | Apply robots.txt to the explicitly requested page(s). Default false for scrape; writ_crawl_site defaults true for autonomous discovery. | |
| use_residential | No | Fetch through the platform residential network (premium) for a site that blocks datacenter IPs or shows a bot wall. Default off. | |
| residential_country | No | Two-letter ISO country the residential exit should be in (e.g. 'us', 'fr'), also used by the automatic residential retry on a blocked page. Omitted = an automatic exit. Ignored unless the session egresses residential. |
writ_search_dataSearch collected dataRead-onlyIdempotentInspect
Searches everything the account's workflows already collected for a term: answers data questions from past runs without running anything. Scopes to one workflow when given, else fans out. Matches come back preview-sized; writ_workflow_data(refs=...) returns full records.
| Name | Required | Description | Default |
|---|---|---|---|
| q | Yes | Search term (required). | |
| limit | No | Rows per workflow (default 10). | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| workflow | No | ||
| workflow_id | No | ||
| preview_chars | No | Cut matched string cells to this many characters (default 300; 0 = full cells). |
writ_set_scheduleSchedule a workflowDestructiveIdempotentInspect
Schedules a saved workflow to run automatically: every_minutes for an interval, or kind='daily'/'weekly' with time (HH:MM) and days. On a multi-function workflow (an API from writ_website_to_api) functions schedules one or several of its functions instead of the whole workflow, with saved inputs for them: each run executes those functions plus whatever they need (sign-in, a token, the search whose ids another reads) exactly once. The answer names also_runs and any missing_inputs.
| Name | Required | Description | Default |
|---|---|---|---|
| tz | No | IANA timezone for daily/weekly. | |
| days | No | ISO weekdays for weekly: 1=Mon .. 7=Sun. | |
| kind | No | interval | daily | weekly | |
| time | No | HH:MM local time for daily/weekly. | |
| inputs | No | Scheduled-run inputs {input_name: value} (text, number, true/false), used over the workflow's saved values for scheduled runs only. {} clears them; omitted keeps them. No secrets: those are workflow secrets or a persona. | |
| enabled | No | Schedule on or off (default on). | |
| function | No | One function name (alias of `functions`); "" or "all" = the whole workflow. | |
| workflow | No | ||
| functions | No | Function names a scheduled run calls (from writ_list_workflows / writ_update_workflow). [] or ["all"] = the whole workflow. Omitted: current target kept. | |
| workflow_id | No | ||
| every_minutes | No | Interval schedule: minutes between runs. |
writ_update_saved_crawlEdit a saved crawlADestructiveIdempotentInspect
Inspect or customize a saved crawl in place. With no changes, returns its complete definition. settings recursively merges any crawl option into the stored config (scope, paths, budgets, extraction, rendering, persona, residential egress/country, speed, output shape and other flags) without dropping unrelated settings.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | ||
| crawl | Yes | Saved crawl slug, name, or id. | |
| settings | No | Sparse crawl config patch merged recursively into the complete saved settings. | |
| description | No | ||
| default_max_age_seconds | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare destructiveHint=true, idempotentHint=true, and readOnlyHint=false. The description adds genuinely useful context beyond them: the settings patch is a recursive merge that preserves unrelated settings, which materially softens how an agent should interpret the destructive hint. It omits auth requirements and whether the merge can be reverted.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with purpose, then the non-obvious merge semantics. No filler or repetition of the title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation tool with annotations covering safety and no output schema, the description explains the read-vs-edit behavior and merge semantics adequately. It could say more about whether changes require a separate apply/save step or what happens on invalid patch keys.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is only 40%, but the description compensates for the most complex parameter by explaining that `settings` is a sparse recursive patch over the full stored config. `name`, `description`, and `default_max_age_seconds` are self-evident and need no further prose.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('inspect or customize') and resource ('a saved crawl') and the in-place nature, distinguishing it from writ_saved_crawls (listing) and writ_run_saved_crawl (executing). It is clear but does not explicitly name a sibling to contrast against.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The dual behavior is implied ('With no changes, returns its complete definition' vs. merging changes), which tells an agent it can be used as a read. However, there is no explicit when-to-use guidance, prerequisites, or named alternatives among the many sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
writ_update_workflowEdit a saved workflowDestructiveInspect
Inspects and edits a saved workflow. No edits returns a compact outline with stable step ids and source hashes; verbose=true reads the full definition. section=contract lists every HTTP operator with its operands and what it does (operator selects one); section=flow + function_name lists nested node paths. function_name reads one function's source, with optional step_id and path (JSON Pointer, e.g. /config/flow or /config/script); offset/max_chars window the source. Targeted edit: function_updates=[{function_name, step_id?, expected_hash?, patch?, json_edits?, script_edits?}]. An api_call step binds config.function_name. patch merges type/config/enabled; json_edits use test/add/replace/remove with path/value to change one nested HTTP action, JSON field or array item without rewriting the program. script_edits use path/old/new and optional expected_hash; old must match exactly once. Function identity is preserved. validate_only=true previews edits and validates HTTP grammar without running JavaScript or making requests. expected_updated_at from a read prevents concurrent overwrite (409: stale read). function_updates is the targeted repair; step_updates merge indexed steps; replace_steps replaces all steps. patch edits settings and metadata; credentials stay in persona/vault. Saving runs nothing; a run on representative inputs, diagnosed by its task_id, proves the edit. regenerate_skill=true rebuilds the agent skill; patch.skill_md edits it.
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | JSON Pointer into the selected step, e.g. /config/flow/steps/2 or /config/script. | |
| limit | No | ||
| patch | No | Sparse settings patch. Supports every WorkflowUpdate field except credentials and captured recorded_session material, which vault/persona/session management own. human_behavior: 'on' types text using keyboard events every browser run; 'auto' does so on a bot-block retry; 'off' disables the default. | |
| offset | No | ||
| section | No | source reads a source window; flow lists nested node paths; contract lists operators and their semantics without requiring a workflow. | |
| step_id | No | Stable step id that picks one step of a multi-step function. | |
| verbose | No | Edits answer with the full workflow definition instead of the compact step outline (default false). | |
| operator | No | section=contract: one operator, with its operands, semantics and example. | |
| workflow | No | Workflow name (or workflow_id). | |
| max_chars | No | ||
| workflow_id | No | ||
| step_updates | No | ||
| function_name | No | Source of this function only. | |
| replace_steps | No | Complete recorded-step replacement; step_updates is the targeted edit. | |
| validate_only | No | Preview/validate without saving or executing. | |
| function_updates | No | ||
| regenerate_skill | No | Rebuild the workflow's agent skill (SKILL.md) from its current functions, inputs and sign-in, replacing any edited version; the answer carries the new skill_md. Runs after any patch in the same call. | |
| expected_updated_at | No | Workflow timestamp returned by inspection; checked under the row lock. |
writ_website_to_apiTurn a website into an APIInspect
Turns a website into a callable API: the one tool for this, every lane. For a service with no official or practical API whose data or actions are wanted programmatically: "turn into an API", "map the API of ", "expose every feature", "give me an endpoint for ".
A build is 3 CALLS: (1) this tool with url + goal (+ save_as); (2) writ_discovery_status(build_id, wait=true), one held call that follows every rung; (3) on succeeded, the answer's run_example (writ_run_workflow: workflow_id + function_name + inputs) runs it, and a working function answers two different inputs differently. START ON THE PAGE THAT ALREADY SHOWS THE ROWS: the url is the search-results / category / listing URL (e.g. https://www.google.com/maps/search/bakeries+Montreal/), not the app's home page; a build seeded at an empty shell spent 6 minutes over three rungs and produced no function. A goal naming the inputs and the fields ("page number in; quotes with text/author/tags and has_next out") shapes the functions. The build has its own browser, and an identical request during it joins it. An answer of existing_workflows / marketplace_candidates is a proposal, not a build: the match runs as is, and skip_existing / skip_marketplace start a fresh build. Status needs_guidance means the build is the caller's: mode=guided build_id= continues it in a browser the caller drives as its brain. writ_diagnose_http_workflow(workflow_id, task_id) explains a run that returns nothing.
NOT FOR: reading a page's content (writ_scrape), collecting a site as a dataset (writ_crawl_site), or a task that is not an API surface (writ_record_website).
Login: an app behind a sign-in builds as a saved identity (persona_id from writ_personas, which also carries 2FA); credentials never pass through this tool. Without one, persona_needed carries tell_user, written to ask the user for that sign-in.
Default mode intelligent: Writ runs the whole ladder itself, and the caller only starts it and waits. The ladder: the user's own matching workflows (existing_workflows; skip_existing=true bypasses), ready-made marketplace APIs (marketplace_candidates; skip_marketplace=true), then the fast path: one real cloud browser (the persona's session, residential exit, CAPTCHA and bot-wall handling, like every Writ session) where one AI call plans the functions the goal needs (GET reads, POST writes, in-page extractions) and the steps that reach each, the browser runs them, and per function one more AI call picks what backs it — the site's own captured request (compiled: tokens traced, inputs templated), the page's list, or the write's captured request (probe_write: never sent) — each live-tested, reads proven on a second input. Only when it cannot prove them does Writ's AI browser rung take over (turn by turn: ranks traffic, promotes HTTP requests, tests pagination); the newer rung's status carries escalations, the reason the fast path handed over. A saved fast-path API with gaps names them in missing_functions.
mode=crawl / mode=browser run the whole-site crawl rungs instead (static / rendered; robots.txt respected unless respect_robots=false): broad maps of server-rendered sites, unverified (verified:false) until a run proves them.
mode=auto is the same ladder with the caller as the last rung: when the fast path cannot prove the functions, the build parks as status=needs_guidance with map (pages, captured calls, each planned function and why it fell short) and what it already defined, instead of spending Writ's agent. mode=fast, mode=crawl and mode=browser start on their own rung and park the same way when they fall short. mode=guided (with build_id= to continue, or alone to start) opens a real browser bound to the build, driven turn by turn with writ_browser_act (navigate, sign in, capture_network, evaluate_js; calls read with writ_browser_network), where writ_browser_compose define_function defines the API: api functions from captured calls via from_index, or proven scripts/extractions, each live-tested as it is defined; set_inputs for parameters; is_auth for a sign-in function whose response_extractions feed the others. writ_browser_save settles the build: the workflow is callable at once (writ_run_workflow with function_name), pinnable, schedulable, exposable as REST (writ_expose_workflow_api), and its API docs are at GET /api/v1/workflows/{workflow_id}/api-docs.
HTTP-first: the guided browser is the experiment bench, and what ships are direct API functions defined from a captured representative search/filter request and next-page request. The default shape is the Auphan-style named function graph: ordered is_auth functions publish tokens/ids/origins through response_extractions, and data functions consume {{extracted:name}}; config.flow covers loops, recursive mapping, cross-page dedupe and composite returns. Typed extraction sources: json, embedded_json, html_css, regex, header, body. search/filter/limit/page/offset/cursor are declared inputs, and next_cursor/next_offset/has_more are returned. writ_diagnose_http_workflow(task_id=...) checks a saved run made with the intended persona; an API is ready to expose once engine=http returns non-empty data and pagination matches the browser baseline, or once a measured browser-only dependency is named.
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | The page that already shows the rows (search-results, category or listing URL), not the home page. Required unless build_id continues a parked build. | |
| goal | No | What the API should return or do, in plain language. Matched to the user's own workflows and marketplace listings first; narrows the crawl to what was asked (equivalent to `scope`) instead of mapping the whole app. | |
| mode | No | intelligent (default): the full ladder driven by Writ — the AI fast path (a live browser, a few AI steps, every function live-tested), then Writ's own AI browser rung only if needed. guided: a browser bound to a build that the caller drives and composes (sign in, capture_network, define_function, save); with build_id it continues a parked build and inherits its map. 'auto': the same ladder with the caller as the last rung — own workflows and marketplace proposed first, then the fast path; not enough => the build parks as needs_guidance, to finish guided. 'fast' = starts on the fast path. 'crawl' / 'browser' = the whole-site static / rendered crawl (unverified maps). For compatibility, regular/deep remain aliases of guided. | |
| level | No | Write policy for the crawl rungs. 'light' (default) captures every endpoint and payload but never performs a real create/update/delete. 'deep' performs each write once to capture its real confirmation response, so it changes real data; opt-in, for an explicit request. | |
| scope | No | Crawl lanes: maps only this surface (e.g. 'employees') and what it depends on. No value maps the whole app. | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| save_as | No | Name for the workflow the build saves. | |
| build_id | No | Continues a parked build (status needs_guidance from writ_discovery_status) on the guided rung: opens the browser bound to it, seeded with its map. | |
| anonymous | No | Builds from what is visible without an account even though the site shows a sign-in page; for when the user said the public part is enough. | |
| persona_id | No | Saved identity to sign in with (writ_personas). Without one, a site whose entry page is a sign-in wall is not built: the answer names the persona to pass, or returns `persona_needed` with `tell_user` (a message for the user) and `create_url`; a later call with the new persona_id builds. Required for 2FA: the code is minted server-side and never reaches the caller. | |
| ai_supervise | No | AI-supervised crawl rungs (mode=crawl/browser; default true): after the crawl mines forms, POSTs, query links, scripts and listings, one bounded Writ AI call authors the API from them — which functions serve the goal, their names, inputs and example values; the live verify call then measures each response shape. false = the purely mechanical surface map (no AI spend on the crawl rungs). | |
| skip_existing | No | Skips the proposal of the user's own matching workflows (for after they declined). | |
| respect_robots | No | Crawl rungs (mode=crawl/browser) obey the site's robots.txt (default true). false is for a target the user vouches for after a rung reported robots.txt refused the crawl (`escalations` / `message` name the rule); otherwise such a rung fetches nothing and the build goes straight to a browser. Does not apply to the AI or guided browser rungs. | |
| use_residential | No | Runs on the platform residential network (premium), for a site that blocks datacenter IPs. Default off. Continuing a build (build_id) keeps the build's own persona, residential exit and country unless these fields are passed. | |
| execution_target | No | 'cloud' (the fleet) or a linked desktop's agent_id: builds there, in its own browser and connection. Omitted = the desktop chosen with writ_devices, else the cloud. | |
| skip_marketplace | No | Skips the ready-made marketplace proposals and builds fresh. | |
| residential_country | No | Two-letter ISO country the residential exit should be in (e.g. 'us', 'fr'); applies to every rung of the build, the guided browser included. No value means an automatic exit. Ignored unless the session egresses residential. |
writ_wire_monitorConnect a monitor to an actionInspect
Wires a writ_create_monitor monitor's change_detected event to an action. action='workflow' runs a saved workflow when the monitored page changes; action='notify' sends a notification to channels + recipients the account has configured; action='ai_task' wakes an AI agent with a task prompt: the agent opens the monitored page in a cloud browser, sees what changed (diff + extracted values) and works the prompt autonomously (channels/recipients also notify when it finishes). A price watch takes threshold: the action then runs once, when a check reads a price at or below it (threshold_op picks the side), not on every change. Buying at the price, in order: 1) writ_create_monitor reads the price; 2) writ_record_website or writ_browser_use, then writ_browser_save, record the checkout up to the order page, with the product, quantity and shipping as inputs where they vary, an extract of the total, and the order button never pressed (its selector kept); 3) writ_run_workflow with mutation_mode='dry_run' rehearses it once, to end on the order page and return the total (safe to rehearse only a checkout recorded this way: an older one may hold the order click); 4) action='workflow' + that workflow + buy (payment {kind, ref} from writ_payment, total_selector, commit_selector = that button (Writ clicks it live), fallback_ai_session: true); 5) the fallback, ONLY when the checkout can't be recorded or its rehearsal fails (bot wall, a checkout that won't replay, a login the persona can't hold): action='ai_task' with a prompt naming what to buy and the same buy; an AI PURCHASE session browses to the order page and hands over to Writ's confirmed checkout. Either way it is saved as a REHEARSAL (dry run) until the user turns purchases on in the Writ app, and a person confirms each purchase unless an auto-confirm rule they set covers automation purchases. The answer's buy.runs_as names the path: recorded_checkout or ai_purchase_session.
| Name | Required | Description | Default |
|---|---|---|---|
| buy | No | action='workflow': the workflow is a checkout and runs as a purchase. action='ai_task' (with a `prompt` saying what to buy): an AI purchase session browses to the order page and Writ checks out. Always saved with dry_run on (a rehearsal that places no order), whatever is sent; the user arms real purchases in the Writ app. Cloud monitors only; an AI purchase session can't use device_card. | |
| name | No | Optional automation name. | |
| title | No | ||
| action | Yes | What to do on a detected change. | |
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| prompt | No | action='ai_task': what the agent does when the monitor fires, e.g. "Check whether the price dropped below $500 and summarize what changed". Supports {{placeholders}} like {{diff_snippet}} and {{extracted.price}}. With `buy`, say exactly what to buy (product, quantity, options, shipping). | |
| enabled | No | ||
| message | No | Notification body template (supports {{event.url}}). | |
| channels | No | Notification channels, e.g. ["pushover","email"] — required for action='notify'; optional with action='ai_task' (finish alert). | |
| workflow | No | Workflow to run (name) — required for action='workflow'. | |
| entry_url | No | action='ai_task': page the agent starts on (defaults to the monitored URL). | |
| max_steps | No | action='ai_task': cap on agent steps per wake (default 20, max 100). | |
| threshold | No | Price watch: act only when the watched price reaches this number (e.g. 477.04), once; by default at or below it (see `threshold_op`), and a price on the other side, or one the page no longer shows, does nothing. Needs a monitor that reads the price (a selector or extract, not a visual zone). Without it, any change fires. Works for a desktop monitor too (`device`). | |
| monitor_id | Yes | Monitor id from writ_create_monitor. | |
| recipients | No | Notification recipients, e.g. ["pushover:1"]. Omitted = every enabled recipient on the channel; the answer names who the alert actually reaches, and warns when nobody is configured. | |
| workflow_id | No | ||
| threshold_op | No | Which side of `threshold` fires: lte = at or below (default: "drops to 477.04" fires at 477.04), lt = strictly below ("below / under / less than"), gte = at or above, gt = strictly above (a rise alert). | |
| ai_session_id | No | action='ai_task': re-run this saved AI session instead of (or as well as) a prompt. | |
| cooldown_minutes | No | action='ai_task': minimum minutes between wakes (default 10; 0 disables). |
writ_workflow_dataRead a workflow's collected dataRead-onlyIdempotentInspect
Reads the accumulated extracted data of a saved workflow as a table (columns + rows). q filters; run_id selects one run. Long text cells arrive preview-cut (_truncated lists the fields); refs returns full records. Crawl ids are a different namespace: a writ_crawl_site run's data lives behind writ_crawl_status / writ_saved_crawl_data, not here.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | Substring filter across fields. | |
| refs | No | Hydration: full untruncated records by ref '<run_id>:<record_index>' (both fields are on every row). Max 100. | |
| view | No | all | latest | run | |
| limit | No | ||
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| output | No | Response shape, for an answer a program or an API consumes rather than a reader. {shape: 'envelope' (default: Writ's full answer, projected) | 'table' ({columns, rows, total}) | 'records' (bare list of records) | 'record' (the newest record alone: one entity, a usage meter, a dashboard), fields: ['used', 'percent_used as pct', 'items.0.price as first_price'] (ordered pick, renames, dotted paths; a missing path is null, so keys are stable), exclude: ['depth'], include_meta: false (page metadata content_kind/depth/thumbnails is stripped unless true), key: 'usage' (wrap)}. On writ_crawl_site with save_as it is saved as the API's default shape. | |
| run_id | No | ||
| workflow | No | ||
| workflow_id | No | ||
| preview_chars | No | Cut string cells to this many characters (default 2000; 0 = full cells). |
writ_workflow_runsList workflow runsRead-onlyIdempotentInspect
Run history (status, timing, errors) for one workflow or across all of them.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| device | No | A linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any. | |
| status | No | pending|running|success|failed|cancelled|skipped | |
| workflow | No | ||
| workflow_id | No |
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
39 tool updates
- First observed
writ_browser_act - First observed
writ_browser_ask_user - First observed
writ_browser_cancel - First observed
writ_browser_compose - First observed
writ_browser_context - First observed
writ_browser_network - First observed
writ_browser_save - First observed
writ_browser_sessions - First observed
writ_browser_use - First observed
writ_crawl_files - First observed
writ_crawl_site - First observed
writ_crawl_status - First observed
writ_create_automation - First observed
writ_create_http_extraction - First observed
writ_create_monitor - First observed
writ_devices - First observed
writ_diagnose_http_workflow - First observed
writ_discovery_status - First observed
writ_export_data - First observed
writ_expose_workflow_api - First observed
writ_list_webhooks - First observed
writ_list_workflows - First observed
writ_payment - First observed
writ_personas - First observed
writ_pin_workflow_tool - First observed
writ_record_website - First observed
writ_run_saved_crawl - First observed
writ_run_workflow - First observed
writ_saved_crawl_data - First observed
writ_saved_crawls - First observed
writ_scrape - First observed
writ_search_data - First observed
writ_set_schedule - First observed
writ_update_saved_crawl - First observed
writ_update_workflow - First observed
writ_website_to_api - First observed
writ_wire_monitor - First observed
writ_workflow_data - First observed
writ_workflow_runs
Related MCP Connectors
Websites as APIs: read pages, run site tasks via learned APIs, cloud browser with saved logins.
Automate any website: discover, run and create browser scripts that work behind logins.
AI-powered browser automation — navigate, click, fill forms, and extract data from any website.
Turn any webpage into a structured action manifest — clickable, fillable, submittable elements.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceEnables AI assistants to browse websites, extract information, fill forms, interact with pages, and execute multi-step web tasks autonomously.-
- AlicenseNot gradedqualityBmaintenanceLets AI assistants control your real Chrome browser to perform web tasks like reading pages, taking screenshots, clicking, and typing, using your existing logged-in sessions.132MIT
- AlicenseNot gradedqualityDmaintenanceAutomate web browsing and data extraction by converting live pages into clean Markdown. Execute multi-step workflows and interact with websites using human-like movements.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search the web, read and extract content from webpages, fetch JSON from REST APIs, and collect links while bypassing anti-bot protections.19 npmISC
Glama MCP Gateway
Add one secure layer between your agents and this server.