| writ_browser_useA | A REAL CLOUD BROWSER FOR A TASK ON A WEBSITE: the user's own signed-in account (email, social, shop, bank or work portal — pass a persona_id from writ_personas; the password stays sealed in Writ, so never ask for a password), a click, form, submit, search inside an app, setting change or buy/book/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 — YOU are the brain and the driver (Writ runs no model here): do every step with writ_browser_act(session_id), turn by turn, until the task is done. Call writ_browser_use ONCE per task; never again for the same task, and never wait for it to finish anything.
RECORDING IS ALWAYS ON, SAVING IS ON DEMAND: every interaction you drive is recorded. If the user wants to REUSE the task ("record it", "so I can re-run it"), drive it the recordable way from the first action — a value they will want to change goes in as {{name}} with the real value in writ_browser_act inputs, the data goes out through an extract action — then writ_browser_save(name): its answer lists the steps, inputs and a run_example, and the workflow replays at zero AI cost (writ_run_workflow). Otherwise just finish the task and writ_browser_cancel — an open browser bills until it is closed.
WHAT YOU CAN DO IN IT: 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: just READING a page, a few pages, or the top N items of a listing — that is 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, so open one only when the task needs interaction. The page comes back after every batch and on demand via writ_browser_context(section=page). FOLLOW THE USER'S DIRECTIONS and ASK the user directly in chat whenever you need a decision, a value to type, a credential, or a 2FA/OTP code — never guess or invent secrets (for a sensitive fill set data_key so 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). Prefer replaying an existing saved workflow (writ_list_workflows -> writ_run_workflow) when one already does the task. BLOCKED BY A BOT WALL / CAPTCHA? A browser's exit IP is fixed once it opens, so cancel it (writ_browser_cancel) and reopen with use_residential=true. And before opening a browser at all, call writ_browser_sessions: an open one is warm and cheaper to continue than a new one. |
| writ_record_websiteA | Record a repeatable website TASK as a workflow that replays on demand. Use whenever the user asks to record, capture, teach, automate or repeat actions on a site and the point is the TASK, not an API surface. Writ opens a real cloud browser and returns an observation; YOU are the brain: drive it with writ_browser_act, author what the recorder cannot see with writ_browser_compose (inputs a caller passes, explicit steps, named functions), and call writ_browser_save when the goal is complete — the saved workflow then 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 to writ_website_to_api's intelligent ladder automatically (static crawl, rendered crawl, then Writ's AI browser rung), so the recording is a real build. Before anything runs it proposes the user's OWN matching workflows (existing_workflows) and ready-made marketplace APIs (marketplace_candidates); skip_existing / skip_marketplace bypass those. |
| writ_website_to_apiA | TURN A WEBSITE INTO A CALLABLE API — the one tool for this, every lane. Use it whenever a service has no official/practical API but the user wants its data or actions programmatically: "turn into an API", "map the API of ", "expose every feature", "give me an endpoint for ".
THE WHOLE JOB IS 3 CALLS: (1) this tool with url + goal. START ON THE PAGE THAT ALREADY SHOWS THE ROWS (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), and name the inputs and the fields wanted ("page number in; quotes with text/author/tags and has_next out") — + save_as; (2) writ_discovery_status(build_id, wait=true): ONE held call that follows every rung; (3) on succeeded, run it exactly as the answer's run_example shows (writ_run_workflow: workflow_id + function_name + inputs), with TWO different inputs, and check the answers differ — then report. Do not open a browser or start a second build for the site meanwhile. An answer of existing_workflows / marketplace_candidates is a PROPOSAL: run the match, or call again with skip_existing / skip_marketplace for a fresh build. status needs_guidance = the build is YOURS: call again with mode=guided build_id= (you are the brain of that browser). An empty run → writ_diagnose_http_workflow(workflow_id, task_id).
LOGIN: if the app is behind a sign-in, ASK THE USER which saved identity to use (writ_personas) and pass its persona_id — never guess or type credentials; a persona also carries 2FA.
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).
DEFAULT = intelligent: Writ runs the WHOLE cost ladder for you, cheapest rung first, and you only start it and wait. The ladder: the user's OWN matching workflows (answered as existing_workflows — propose replaying those; skip_existing=true to bypass), ready-made MARKETPLACE APIs (marketplace_candidates; skip_marketplace=true), a STATIC HTTP crawl (forms, search boxes and query links become functions with inputs; inline JS; OpenAPI/Swagger specs; server-rendered listings), then a RENDERED crawl for JS/SPA pages, then Writ's AI BROWSER rung (its discovery brain drives a browser, ranks data-bearing traffic, promotes the site's own HTTP requests, tests inputs and pagination, saves the workflow). A rung that proves enough ENDS the build there; one that does not escalates, and the status of the newer rung carries escalations: why each cheaper rung handed over (robots.txt refused the crawl, no pages fetched, nothing matched the goal, no structured list...). Read it before telling the user why a browser was needed. A crawl-rung result is UNVERIFIED (verified:false) until a real run proves it — say so.
ROBOTS: the crawl rungs obey the site's robots.txt by default. A site that disallows the target (many search paths; some whole hosts) admits zero pages, so both crawl rungs end at once and the build goes to the AI browser. When escalations names robots.txt and the user vouches for the target, call again with respect_robots=false.
mode=auto is the same ladder with YOU as the last rung: the crawl rungs run, and when they do not prove enough the build PARKS as status=needs_guidance with map (every endpoint seen, specs, candidate functions) instead of spending Writ's agent. Continue it — or start directly — with mode=guided (and build_id=). That opens a real browser BOUND TO THE BUILD that you drive turn by turn (writ_browser_act: navigate, sign in, capture_network, evaluate_js, read calls with writ_browser_network), on which you DEFINE the API (writ_browser_compose define_function — api functions from captured calls via from_index, or proven scripts/extractions; each is live-tested as you define it; 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 GATE: this browser is the experiment bench. Capture a representative search/filter and next-page request, then define direct API functions. Use the Auphan-style named function graph by default: ordered is_auth functions publish tokens/ids/origins through response_extractions and data functions consume {{extracted:name}}. Use config.flow only for loops, recursive mapping, cross-page dedupe or composite returns. Typed extraction sources are json, embedded_json, html_css, regex, header and body. Expose search/filter/limit/page/offset/cursor as declared inputs and return next_cursor/next_offset/has_more. After save, run with the intended persona and call writ_diagnose_http_workflow(task_id=...). Do not expose until engine=http returns non-empty data and pagination matches the browser baseline, unless you can name a measured browser-only dependency.
Pass mode=guided to drive the browser yourself; mode=fast / mode=browser START on that crawl rung with you as the driver (it parks as needs_guidance when it falls short, exactly like auto). |
| writ_discovery_statusA | Poll a build started by writ_website_to_api. Terminal states are succeeded, failed and cancelled. RESTING state: needs_guidance — Writ's mechanical rungs are done and the build is YOURS to finish: it carries map (endpoints seen, specs, candidate functions) and you continue it with writ_website_to_api mode=guided build_id=. A rung the ladder replaced reports superseded=true with fallback_build_id; with wait=true this tool FOLLOWS that pointer for you and answers with the rung now running (followed_from lists the ids it walked; poll the returned build_id 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...) — quote it when you explain 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; say so when you report them. Run any function with writ_run_workflow (function_name), and fetch the generated API docs (OpenAPI 3, Markdown or a Postman collection, all pointing at the real Writ endpoint) from GET /api/v1/workflows/{workflow_id}/api-docs. |
| writ_browser_actA | Run one batch of actions on an open browser session and get the fresh page back. YOU are the brain: every navigation, click, fill, sign-in, capture, probe and script is yours to decide, one batch at a time. No writ_browser_compose in this client? This tool composes too: actions=[{action:'define_function', name:'feed.list', from_index:3, ...}] or [{action:'compose', operation, payload}], never mixed with clicks in one batch. After a navigate, a click or a select that changes the page, END the batch and look at the new page before acting on it. RECORDING RULES: (1) a caller INPUT — write the value as {{name}} in the action (select/fill/type_text value, navigate url) and pass the real value in inputs ({"name": "real value"}); the page gets the real value, the recorded step keeps {{name}}, and name becomes a workflow input by itself. (2) DATA — record an extract {variable,script} at the position that shows it (a read-only JS IIFE returning rows/fields); its result comes back in this answer, so check it before saving. (3) a SECRET — fill with data_key, never a literal. Interactions (navigate/click/fill/select/press_key/...) 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 truly 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?} · 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 with a css path to target next) · 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 — start here for any list/table) · list_frames · get_dom {selector?,depth?,max_chars?} (the real cleaned HTML — the expensive last resort) · 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 — see 2FA RULES). HEAR: get_console {level?,since?,query?,limit?} (console messages, uncaught JS errors with stack, failed/blocked requests since your last read — the page's console_since_last_read counts tell you when it is worth a call; read it BEFORE guessing 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 — how you find the site's real API; then search/read them with writ_browser_network) · get_request {url substring} (one call in full). RUN CODE: evaluate_js {script} (any JS on the live page, returns JSON — your main probing tool; read-only). 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} (replay a sign-in as one request) · probe_write {selector} (learn a create/update/delete request WITHOUT sending it) · confirm_write {selector} (perform it ONCE for its real confirmation — changes real data, only when authorized). A sensitive fill MUST carry data_key so the value is held server-side and the saved step keeps a {{secret:...}} placeholder. ASK the user for any credential, 2FA code or decision — never invent one. 2FA RULES: the one-time code is minted server-side from the attached persona and never shown to you. BEFORE twofa, READ the challenge and IDENTIFY the method the page is using — a phone number / 'text message' = sms, an email address = email, 'authentication app' = authenticator, approve-on-phone / passkey / QR / WhatsApp = other — and pass it as challenge_method. A persona receives exactly ONE method (see twofa_method in writ_personas) and a site picks its own default: Facebook texts an SMS even when the account has email. If the page's method is not the persona's, do not emit twofa: click the page's 'Try another way' / 'Use another method' / 'More options' / 'Didn't get a code?' control (in the page's own language), choose the persona's method, confirm, THEN twofa. On twofa_method_required, twofa_method_mismatch or twofa_verify_method do exactly what the message says — verify the method on the page and switch or resend — and do NOT ask the user yet. Only twofa_mint_failed / twofa_no_persona mean: call writ_browser_ask_user kind='twofa' so the Writ user supplies it. |
| writ_browser_contextA | Read 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 endpoints, specs and candidate functions Writ's crawl rungs found (evidence to verify), plus what you have composed so far. For ONE list/search API: open the guided session ON the results URL, read section=lists, pass its define_function to writ_browser_compose, save. section=lists SCANS the live page for you at no AI cost: the repeating rows, their field selectors, which captured request carries them, and a live-tested define_function payload to hand to writ_browser_compose. Read it BEFORE probing a results 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. |
| writ_browser_networkA | Search or read the requests the live page has made — how you find a site's real backend API instead of scraping its HTML. operation=search lists matching calls (filter with 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. Nothing captured yet? Run the capture_network action with writ_browser_act first — it reloads the page with capture armed; to catch a POST (login/search/submit), perform the action that triggers it, then capture. A call you want as a callable function goes straight into writ_browser_compose define_function via from_index=. Held credential values are replaced with their placeholder in the output. |
| writ_browser_composeA | AUTHOR the workflow being built in an open browser session — the power to turn what you drove into a real, complex, callable workflow rather than a replay of clicks. YOU decide its shape.
WHEN YOU NEED IT: not for a plain recording — there a caller input is a {{name}} value + inputs on writ_browser_act and the data is an extract action. Use this to expose NAMED FUNCTIONS (an API), to give an input a description/default, or to add a step the recorder cannot see.
OPERATIONS: define_function {name, fn_type api|list|script|extraction, ...} · compile_function {name, from_index} — DETERMINISTIC (no-LLM) capture->function: 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).
FASTEST PATHS: (a) a list / table / search-results page → writ_browser_context section=lists returns a live-tested define_function payload; pass it here with then_save:{name} — ONE call defines, tests and saves. (b) a site endpoint → capture_network, find the call with writ_browser_network, then 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 you define it; a failed test keeps NOTHING — fix it and define it again with the SAME name. Saved functions are called with 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 — pass from_index=<a captured call's index from writ_browser_network> and Writ seeds method/url/headers/body from the capture; override request fields to parameterize them with {{name}} placeholders; secrets as {{secret:name}}, anti-CSRF echoes as {{cookie:NAME}}), script (a read-only JS IIFE returning the data from the page), list (PREFERRED for any list/table: row_selector + fields {name: sub-selector | {selector, attr}}; the JS is generated for you, and writ_browser_context section=lists hands you this payload ready-made), or extraction (one selector's text). A list/script/extraction function reads the page it was defined on: pass page_url as a template (https://site/search?q={{query}}) or give each input_variable an example and the URL is templated from it. Add then_save:true (or {name, description}) to SAVE the workflow the moment the function passes its live test: one call instead of compose then save. Name it . (orders.list, orders.create) so functions group by surface. Declare input_variables=[{name,description,required,example}], output_fields, and response_extractions for the fields callers 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 — prefer this over 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'. Default to 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. Pass flow={version:1,steps:[...]} only for request loops, recursive mapping, cross-page dedupe, cursor pagination or a composite return. The flow is schema-validated and live-tested immediately in the current browser session with its cookies, persona and egress, using the same interpreter as the saved HTTP lane. 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:}}. The function is LIVE-TESTED the moment you define it (an in-session request, or a DOM read) with sample_inputs={name: value}; a failed test returns feedback and keeps NOTHING — fix it and define it again with the SAME name. test=false skips the proof (a real run proves it later). test_function {name, sample_inputs}: prove 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; every {{name}} in a step or function must be a declared input, a credential, a {{cookie:}}/{{extracted:}} runtime reference, or produced by an earlier step, or the save is refused. Credentials are never inputs — they come from the persona or a data_key fill. list: the draft so far (steps, functions, inputs, build).
Then 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.
|
| writ_browser_saveA | Save the open browser session as a clean, replayable workflow and close the browser. Everything you composed (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 with 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 — fix it in the session and save again. Only save once the task actually worked on the live page — verify first. |
| writ_browser_ask_userA | Ask 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 you cannot decide yourself. Use it when writ_browser_act returns security_check with auto_solved false, when a twofa action fails with twofa_mint_failed or twofa_no_persona (kind='twofa'), or whenever only a human can proceed. A one-time code is NOT a first-resort ask: when twofa answers twofa_method_mismatch or twofa_verify_method, first verify on the page which method it is using and switch it to the persona's (or resend) as the message says; interrupt the user only once that has failed. Never try to click through a CAPTCHA yourself, and never ask for a one-time code through kind='question' or type one into the page: with kind='twofa' the user pastes the code in the Writ app and Writ enters it server-side, so it never reaches you. 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' (call again with the same session_id to keep waiting — do not act meanwhile), or 'expired'. |
| writ_browser_cancelA | Close an open browser session. Call this when the task is done and the user does not want to reuse it, or when abandoning a session — an open cloud browser keeps consuming execution time until it is closed. Work you never saved is NOT lost: 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; inactive draft when it would not replay). Pass discard=true to close without keeping anything. A session bound to a build is settled by that auto-save, or marked cancelled. |
| writ_browser_sessionsA | List the cloud browser sessions this account has open, so you can RESUME one instead of opening a second browser beside it. A session you already opened is warm, parked on its current page, and keeps billing while it stays open — so when you need a browser, check here first and continue an open one by passing its session_id to writ_browser_act / writ_browser_context, rather than calling writ_browser_use again. Returns each session's id, status, resumable flag, current url and goal. |
| writ_personasA | The user's OWN accounts on websites. When a task needs them signed in (their 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, so never ask for one. 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. USE one by passing its persona_id to writ_browser_use, writ_crawl_site, writ_scrape or writ_run_workflow. BEFORE asking the user for credentials for a site, call action='list' (filter by domain). 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 always sign itself back in. NONE FITS? This tool can NOT create a persona and no credential ever passes through it. list with a domain answers persona_needed: a tell_user, a create_url (a MINTED link: 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). Relay it BEFORE starting the task, then action='wait' link_id=: ONE held call that answers the moment the user saves it, with the persona_id — continue on your own. ALSO LISTED: the personas of the user's linked Writ DESKTOP (source='device', id device:<agent>:<id>, name and site only). Pass that id as persona_id to writ_run_workflow, writ_browser_use / writ_record_website, writ_scrape or writ_crawl_site: the work goes to that desktop, which signs in from its own vault and only on the persona's own site - the credentials never leave it. |
| writ_paymentA | Pay on a website with one of the user's cards, without the card number ever reaching this conversation: the AI 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. PLACING THE ORDER: 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. You never click an order button yourself (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}. |
| writ_devicesA | The user's LINKED WRIT DESKTOPS (they can have 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; list them with action='monitors') 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. Use this when the user says 'on my laptop', 'on my work computer', or wants their own browser and logins. |
| writ_list_workflowsA | List the workflows saved in your 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. |
| writ_run_workflowA | Run a saved workflow by id or name and (by default) wait for it to finish, returning the extracted data. Pass workflow inputs as top-level fields or under inputs, and any file inputs under files. |
| writ_update_workflowA | Inspect or completely customize an existing saved workflow in place. An edit answers with a compact outline of the steps (verbose=true for the full definition). An api_call step is {type:'api_call', config:{function_name, method, url, headers, body_template, response_extractions}} — function_name binds the step to the function of that name, which is what writ_run_workflow function_name selects. With no edit arguments, returns the full workflow and its zero-based step indexes. patch changes workflow settings including persona, residential egress/country, human behavior, headless/fast mode, device routing, timeouts, retries, schedules, auth/browser config, functions, streaming, sessions and raw replay. step_updates recursively edits one or more recorded steps (selectors, scripts, URLs, waits, flags, extraction config, and generic api_call.config.flow programs); replace_steps deliberately replaces the complete step list. AGENT SKILL: skill_md (in the read) is the SKILL.md that teaches an agent to call this workflow over MCP; regenerate_skill=true rebuilds it from the current functions, then patch.skill_md writes your edited version (YAML frontmatter name + description, then Markdown); patch.skill_md=null removes it. |
| writ_create_http_extractionA | Create or revise an advanced browserless HTTP extraction from plain language and real browser-network evidence. Use AFTER a browser experiment/capture_network when one simple request or an Auphan-style named auth/function graph is insufficient (request loops, GraphQL descriptor discovery, recursive JSON, cross-page dedupe, sorting, cursor pagination). Ordinary login/bootstrap/data chains belong in writ_browser_compose define_function with is_auth/order/typed response_extractions. This tool generates a universal api_call.config.flow; site behavior stays inside the workflow. First call with apply=false to review validation, then apply=true. After applying, run the workflow and prove engine=http with writ_diagnose_http_workflow before exposing it. Never put fetch in evaluate_js and never send Writ-only controls to the site. |
| writ_diagnose_http_workflowA | Diagnose 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. Pass task_id after a representative run to confirm 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) — read that FIRST 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'. |
| writ_pin_workflow_toolA | Pin (or unpin) a saved workflow as its own run_ tool on this server. Workflows are NOT exposed as individual tools by default — every one is always callable via writ_run_workflow — so pin only the few the user runs often enough to deserve a first-class tool (the derived list is capped). Do this when the user asks for it, or after saving a workflow the user clearly intends to call as a tool from here. |
| writ_workflow_dataA | Read the accumulated extracted data for a saved WORKFLOW as a table (columns + rows). Filter with q, or inspect one run with run_id. Long text cells arrive preview-cut (_truncated lists the fields) — hydrate full records via refs. NOTE: crawl ids are a different namespace — a writ_crawl_site run's data lives behind writ_crawl_status / writ_saved_crawl_data, not here. |
| writ_search_dataA | Search across everything already collected by your workflows 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; fetch full records with writ_workflow_data(refs=...). |
| writ_export_dataB | Export a workflow's full extracted-data table as CSV or JSON (search/filter applied, un-paginated). |
| writ_workflow_runsB | Inspect run history — status, timing, errors — for one workflow or across all of them. |
| writ_set_scheduleA | Schedule a saved workflow to run automatically. Use 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 to fill. |
| writ_expose_workflow_apiA | Publish a saved workflow through Writ's managed REST gateway, using the same resource as the frontend's REST endpoint switch. The returned POST URL waits for the workflow and returns its JSON result. Repeated calls reuse the existing endpoint. |
| writ_crawl_siteA | COLLECT A SITE (or a section of it) INTO A DATASET — a distributed Dragnet crawl that discovers pages and stores every one as a queryable, change-tracked row. This is the tool for 'crawl ', 'get every page of the docs', 'all products in this category', 'build a dataset of ', or anything that will be queried, exported, monitored or re-run later.
NOT FOR: reading a page or a handful of pages right now — that is writ_scrape (url / urls / top_n answers in one call, no dataset); acting on a page (writ_browser_use). CHOOSE THE MODE — all three fetch pages the same way; they differ in who READS each page: CLASSIC (default: extract_mode='markdown', executor='regular') — every page becomes clean markdown, no AI spent, fastest. Right for content, docs, articles, discussions (threads keep [top-level]/[reply · depth N] tags). Pick this unless a rule below applies. SCHEMA (extract_mode='schema' + extract_schema) — every page holds the SAME structured record (a product, a listing row) and you want rows, not prose. Deterministic CSS extraction, no AI. AI-ASSISTED (executor='ai' + extract_prompt) — the wanted fields 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 — never use it for a few pages you could read yourself, and never to 'be sure'.
SCOPE IT — an unscoped crawl of a real site collects hundreds of nav, tag and pagination pages and bills for every one. Match the ask to a shape: A SECTION ('the docs', 'the pricing and blog pages'): pass intent in plain language — the server derives include/exclude paths and depth from the site's real URLs — and relevance_threshold ≈0.3 to drop off-goal pages. KNOWN PAGES as a dataset: seed_urls (no discovery). For an immediate answer use writ_scrape(urls) instead. TOP-N of a listing as a dataset (re-run later, monitored): rank_cap=N. For an immediate answer use writ_scrape(url, top_n) instead. WHOLE SITE ('every page'): the defaults; set page_budget to cap 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 to poll with writ_crawl_status — results land as a workflow dataset (writ_workflow_data, writ_search_data, writ_export_data). If the ask mentions comments or discussion, set content_spec {"preset": "full", "include_comments": true} (rank_cap crawls do this already). Behind a login: persona_id — never sign in yourself. save_as ONLY when the user will re-run it; writ_saved_crawls lists those — re-run one only when its scope matches the ask. YOU OWN THE RESPONSE SHAPE. Every answer is Writ's envelope by default (definition + crawl status + a data table whose rows wrap fields in run bookkeeping, and whose records carry page metadata like content_kind/depth). When you are BUILDING AN API on a crawl — anything a program or the user will consume — set output so the answer is THEIR 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 (override per call on writ_run_saved_crawl / writ_saved_crawl_data). Do NOT try to prompt the metadata away in extract_prompt — it is added after the model answers; output is the fix. |
| writ_saved_crawlsA | List the crawls the user SAVED for re-running (callable by API, each with a scope: seed_url, rank_cap, include_paths, extract_mode, executor). Use one via writ_run_saved_crawl(max_age=…) when its scope matches the ask — recent data then comes back instantly and costs nothing. A saved crawl of a DIFFERENT page, or one using executor=ai, is not a shortcut for a fresh question: start writ_crawl_site(rank_cap=N) instead. |
| writ_update_saved_crawlA | 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. |
| writ_run_saved_crawlA | Run a saved crawl with its stored settings. Pass max_age to get the data it already collected if that run is recent enough — the cheap path. Otherwise it re-crawls. The response carries _cache.hit and _cache.age_seconds so you can tell which happened. |
| writ_saved_crawl_dataA | Read the data a saved crawl already collected on its most recent completed run. Never starts a crawl — use this when you want what is already there, at any age. |
| writ_scrapeA | 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). This is the tool 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. Add include_paths=['item\?id='] when you know the item-link shape; the server otherwise 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 tells you how to fetch a full page. YOU read the markdown — no AI is spent here. Behind a login: persona_id. Bot wall: use_residential=true. |
| writ_crawl_statusA | Status of a crawl by id — page counts, status, the dataset workflow id. With wait=true it is ONE held call (≤75s) that returns when the crawl converges, WITH the collected rows inline (data, shaped by output): the answer to a crawl tool's 504 / crawl-id handle. Never poll this in a loop — pass wait=true and, if it is still running at the ceiling, call it once more the same way. |
| writ_crawl_filesA | 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; use this when you want the actual files. Pass crawl_id for one run, or crawl (saved crawl slug/name/id) for its most recent completed run(s). |
| writ_create_automationA | Create an automation: on an EVENT, run a workflow, send a notification, and/or wake an AI agent. Chain workflows (when workflow A completes → run workflow B), alert on completion, or have an agent act on the event (ai_prompt). Give a source workflow via on_workflow for workflow_* events, and at least one of run_workflow / notify / ai_prompt.
EMAIL THE DATA, not just that it ran — notify is a template over the event: 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. THE DIGEST PATTERN: writ_set_schedule on the workflow (or a saved crawl), then this with when=workflow_completed / 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): 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). |
| writ_list_webhooksA | 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. |
| writ_create_monitorA | Create a MONITOR — a target Writ checks on a schedule and that fires a change_detected event when the page, a CSS selector's text, or a visual ZONE of the page changes. Use when the user wants to WATCH a URL for changes/updates. Returns the monitor id for writ_wire_monitor.
PROVE THE SELECTOR FIRST: open the page with writ_browser_use and pass its session_id — 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. NO SELECTOR FOUND 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 — wire a change alert, not a threshold. selector_check in the answer says what was done.
HOW OFTEN + ACCOUNT: no interval → needs_input (this plan's options + a tell_user to relay; 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 → it offers use_residential. |
| writ_wire_monitorA | Wire a monitor's change_detected event to an action. action='workflow' runs a saved workflow when the monitored page changes; action='notify' sends a notification (provide 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 (add channels/recipients to also get notified when it finishes). Use after writ_create_monitor to make the monitor DO something on change. 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. AUTO-BUY: action='workflow' with a checkout workflow and buy (payment {kind, ref} from writ_payment, approval, spend cap) makes it a purchase, always saved as a rehearsal (dry run) that stops before the order is placed; the user turns real purchases on in the Writ app. |