Skip to main content
Glama

Compose a workflow in a session

writ_browser_compose
Destructive

Define, 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 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.

Input Schema

TableJSON 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 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?}}}.
operationYes
session_idYesSession id from the start tool.

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observedv1.1.0

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Well beyond the annotations (destructiveHint/openWorldHint/idempotentHint), the description discloses that every function is live-tested on definition, that a failed test 'keeps NOTHING', that the build never sends writes while a real run does, that a save is refused if a {{name}} is undeclared, and that credentials are never inputs. These are exactly the behavioral traits an agent needs and are not inferable from the hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose, then WHEN YOU NEED IT, OPERATIONS, FASTEST PATHS and DETAILS, which is strong structure for an eight-operation tool. It is nonetheless extremely long, and the 'live-tested / failed test keeps NOTHING / redefine with the SAME name' point is repeated in both FASTEST PATHS and DETAILS, costing some conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/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 still covers return shape (functions callable by name, api-docs endpoint), save behavior, draft inspection via `list`, and per-operation argument semantics for all eight operations. Given the complexity and nested payload, almost nothing an agent needs to call it correctly is missing.

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 67% and the payload schema is already richly documented, so the baseline is high. The description nonetheless adds real semantic depth the schema lacks: {{name}} placeholder semantics, {{secret:}}/{{cookie:}}/{{extracted:}} references, response_extractions spec formats, and then_save testing behavior. Some of this overlaps the existing payload description, keeping it just shy of a 5.

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?

The opening line states a specific verb and resource ('AUTHOR the workflow being built in an open browser session') and immediately frames the tool's role versus a plain recording. It explicitly names sibling tools (writ_browser_act, writ_browser_context, writ_browser_network, writ_browser_save) and distinguishes their roles, so an agent can separate this from neighbors without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'WHEN YOU NEED IT' block gives explicit when-to-use (expose named functions, add descriptions/defaults, add steps the recorder cannot see) and when-not ('not for a plain recording'), plus the alternative pattern for that case. FASTEST PATHS routes to specific siblings with concrete conditions, covering both selection and exclusion.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.