Skip to content
Kelly Ferrone edited this page Sep 12, 2026 · 3 revisions

Flows

A flow is a list of tool calls saved under a name. run_flow replays the whole list server-side, in order, in the browser you already have.

The saving is not bandwidth. It is decisions: a twelve-step form becomes one call instead of twelve, and it replays a sequence somebody already got right instead of re-deriving the selectors every time.

Flows are off unless the server has somewhere to put them. Set FLOW_DATA_DIR — see Deployment.

A step is a tool call

name: sign-up
description: Create an account on the demo site
steps:
- tool: navigate
  args:
    url: https://demo.example.com/join
- tool: write
  id: email
  args:
    css: "#email"
    text: a@example.com
- tool: interact
  args:
    action: click
    css: button[type=submit]
- tool: extract
  args:
    css: h1
  return: true

args is exactly the arguments you would pass that tool directly. Nothing is renamed and nothing is added. A step may also carry:

Key Does
id a name for the step, shown in the report
note a comment for the next reader
onError abort (default) or continue past a failure
return include this step's full result in the report

Not steps: open_session and end_browser. A flow runs in the browser the caller already holds, which is what lets the same flow run on Firefox unedited. Never put session_id in a step either — the run supplies it.

flow_schema lists every tool that may be a step and what each takes. It is derived from the live tools, so it cannot describe a step that would not run. save_flow validates against the same schemas and reports every problem at once, so a broken flow is refused when you save it rather than halfway through a form.

Parameters: what varies between runs

Declare them as JSON Schema and write ${name} where the value goes:

parameters:
  type: object
  required: [site, email]
  properties:
    site: {type: string}
    email: {type: string}
steps:
- tool: navigate
  args:
    url: ${site}/join
- tool: write
  args:
    css: "#email"
    text: ${email}

Then run_flow(name="sign-up", params={"site": "https://demo.example.com", "email": "a@example.com"}).

A parameter is text, and ${name} may sit anywhere in any argument of any step — including in the middle of a longer string, as ${site}/join is.

Three rules worth knowing:

  • Single pass. A value you supply is never re-scanned, so passing "${admin}" as a parameter types that text and resolves nothing.
  • $${ is a literal ${, for a page that genuinely wants one.
  • A name you did not declare is refused when the flow is saved, not at step nine with a form half filled. At run time an unsupplied reference is left exactly as written rather than blanked, so a script holding a JavaScript template literal survives.

A parameter is never secret. Everything you pass may appear in the report — a step's summary shows a parameter's length, text=18 chars, where a secret shows text=<hidden>. For a password, do not use a parameter at all.

Typing a secret you are never shown

For a password, name a secret. write takes a secret argument instead of text, and the server reads it and types it:

- tool: write
  args:
    css: "#password"
    secret:
      name: the-internet
      key: password

The value never passes through the caller, and the step's line in the report reads text=<hidden>.

A secret is structural and is never written into a string. It is the exact opposite of a parameter in every respect: the caller never sees it, it reaches exactly one sink, and it is checked against the page about to receive it. Only write has a secret argument — so there is nowhere to put one on any other action — and a secret's name may not come from a parameter either, because choosing which credential gets typed is not a decision a caller may make. See Secrets.

Whose flows you see

This depends on your session name, and it is not guessable from any schema.

You are You save into You can run
named (?session= / X-Session-Key) your own library yours, plus the shared library
stdio its own library its own, plus the shared library
unnamed, or named global nowhere — you cannot save the shared library

global is read-only. Every session lists and runs what is in it and no session may change it: save_flow and delete_flow refuse. That is not tidiness — the shared library is live, so a flow one caller rewrote or deleted would change or vanish underneath another part-way through running it. Only an operator moves a flow into global, from the admin UI.

So name your session before you save anything. Without a name you have no library of your own, and save_flow says so rather than writing somewhere you would never look again.

Where a name exists in both, yours wins, and list_flows marks each entry shared: true or false so you can tell which one will run.

Running one

Open a browser first — a flow never opens one. To run the same flow on Firefox, open_session(browser="firefox") and run it again, unchanged.

The report has one line per step plus where the browser ended up. It stops at the first failing step unless that step says onError: continue, and says which step stopped it, the error, and the page it was on — so check that page with extract before deciding the selector is wrong.

A run answers with the steps that said they were the answer. Mark each one return: true — any number may, and nothing else in the report carries a result. A flow whose point is its closing extract needs the flag on that extract; without it the step is reported as having run and its result is discarded.

Running a flow you cannot edit — one from the shared library — pass verbose and read every step instead.

url_redacted: true means the page it ended on carried a guarded value, so the URL shown is scrubbed and is not a real address. Do not navigate back to it.


Actions · Files · Secrets · Sessions

Clone this wiki locally