-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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: trueargs 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.
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.
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: passwordThe 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.
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.
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.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out