Cookbook

Worked examples.

Copy-paste recipes for the patterns Threads actually use. Grouped by what you're trying to accomplish.

1. Post a tweet on X via XHR replay (bypass Draft.js)

X composer is Draft.js — synthetic clipboard events don't reach EditorState. The proven path is replaying the GraphQL mutation that posting fires.

step-by-step
# 1. Install persistent XHR interceptor — survives navigations
inject_on_new_document(session="slot-1", tab="x-tab", script="""
(() => {
  const oO=XMLHttpRequest.prototype.open, oS=XMLHttpRequest.prototype.send, oH=XMLHttpRequest.prototype.setRequestHeader;
  XMLHttpRequest.prototype.open=function(m,u,...r){this.__url=u;this.__method=m;this.__headers={};return oO.apply(this,[m,u,...r])};
  XMLHttpRequest.prototype.setRequestHeader=function(k,v){if(this.__headers)this.__headers[k]=v;return oH.apply(this,[k,v])};
  XMLHttpRequest.prototype.send=function(b){
    if(this.__url?.includes('CreateTweet')){
      window.__xhrCaptures = window.__xhrCaptures || [];
      window.__xhrCaptures.push({url:this.__url, method:this.__method, headers:this.__headers, body:b});
    }
    return oS.apply(this,[b]);
  };
})();
""")

# 2. User posts ONE seed tweet manually (humans, not bots — captures auth/csrf/transaction-id)

# 3. Replay with new text
eval_js(code="""
(async (newText) => {
  const c = window.__xhrCaptures.at(-1);
  const body = JSON.parse(c.body);
  body.variables.tweet_text = newText;
  const r = await fetch(c.url, {method:'POST', credentials:'include', headers:c.headers, body: JSON.stringify(body)});
  const data = await r.json();
  return {ok: !!data?.data?.create_tweet?.tweet_results?.result, status: r.status};
})("Hello from WebLoom")
""")

2. Reply to a tweet via the UI (draftjs_set_text)

If you don't want to fight XHR signing, drive the reply composer directly:

navigate(url="https://x.com/yourhandle/status/<tweet_id>")
# click the reply button (data-testid stable)
click(selector='[data-testid="reply"]')
# fill the reply composer — defaults to 80ms/char which is safe for most builds
draftjs_set_text(container_selector='[data-testid="tweetTextarea_0"]', text="Your reply")
# submit
click(selector='[data-testid="tweetButtonInline"]')

3. Submit a Reddit comment

One-call helper that handles Lexical composer mounting + Reddit's churned selectors:

reddit_submit_comment(
    post_url="https://www.reddit.com/r/sideproject/comments/abc123/...",
    markdown="My honest take is...",
    verify_landed=true,
)

4. Upload a book cover + manuscript to KDP

KDP uses AjaxInput — programmatic input.files clears on assignment. Use xhr_upload after capturing the real endpoint.

# 1. Capture a real upload
capture_network_start(session="slot-1", tab="kdp")
# user uploads one cover manually
capture_network_stop(full=true)  # returns URL + form fields

# 2. Replay programmatically for subsequent books
xhr_upload(
    url="<captured-url>",
    files=[{"path": "/abs/path/cover.jpg", "field": "cover_image"}],
    fields={"book_id": "GD5FHHAM7P9", "_csrf": "..."}
)

5. Crack a canvas-rendered widget (vision fallback)

Some sites render their critical UI to canvas (Figma comments, drawing tools, custom video editors). DOM strategies see nothing. Vision_check is the unlock.

v = vision_check(question="click coords for the publish button", session="slot-1", tab="t1")
# returns {ok: true, answer: "...", click: {x: 1240, y: 64}}
if v["click"]:
    click_at_coords(x=v["click"]["x"], y=v["click"]["y"])
else:
    pause_for_human(reason="Publish button not visible — please confirm UI state")

6. Heal a broken selector (drift)

When pre-flight reports a selector missing, ask the engine for replacement candidates instead of failing.

hints = drift_heal_suggest(
    old_selector='[data-testid="oldid"]',
    descriptor="post tweet button"
)
# hints = {ok: true, candidates: [{selector: "...", score: 7, reason: "data-testid present"}, ...]}
# Use the top candidate, log to playbook, push patched Thread

7. Cross-post: same content to 5 platforms in parallel

run_parallel(
    max_concurrency=3,
    calls=[
        {"tool": "navigate", "args": {"session": "slot-1", "tab": "x", "url": "https://x.com/compose/post"}},
        {"tool": "navigate", "args": {"session": "slot-1", "tab": "li", "url": "https://www.linkedin.com/feed/?showShareBox=true"}},
        {"tool": "navigate", "args": {"session": "slot-1", "tab": "bs", "url": "https://bsky.app/"}},
        {"tool": "navigate", "args": {"session": "slot-1", "tab": "ih", "url": "https://www.indiehackers.com/"}},
        {"tool": "navigate", "args": {"session": "slot-1", "tab": "th", "url": "https://www.threads.net/"}},
    ],
)
# Then fill each composer in turn (each platform has its own quirks)

8. Defeat a Cloudflare challenge with stealth + captcha

# 1. Apply stealth patches BEFORE the page loads its challenge
enable_stealth(session="slot-1", tab="cf-target")
navigate(url="https://target.example.com/")

# 2. If a Turnstile or hCaptcha still shows up, solve it
# Pull the site_key from the page
key = eval_js(code="document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')")
result = solve_captcha(type="turnstile", site_key=key)
# result = {ok: true, token: "<the token>"}
# Submit the token via the page's expected flow (varies; usually a hidden input + submit)

9. Schedule a tweet for later (no native scheduling)

X requires Premium for native scheduling. Workaround: capture a CreateTweet body, store it, run a cron that replays at the target time.

# At authoring time, capture once + store
captured = eval_js(code="JSON.stringify(window.__xhrCaptures.at(-1))")
# save 'captured' + target_time to your DB

# Later (via cron job, scheduled function, etc):
# Reload the body, substitute tweet_text, replay
replay_xhr(url=captured.url, method="POST", headers=captured.headers,
           body={"variables": {"tweet_text": "Scheduled post text"}, ...})

10. Detect drift and ship a patch

Pre-flight runs before every recipe. When a check fails:

# 1. Pre-flight ran, selector X is gone. Engine surfaces drift.
# 2. Open the site, drift-heal:
candidates = drift_heal_suggest(old_selector="...", descriptor="...")

# 3. Confirm the top candidate works:
click(selector=candidates["candidates"][0]["selector"])

# 4. Update the Thread JSON's proven_actions to the new selector
# 5. weaver publish thread → buyers' copies auto-update

What's missing from this cookbook?

Open a Thread for any site whose pattern isn't covered here. The author share is 75%. The patterns above are the building blocks — the Threads are the specific recipes.