Compose a workflow in a session
writ_browser_composeDefine, test, and save named callable functions from an open browser session, turning driven steps into a workflow API invoked by name.
Instructions
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
exampleand 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} — withfieldsit returns ROW OBJECTS, which is how a server-rendered list becomes a BROWSERLESS function: {from:'html_css',selector:'tr.athing',all:true,base_url:'',fields:{title:{selector:'.titleline > a'},url:{selector:'.titleline > a',attribute:'href'}}} on anapifunction that GETs the page (no browser at replay — prefer this over alist/scriptfunction whenever the rows are in the served HTML), regex {from:'regex',pattern:'...',group:1}, header {from:'header',name:'x-next'}, body {from:'body'}, or legacy '$.json.path'. 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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| payload | No | The operation's arguments. define_function: {name, fn_type?, description?, surface?, from_index?, request?{method,url,headers,body_template}, flow?{version,steps}, script?, selector?, input_variables?, output_fields?, response_extractions?, is_auth?, order?, sample_inputs?, test?}. compile_function {name, from_index, sibling_index?, input_variables?, response_extractions?, page_url?}: the DETERMINISTIC (no-LLM) way to turn a captured authenticated request — a GraphQL/RPC POST, a form submit — into a callable function. Writ decodes the body, keeps the static parameters, TRACES each session-minted token (csrf/xsrf/dtsg/lsd/etc.) to where a fresh session re-reads it and emits an is_auth bootstrap that publishes it as {{extracted:}}/{{cookie:}}, replaces per-call client values (idempotence token, session id, timestamp) with runtime GENERATORS ({{uuid()}}, {{uuid(session)}}, {{timestamp_ms()}}, {{counter()}}), templates your caller inputs, and CLASSIFIES a write (create/post/send/delete): the BUILD never sends it, a real run does (pass mutation_mode='dry_run' to preview). Prefer this over a hand-built api function for any authenticated mutation or token-bound endpoint; pass sibling_index=<another capture of the same request> so the pagination inputs are told apart from the minted noise. test_function: {name, sample_inputs?}. remove_function: {name}. add_steps: {steps:[{type, config|flat fields, description?}], at?}. remove_step: {id}. set_inputs: {inputs:{name:{default?, description?, required?, example?}}}. | |
| operation | Yes | ||
| session_id | Yes | Session id from the start tool. |