| name | ob-routines |
| description | Record, compile, and replay Browser Routines — saved, named browser workflows. (Alias for openbrowser-routines.) Supports subcommands: "list [query]" to list/search routines, "new" to record a new routine, "execute <name>" to replay a saved routine. Use when the user says "list routines", "record a routine", "replay X", "execute X", or "/ob-routines <subcommand>". |
Browser Routines
Browser Routines are named, compiled workflows captured from real Chrome sessions.
The pipeline has four stages: record → compile → name → replay.
Subcommand dispatch
When invoked with arguments, act immediately — do not ask the user what they want:
| Invocation | Action |
|---|
/ob-routines | Show available routines and ask what to do |
/ob-routines list [query] | Run list_routines.py [query] and display results |
/ob-routines new | Ask only for the one-line goal/intention, then start recording immediately (see "Before recording" below) |
/ob-routines execute <name> | Run replay.py <name> immediately |
Your role during compilation
You are a bridge and quality gate, not the compiler. The Compiler Agent does
the reasoning; you ensure it did its job correctly before finalizing.
Bridge duties
- Run
compile.py in a tmux pane (mandatory — see below).
- Watch for
[compiler:question] — relay it to the user, send their answer back.
- Watch for
[compiler:stalled] — show the agent's message, optionally prompt a follow-up.
- At
[compiler:name_prompt] — help the user pick a short slug.
Quality gate (run before every finalize)
After the compiler reports status=review, read the compiled routine markdown
and check both of the following before calling /compile/finalize:
Gate 1 — Intent clarity
Did the compiler understand why the user performed each action, not just what
they clicked? Red flags:
- Steps that say "click X" with no explanation of goal or condition
- A position-based selection from a sorted/filtered list without asking whether
to replay by position or by identity (e.g. "upvote the top 3 posts" — top 3
today vs. the same 3 posts always?)
- A value (date, search query, ticker, ID) that will obviously change between
runs, not parameterized
If any red flag is present and the compiler did NOT ask about it: relay the
ambiguity to the user yourself, get their answer, then send it via
POST /recordings/{id}/compile/answer so the compiler can revise.
Gate 2 — Delivery goal for read-only workflows
A workflow is read-only if it has no form submission, no purchase, no
send/post/create/delete action — the user only navigated, read, filtered, or
inspected. For read-only workflows, ask: does the compiled routine end with a
delivery step (a file_editor write, a terminal command, or an explicit
instruction to report results in chat)?
If the routine is read-only AND has no delivery step, the compiler made an
error. Do not finalize. Instead:
- Tell the user: "This routine reads data but doesn't capture results anywhere.
How do you want results delivered on replay?"
- (a) Summary shown in chat (brief / structured table / full details?)
- (b) Written to a local file (path + format: plain text, Markdown, CSV, JSON?)
- (c) Both
- Get their answer.
- Send it to the compiler via
POST /recordings/{id}/compile/answer — the
compiler will revise the routine to include the delivery step.
- Wait for the next
status=review, then re-run both gates.
Why this matters: A routine that just clicks through pages is useless on
replay — OpenBrowser will navigate and stop with no output. The delivery step
is what makes the routine meaningful.
Preconditions
First time? Complete the full setup in ~/.claude/skills/open-browser/references/setup.md
before using this skill. That guide covers: loading the Chrome extension, connecting
it to the server, and obtaining a valid OPENBROWSER_CHROME_UUID. Without that,
recording and replay will fail immediately.
For subsequent uses, confirm:
- OpenBrowser server at
http://127.0.0.1:8765
- Chrome extension connected
OPENBROWSER_CHROME_UUID set (or passed via --chrome-uuid)
Quick check:
python3 ~/.claude/skills/open-browser/scripts/check_status.py --chrome-uuid "$OPENBROWSER_CHROME_UUID"
Start the server if needed:
cd /Users/yangxiao/git/OpenBrowser && uv run local-chrome-server serve
Scripts path: ~/.claude/skills/ob-routines/scripts/.
List & search routines
python3 ~/.claude/skills/ob-routines/scripts/list_routines.py
python3 ~/.claude/skills/ob-routines/scripts/list_routines.py "login"
python3 ~/.claude/skills/ob-routines/scripts/list_routines.py --recordings
Record a routine
Before recording — DO NOT interrogate the user
The whole point of record → compile is that the browser actions are observed,
and the Compiler Agent asks clarifying questions after it has seen them.
Ask the user only for a short goal/intention (one line). Do NOT ask:
- which site or URL to start from
- which tool/screener to use
- how to define filter terms ("what's high-value?", "what's significant?")
- which parameters should vary between runs
All of that is the compiler's job during Gate 1. Pre-record interrogation
defeats the pipeline and wastes the user's time. If the user's goal is vague
("find good stocks"), that's fine — start recording. The compiler will ask.
Step 1 — start recording
python3 ~/.claude/skills/ob-routines/scripts/start_recording.py \
--chrome-uuid "$OPENBROWSER_CHROME_UUID" \
--name "xiaohongshu-messages" \
--intent "check messages on Xiaohongshu"
Prints [recording:started] <recording_id>. Save this ID.
Tell the user: "Perform your actions in the browser window, then come back and say done."
Do NOT proceed until the user confirms.
Step 2 — stop recording
python3 ~/.claude/skills/ob-routines/scripts/stop_recording.py <recording_id>
Compile to a routine — MANDATORY: tmux interactive session
compile.py uses input() for Q&A and the name prompt. It MUST run in an
interactive shell. Never invoke it directly via the Bash tool — it will block
and then be killed, losing the compiler session.
Pick the compiler model before launching
The Compiler Agent benefits from a large model. Before launching compile.py:
curl -s http://127.0.0.1:8765/api/config | jq '.config'
- If
default_compiler_alias is set → launch compile.py with no
--model-alias flag; the server uses that alias.
- If
default_compiler_alias is null → pick the best alias from
llm_configs (only entries with has_api_key: true) and pass it via
--model-alias <alias>. Prefer *-plus over *-flash, and prefer the
regular dashscope.aliyuncs.com/compatible-mode/v1 endpoint over
coding.dashscope.aliyuncs.com (the coding endpoint has tighter quota
semantics — see project memory).
Always tell the user which model the compiler is using, in one line, before
launching — whether it came from the server default or your pick.
Launch in tmux
Default (compiler alias configured on server, or no override needed):
tmux new-window -n "compile" \
"python3 ~/.claude/skills/ob-routines/scripts/compile.py <recording_id>; echo '[compile-done]'; exec zsh"
With an explicit override (when you picked a model automatically):
tmux new-window -n "compile" \
"python3 ~/.claude/skills/ob-routines/scripts/compile.py <recording_id> --model-alias <alias>; echo '[compile-done]'; exec zsh"
Why exec zsh: Without it, the tmux window closes the instant compile.py
exits — the final [compiler:saved] / [compile-done] markers can land
between monitor polls and be lost, leaving you unable to capture confirmation.
The trailing shell keeps the window alive so the last lines of the pane are
always readable. Close it manually with tmux kill-window -t compile after
confirming the routine saved.
Monitor output
tmux capture-pane -t "compile" -p
For long runs, stream markers via the Monitor tool instead of polling by hand.
The script MUST detect pane-gone and emit a terminal event — otherwise, if
the window exits between polls, the monitor silently runs forever on a dead
pane and you never learn the routine finished. Template:
rm -f /tmp/ob_compile_seen
while true; do
if ! tmux list-windows -F '#{window_name}' | grep -qx compile; then
echo "[tmux:pane-gone] compile window exited"
break
fi
cur=$(tmux capture-pane -t compile -p -S -400 2>/dev/null)
echo "$cur" | grep -E --line-buffered \
"\[compiler:question\]|\[compiler:stalled\]|\[compiler:gate_check\]|\[compiler:name_prompt\]|\[compiler:saved\]|\[compile-done\]|\[compiler:error\]|Traceback" \
| while read -r line; do
grep -qxF "$line" /tmp/ob_compile_seen 2>/dev/null && continue
echo "$line" >> /tmp/ob_compile_seen
echo "$line"
done
sleep 3
done
Send an answer
tmux send-keys -t "compile" "the answer" Enter
Markers to watch for
| Marker | Your action |
|---|
[compiler:thought] / [compiler:action] | Relay as progress to user |
[compiler:question] <text> | Relay to user, wait for answer, send via tmux send-keys |
[compiler:stalled] <text> | Show message, ask user for follow-up |
[compiler:complete] goal=… steps=N | Compilation reached review state |
[compiler:routine_draft] | Full routine markdown printed for inspection |
[compiler:gate_check] | Run both quality gates here. Send feedback or press Enter |
[compiler:name_prompt] | Gates passed — help user pick slug |
[compiler:saved] | Run list_routines.py <name> to verify, then report name + id to user |
Quality gate checkpoint
When [compiler:gate_check] appears in the pane, compile.py is explicitly
paused waiting for your review of [compiler:routine_draft]. The gate
check is Claude's judgment, not the compiler's — the compiler's own "Let me
know if I misread your intent" message in [compiler:message:assistant] is
not a substitute. You must:
- Read the
[compiler:routine_draft] markdown in the pane capture.
- Write out Gate 1 and Gate 2 reasoning as user-visible text before
sending any input — one or two lines per gate, stating what passed or
failed and why. This makes the judgment inspectable by the user instead
of hidden inside an empty Enter.
- Then either pass or fail:
- Gates pass → send an empty Enter:
tmux send-keys -t compile "" Enter
- Gate fails → send corrective feedback:
tmux send-keys -t compile "Please add a delivery step: summarise results in chat as a structured list of tickers with metrics." Enter
compile.py forwards non-empty input back to the compiler, streams the revision,
and loops back to another [compiler:gate_check]. Only an empty Enter advances
to [compiler:name_prompt].
Never send gate feedback at the [compiler:name_prompt] stage — that input
goes directly to the routine name field, not the compiler.
Replay a routine
python3 ~/.claude/skills/ob-routines/scripts/replay.py "routine-name" \
--chrome-uuid "$OPENBROWSER_CHROME_UUID"
python3 ~/.claude/skills/ob-routines/scripts/replay.py --list
Name matching: exact → ID → prefix → substring.
Full example workflow
1. /ob-routines new → ask user what to record
2. start_recording → [recording:started] abc123
3. (user records in browser, says "done")
4. stop_recording abc123 → [recording:events] 21 events
5. tmux new-window "compile.py abc123"
6. monitor pane → relay questions → send answers
7. [compiler:complete] → run Gate 1 + Gate 2
Gate 2 fails: routine is read-only, no delivery step
→ ask user: chat summary, file, or both?
→ send answer via tmux send-keys
→ wait for next [compiler:complete]
8. Gates pass → [compiler:name_prompt] → user picks slug
9. [compiler:saved] name='…' id=…
10. list_routines.py <name> to verify, then report saved + verified to user
11. /ob-routines execute <name> → streams [action] … [complete]
Failure handling
- Server unreachable:
uv run local-chrome-server serve
- Browser UUID invalid: reconnect Chrome extension, get fresh UUID
- 0 events captured: browser disconnected; re-record
- tmux not found:
brew install tmux
- tmux window conflict: check
tmux list-windows, use a unique -n name
- Compiler session expired (pane exited before finalize): call
POST /recordings/{id}/compile again to restart — session is fresh
- Relay stuck:
[observation:error] lines in SSE stream; relay to user