writ_browser_act
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`.
writ_browser_compose
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:<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 <surface>.<verb> (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:'<page>',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); a field with several values per row (tags, authors) is {selector:'a.tag',all:true} (a list; without all, the first only); a has_next flag is {from:'html_css',selector:'<next link>',exists:true} (true/false, never the link text) and the next page is its href {selector:'<next link>',attribute:'href',required:false}; 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:<name>}}.
- 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.
writ_browser_use
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.
writ_crawl_site
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 <site>', 'every page of the docs', 'all products in this category', 'a dataset of <site>', 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.
writ_website_to_api
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 <site> into an API", "map the API of <site>", "expose every feature", "give me an endpoint for <site>".
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=<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=<that 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.