Browser Script Builder
Create self-contained JS scripts that run inside platform browser agents (linkedin-agent, reddit-agent, twitter-agent) via browser_run_code. Replaces 5-10 Claude browser tool calls with 2 calls (set params + run script), cutting token usage 10-20x.
Usage
/browser-script <platform> <action-description>
/browser-script linkedin "edit a comment to append link text"
/browser-script reddit "post a reply to a comment"
/browser-script linkedin "extract post data from search results"
Workflow
Step 1: Understand the browser flow
Before writing code, manually walk through the flow in the target browser agent to discover:
- What elements exist (use
browser_snapshot) - What role/name locators work (use
getByRole) - What API calls the page makes (use
browser_network_requests) - What timing/scrolling is needed
Step 2: Write the script
Create scripts/<action>_<platform>.js following this skeleton:
// action_platform.js — One-line description
// Runs inside {platform}-agent browser via browser_run_code.
//
// Params via window.__params:
// { field1: "...", field2: "..." }
//
// Returns: { ok: true, ...data } or { ok: false, error: "error_code" }
async (page) => {
const params = await page.evaluate(() => window.__params);
if (!params || !params.requiredField) {
return JSON.stringify({ ok: false, error: 'missing_params' });
}
try {
// Step 1: Navigate
await page.goto(params.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000);
// Step 2: Scroll to load content
await page.evaluate(() => window.scrollBy(0, 1500));
await page.waitForTimeout(2000);
// Step 3: Find element (with retry after scroll)
const element = page.getByRole('button', { name: /pattern/i }).first();
try {
await element.waitFor({ timeout: 10000 });
} catch {
await page.evaluate(() => window.scrollBy(0, 2000));
await page.waitForTimeout(2000);
try {
await element.waitFor({ timeout: 5000 });
} catch {
return JSON.stringify({ ok: false, error: 'element_not_found' });
}
}
// Step 4: Interact
await element.click();
await page.waitForTimeout(1000);
// Step 5: Verify
return JSON.stringify({ ok: true, result: 'done' });
} catch (e) {
return JSON.stringify({ ok: false, error: e.message });
}
}
Step 3: Test interactively
Test in two calls:
// Set params
mcp__{platform}-agent__browser_run_code code:
async (page) => {
await page.evaluate(() => {
window.__params = { url: "...", text: "..." };
});
}
// Run script
mcp__{platform}-agent__browser_run_code filename:
~/social-autoposter/scripts/the_script.js
If it fails, take a browser_snapshot to debug, fix the script, re-test.
Step 4: Update the shell script
Replace the Claude prompt instructions for this flow with the 2-call pattern. Example from engage.sh Phase D LinkedIn:
# Old: 8-line prompt telling Claude to navigate, find comment, click menu, edit, save
# New:
8. For LinkedIn: use the edit script via linkedin-agent browser:
a. Set params: browser_run_code with code setting window.__params
b. Run: browser_run_code with filename=$REPO_DIR/scripts/edit_linkedin_comment.js
c. Parse JSON result: {ok:true} = success, {ok:false, error} = handle error
Design Rules (MUST follow)
Params via
window.__params— never hardcode. Script must be reusable across different targets.Return JSON always —
{ok: true, ...data}or{ok: false, error: "code"}. Error codes are specific and actionable:comment_not_found,link_already_present,not_logged_in,save_failed,element_not_found.Script owns all waits —
waitForTimeout,waitFor, scroll-retry loops. Caller just runs and reads result.keyboard.type()notfill()— React-based sites (LinkedIn, possibly Twitter) don't detectfill()changes. Alwayskeyboard.type()for rich text editors. For full text replacement:click({clickCount: 3})+Meta+a+keyboard.type(newText).Scroll before find — social platforms lazy-load. Always scroll into view before locating elements.
Locate by role/name —
page.getByRole('button', {name: /pattern/i})not CSS class selectors. Survives redesigns.One script = one complete action — "edit a comment", "scan notifications", "post a reply". Not a single click. Not an entire engagement loop.
Safety checks inside the script — dedup detection (link already present, already replied). Don't rely on the caller.
Generous timeouts — social sites are slow. Use 3s after navigation, 2s after scroll, 1s after click.
Two-phase element finding — first try with 10s timeout, then scroll more and retry with 5s. Return specific error if still not found.
When NOT to Script
- Content generation — Claude needs to decide what to write. Keep that in prompts.
- Project/topic matching — Claude picks which project fits. Keep in prompts.
- One-off investigations — manual browser_snapshot is fine.
- Flows that vary — if the steps change based on page state in unpredictable ways.
The split: Claude decides WHAT (pick project, write text, choose targets). Scripts execute HOW (navigate, click, type, save).
Platform Notes
- Rich text editor uses React —
fill()won't trigger change detection - Edit textbox is the LAST
textbox[name="Text editor for creating comment"]after clicking Edit - Our comment menu:
button[name=/View more options for Matthew/i] - Official API cannot read or edit comments
- Internal Voyager API accessible via in-page
fetch()for read-only operations (notifications) - Agent:
mcp__linkedin-agent__browser_run_code
- old.reddit.com is simpler to automate than new Reddit
- Edit button visible on own comments without menu
- Agent:
mcp__reddit-agent__browser_run_code
Twitter/X
- Tweets cannot be edited
- Agent:
mcp__twitter-agent__browser_run_code
Existing Scripts
| Script | Platform | Action | Params |
|---|---|---|---|
scan_linkedin_notifications.js |
Read notifications via internal API | none (uses cookies) | |
edit_linkedin_comment.js |
Append text to our comment | {postUrl, appendText} |
Reference
Full design doc: scripts/BROWSER_SCRIPTS.md