Skip to main content
Glama

Writ Cloud

Server Details

Read, crawl and act on websites, signed in as the user, and turn any site into an API.

If you are the author of this connector, you can claim ownership by verifying the domain or GitHub account it belongs to. Claimed connector authors can inspect health checks, view analytics, and manage their listing.
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 tools
writ_browser_actAct in a browser session
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
inputsNoValues 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.
actionsYesOrdered action objects, e.g. [{"action":"click","selector":"#login"}].
max_charsNoClip 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_idYesSession 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'.

ParametersJSON Schema
NameRequiredDescriptionDefault
kindNocaptcha: 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.
questionNoThe question, in one short sentence (required for kind 'question').
session_idYes
wait_secondsNoHold up to this long (1-60, default 60).
writ_browser_cancelClose a browser session
DestructiveIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
discardNotrue = the session's unsaved steps and functions are thrown away instead of auto-saved. Default false.
session_idYes
writ_browser_composeCompose a workflow in a session
Destructive
Inspect

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 example on 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}; with fields it 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 an api function that GETs the page (no browser at replay, so cheaper than a list/script function 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
payloadNoThe 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?}}}.
operationYes
session_idYesSession id from the start tool.
writ_browser_contextRead a browser session
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
offsetNoPaging offset for the policy sections.
sectionNopage (default) | lists | map | explorer | concierge_api
max_charsNoCharacters per page (1000–10000, default 8000).
session_idYes
writ_browser_networkRead a session's network calls
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
indexNoWhich call to read, for operation=detail.
queryNoSubstring filter across method, url, status, and bodies.
methodNoFilter by HTTP method.
offsetNo
max_charsNoWindow size, 1000-10000 (default 8000; larger values are clamped); offset pages it.
operationNosearch (default) | detail. list/get are aliases.
session_idYes
writ_browser_saveSave a session as a workflow
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoShort workflow name (defaults to the goal).
keep_openNoLeaves the browser open after saving (default false: saving closes it).
session_idYes
descriptionNo
allow_no_dataNoSaves 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 sessions
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax sessions to return (default 20).
include_closedNoAlso lists recently closed sessions (not resumable) for reference. Default false: only open, resumable sessions.
writ_browser_useOpen a browser
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesStarting URL to open (required).
goalNoOptional 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_modeNoreuse 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_exitNoResidential 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_idNoSaved 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_layerNoNative 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_residentialNoOpens on the platform residential network (premium), for a site that blocks datacenter IPs or shows a bot wall / captcha. Default off (free).
execution_targetNoWhere 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_countryNoTwo-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 files
Read-onlyIdempotent
Inspect

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).

ParametersJSON Schema
NameRequiredDescriptionDefault
runsNoWith `crawl`: how many recent completed runs to aggregate (default 1 — the current version of every document).
crawlNoSaved crawl slug, name, or id (alternative to crawl_id).
limitNoMax files to return (default 100, cap 200).
crawl_idNoCrawl id from writ_crawl_site.
writ_crawl_siteCrawl a site into a dataset
Destructive
Inspect

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'): intent in plain language (the server derives include/exclude paths and depth from the site's real URLs) plus relevance_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_budget caps 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesSeed URL (required).
nameNo
waitNoHold 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.
limitNoRows of collected data to return when wait=true (default 50).
speedNoThroughput 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.
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
intentNoPlain-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.
outputNoResponse 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_ageNoOnly 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_asNoSaves 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_msNoPoliteness delay between fetches per host (default 250).
executorNoregular (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_modeNoauto (default) | off | force
rank_capNoTop 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_depthNo
seed_urlsNoExact known pages to start from: the crawl collects these instead of discovering its own. The cheapest way to scrape a known set.
persona_idNoSaved 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_sizeNoURLs fetched per shard batch (default 20).
page_budgetNo
render_modeNoHow 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_domainNo
content_specNoWhich 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_modeNomarkdown (default) | schema (uniform records via extract_schema) | html (each page's raw HTML, for selectors or embedded JSON)
exclude_pathsNo
include_pathsNo
preview_charsNoCut 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_promptNoRequired with executor=ai: what each agent extracts from each page, in plain language (e.g. 'the product name, price and SKU').
extract_schemaNo
respect_robotsNoHonor robots.txt (default true).
timeout_secondsNoMax seconds to hold when wait=true (≤75).
use_residentialNoRoute 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_subdomainsNo
relevance_thresholdNo0-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_countryNoTwo-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_shardsNoExplicit parallel-shard cap; overrides the `speed` allocation.
writ_crawl_statusCheck a crawl
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
waitNoHold until the crawl is terminal and inline its rows (default false).
limitNoRows to inline when it converged (default 50).
outputNoResponse 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_idYesCrawl id from writ_crawl_site.
preview_charsNoCut inline text cells to this many chars (default 12000; 0 = full).
timeout_secondsNoCeiling 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).

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesName for the automation (required).
whenNoEvent: 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`).
titleNoNotification title (with `notify`).
blocksNoRaw 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}}.
inputsNoWith 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.
notifyNoSend 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).
enabledNo
channelsNoNotification channels for `notify`, e.g. ["pushover","email"] (required for delivery).
priorityNo
scheduleNoWith 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_promptNoWake 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_idNoWith when='change_detected' (required there): the monitor whose changes fire this, i.e. the monitor_id writ_create_monitor returned.
recipientsNoNotification 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.
descriptionNo
on_workflowNoSource workflow name whose event fires this (required for workflow_* events).
ai_entry_urlNoPage 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_functionNoOne function name (alias of run_functions).
run_workflowNoWorkflow to run when the event fires (by name).
ai_session_idNoWith when='ai_session_completed' / 'ai_session_started': only this AI session.
run_functionsNoWith 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_idNo
run_workflow_idNo
cooldown_minutesNoMinimum minutes between AI wakes for `ai_prompt` (default 10; 0 disables).
target_selector_idNoWith target_id: only changes of this one selector of that monitor.
webhook_trigger_idNoWith 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 extraction
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
goalYesExact inputs, output fields, filters, ordering and pagination behavior wanted.
applyNofalse (default) returns a reviewable draft; true writes a valid draft to the workflow.
workflowNoExisting workflow name (or workflow_id).
step_indexNoapi_call step to replace; defaults to the first, or appends one.
workflow_idNo
requirementsNoExtra mapping, dedupe, filtering or cursor requirements.
desired_inputsNoCaller parameters such as query, min_price, max_price, limit and cursor.
request_samplesNoRelevant calls returned by writ_browser_network/capture_network, including representative response bodies when available.
response_sampleNoOptional 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesURL to monitor (required).
modeNo'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.
watchNo'price' also rejects a selector whose text holds no number (it becomes a zone). Default 'content'.
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
enabledNoStart the monitor enabled (default true).
extractNoBrowserless 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.
intervalNoHow 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`.
selectorNoCSS selector for content-change monitoring; unset = uptime/status monitoring.
zone_textNoThe text the page prints where the value is (e.g. '51,77 EUR', 'Currently unavailable'). Locates the zone when no selector exists.
persona_idNoEvery 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_idNoAn 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_anywayNoAfter a bot-check answer: create it on Writ's servers anyway (a check that meets the bot check reads nothing).
request_urlNoWith `extract`: the endpoint the value comes from (an XHR the page calls), when it is not `url` itself.
use_residentialNoCheck 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_minutesNoLegacy: minutes between checks; `interval` replaces it.
requires_browserNoRender 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_countryNoISO-2 exit country for use_residential (e.g. 'ca'); implies use_residential.
writ_devicesChoose a linked desktop
Idempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionNolist (default) | use | clear | monitors (that desktop's monitors) | datasets (what its exposed workflows collected).
deviceNouse: the desktop's agent_id (or its exact name) from list.
writ_diagnose_http_workflowDiagnose a workflow's HTTP lane
Read-onlyIdempotent
Inspect

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'.

ParametersJSON Schema
NameRequiredDescriptionDefault
task_idNoOptional run to verify actual HTTP execution and output.
workflowNo
workflow_idNo
writ_discovery_statusCheck an API build
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
waitNoOne 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_idYesThe build id from writ_website_to_api.
timeout_secondsNoCeiling for wait=true (≤75).
writ_export_dataExport collected data
Read-onlyIdempotent
Inspect

Exports a workflow's full extracted-data table as CSV or JSON (search/filter applied, unpaginated).

ParametersJSON Schema
NameRequiredDescriptionDefault
qNo
viewNo
formatNocsv (default) or json
workflowNo
workflow_idNo
writ_expose_workflow_apiPublish a workflow as a REST API
Idempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
labelNoOptional name for the endpoint.
workflowNo
workflow_idNo
wait_timeoutNoDeprecated alias of timeout_seconds.
timeout_secondsNoManaged run timeout. Defaults to 120, or 300 for AI navigation workflows.
writ_list_webhooksList inbound webhook URLsA
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
webhook_trigger_idNoOnly this webhook.

TDQS

A4/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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 workflows
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
searchNoOptional name/description filter.
writ_paymentPay with a card the user approves
Destructive
Inspect

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}.

ParametersJSON Schema
NameRequiredDescriptionDefault
siteNorequest: the store's domain, e.g. 'store.example.com'. The card is typed only on this site and its payment frames.
stepsNocheckout: 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.
actionYesrequest = 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.
fieldsNorequest, 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.
purposeNorequest: one line the user reads when approving, e.g. 'Buy Nike Dunk Low, size 10'.
sessionNocheckout / fill (required) / request: the session_id of the writ_browser_use session on the checkout page.
summaryNocheckout: one line the user reads when confirming, e.g. '1 x Nike Dunk Low, size 10, shipped to home'.
currencyNorequest / 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_idNowait / fill / checkout: the grant_id action='request' returned.
max_amountNorequest / checkout: the most this purchase may charge, tax and shipping included (e.g. 129.99). checkout without grant_id needs it.
automation_idNorequest: the automation the card is for, when the purchase is a saved automation.
total_selectorNorequest / 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_selectorNocheckout (required): CSS selector of the button that places the order. Writ clicks it after the user confirms.
confirmation_idNowait: the confirmation_id action='checkout' returned.
payment_method_idNorequest: 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
whyNorequest: one line the user sees in the window: why the account is needed.
waitNolist + domain: one call holds up to 75s for a persona for that site to appear. A link_id is awaited with action='wait'.
forceNosign_in: re-run the login even when the current session still looks usable.
actionYesWhat to do (default list). request = mint the persona link for a site (domain); wait = hold until the user saves it (link_id).
domainNolist: 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_idNowait: 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_urlNorecord_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_idNoWhich persona — required for get / sign_in / record_login.
include_runsNoget: include the persona's recent runs (which workflows acted as it, and whether they succeeded).
writ_pin_workflow_toolPin a workflow as a tool
Idempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
pinnedNotrue (default) pins; false unpins.
workflowNoWorkflow name (or workflow_id).
workflow_idNo
writ_record_websiteRecord a website task
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesWebsite URL to start on (required).
goalYesWhat should be recorded on the website, in plain language
deviceNoA 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_modeNoBrowser 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_exitNoResidential 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_idNoSaved 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_layerNoBrowser 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_existingNoAPI builds first propose the user's OWN matching workflows (replaying is instant and free); set true after the user declined those.
use_residentialNoOpen on the platform residential network (premium) for a site that blocks datacenter IPs or shows a bot wall. Default off (free datacenter egress).
skip_marketplaceNoAPI builds then propose compatible ready-made marketplace APIs; set true to skip that and record fresh.
residential_countryNoTwo-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
waitNoHold until the crawl converges (default false: a crawl is slow).
crawlYesSaved crawl slug, name, or id (from writ_saved_crawls).
limitNoRows of collected data to include (default 50).
outputNoResponse 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_ageNoReuse the last completed crawl if it finished within this many seconds. 0 (default) always re-crawls.
preview_charsNoCut 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_secondsNoMax seconds to wait when wait=true.
writ_run_workflowRun a saved workflow
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
waitNoWait for completion and return the data (default true).
filesNoOptional 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.
deviceNoRun on this linked Writ desktop (an agent_id from writ_devices) — overrides the desktop this connection chose with writ_devices action='use'.
inputsNoRun inputs (top-level fields work too).
outputNoResponse 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_ageNoOptional. 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.
workflowNoWorkflow name (or workflow_id).
persona_idNoRun 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_idNoA 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_nameNoOne 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_modeNoHow 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_namesNoSeveral 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_secondsNoMax seconds to wait for completion (default 120).
use_residentialNoPer-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_targetNoWhere 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_countryNoTwo-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 data
Read-onlyIdempotent
Inspect

Reads the data a saved crawl already collected on its most recent completed run, at any age. It never starts a crawl.

ParametersJSON Schema
NameRequiredDescriptionDefault
crawlYesSaved crawl slug, name, or id.
limitNoRows to return (default 25).
outputNoResponse 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_charsNoCut 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 crawls
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax 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, pages in 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoA page to read or, with `top_n`, the listing page whose top items are read.
urlsNoKnown pages to read together (max 20), fetched in parallel in one call.
top_nNoWith `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.
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
formatNomarkdown (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_idNoSaved 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_modeNoauto (default: plain HTTP, browser only if the page needs JS) | http | browser.
include_pathsNoWith top_n: regex(es) the item links match (e.g. 'item\\?id=', '/products/'). Optional: the server detects detail links when omitted.
preview_charsNoCut each page's text to this many characters (default 12000; 0 = full pages).
respect_robotsNoApply robots.txt to the explicitly requested page(s). Default false for scrape; writ_crawl_site defaults true for autonomous discovery.
use_residentialNoFetch through the platform residential network (premium) for a site that blocks datacenter IPs or shows a bot wall. Default off.
residential_countryNoTwo-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 data
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qYesSearch term (required).
limitNoRows per workflow (default 10).
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
workflowNo
workflow_idNo
preview_charsNoCut matched string cells to this many characters (default 300; 0 = full cells).
writ_set_scheduleSchedule a workflow
DestructiveIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
tzNoIANA timezone for daily/weekly.
daysNoISO weekdays for weekly: 1=Mon .. 7=Sun.
kindNointerval | daily | weekly
timeNoHH:MM local time for daily/weekly.
inputsNoScheduled-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.
enabledNoSchedule on or off (default on).
functionNoOne function name (alias of `functions`); "" or "all" = the whole workflow.
workflowNo
functionsNoFunction names a scheduled run calls (from writ_list_workflows / writ_update_workflow). [] or ["all"] = the whole workflow. Omitted: current target kept.
workflow_idNo
every_minutesNoInterval schedule: minutes between runs.
writ_update_saved_crawlEdit a saved crawlA
DestructiveIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNo
crawlYesSaved crawl slug, name, or id.
settingsNoSparse crawl config patch merged recursively into the complete saved settings.
descriptionNo
default_max_age_secondsNo

TDQS

A3.9/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 workflow
Destructive
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathNoJSON Pointer into the selected step, e.g. /config/flow/steps/2 or /config/script.
limitNo
patchNoSparse 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.
offsetNo
sectionNosource reads a source window; flow lists nested node paths; contract lists operators and their semantics without requiring a workflow.
step_idNoStable step id that picks one step of a multi-step function.
verboseNoEdits answer with the full workflow definition instead of the compact step outline (default false).
operatorNosection=contract: one operator, with its operands, semantics and example.
workflowNoWorkflow name (or workflow_id).
max_charsNo
workflow_idNo
step_updatesNo
function_nameNoSource of this function only.
replace_stepsNoComplete recorded-step replacement; step_updates is the targeted edit.
validate_onlyNoPreview/validate without saving or executing.
function_updatesNo
regenerate_skillNoRebuild 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_atNoWorkflow 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoThe page that already shows the rows (search-results, category or listing URL), not the home page. Required unless build_id continues a parked build.
goalNoWhat 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.
modeNointelligent (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.
levelNoWrite 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.
scopeNoCrawl lanes: maps only this surface (e.g. 'employees') and what it depends on. No value maps the whole app.
deviceNoA 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_asNoName for the workflow the build saves.
build_idNoContinues a parked build (status needs_guidance from writ_discovery_status) on the guided rung: opens the browser bound to it, seeded with its map.
anonymousNoBuilds 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_idNoSaved 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_superviseNoAI-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_existingNoSkips the proposal of the user's own matching workflows (for after they declined).
respect_robotsNoCrawl 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_residentialNoRuns 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_targetNo'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_marketplaceNoSkips the ready-made marketplace proposals and builds fresh.
residential_countryNoTwo-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
buyNoaction='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.
nameNoOptional automation name.
titleNo
actionYesWhat to do on a detected change.
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
promptNoaction='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).
enabledNo
messageNoNotification body template (supports {{event.url}}).
channelsNoNotification channels, e.g. ["pushover","email"] — required for action='notify'; optional with action='ai_task' (finish alert).
workflowNoWorkflow to run (name) — required for action='workflow'.
entry_urlNoaction='ai_task': page the agent starts on (defaults to the monitored URL).
max_stepsNoaction='ai_task': cap on agent steps per wake (default 20, max 100).
thresholdNoPrice 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_idYesMonitor id from writ_create_monitor.
recipientsNoNotification 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_idNo
threshold_opNoWhich 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_idNoaction='ai_task': re-run this saved AI session instead of (or as well as) a prompt.
cooldown_minutesNoaction='ai_task': minimum minutes between wakes (default 10; 0 disables).
writ_workflow_dataRead a workflow's collected data
Read-onlyIdempotent
Inspect

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qNoSubstring filter across fields.
refsNoHydration: full untruncated records by ref '<run_id>:<record_index>' (both fields are on every row). Max 100.
viewNoall | latest | run
limitNo
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
outputNoResponse 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_idNo
workflowNo
workflow_idNo
preview_charsNoCut string cells to this many characters (default 2000; 0 = full cells).
writ_workflow_runsList workflow runs
Read-onlyIdempotent
Inspect

Run history (status, timing, errors) for one workflow or across all of them.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
deviceNoA linked Writ desktop's agent_id (writ_devices) to act on. Omitted: the desktop this connection chose with writ_devices action='use', if any.
statusNopending|running|success|failed|cancelled|skipped
workflowNo
workflow_idNo

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 39 tool updates
    • First observedwrit_browser_act
    • First observedwrit_browser_ask_user
    • First observedwrit_browser_cancel
    • First observedwrit_browser_compose
    • First observedwrit_browser_context
    • First observedwrit_browser_network
    • First observedwrit_browser_save
    • First observedwrit_browser_sessions
    • First observedwrit_browser_use
    • First observedwrit_crawl_files
    • First observedwrit_crawl_site
    • First observedwrit_crawl_status
    • First observedwrit_create_automation
    • First observedwrit_create_http_extraction
    • First observedwrit_create_monitor
    • First observedwrit_devices
    • First observedwrit_diagnose_http_workflow
    • First observedwrit_discovery_status
    • First observedwrit_export_data
    • First observedwrit_expose_workflow_api
    • First observedwrit_list_webhooks
    • First observedwrit_list_workflows
    • First observedwrit_payment
    • First observedwrit_personas
    • First observedwrit_pin_workflow_tool
    • First observedwrit_record_website
    • First observedwrit_run_saved_crawl
    • First observedwrit_run_workflow
    • First observedwrit_saved_crawl_data
    • First observedwrit_saved_crawls
    • First observedwrit_scrape
    • First observedwrit_search_data
    • First observedwrit_set_schedule
    • First observedwrit_update_saved_crawl
    • First observedwrit_update_workflow
    • First observedwrit_website_to_api
    • First observedwrit_wire_monitor
    • First observedwrit_workflow_data
    • First observedwrit_workflow_runs

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to browse websites, extract information, fill forms, interact with pages, and execute multi-step web tasks autonomously.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Lets 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.
    132
    MIT
Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.