Airbnb

search-area

airbnb.com · Cowork
Download for Coworkthe skill folder, zipped
How to use it in Coworkonce per skill
Download the skill (.zip)

One .zip per skill. Inside: SKILL.md with the intent and the frontmatter, and the code that runs in the page.

Open Skills in Cowork

Settings → Personalize → Skills. This is Cowork's own skill library, shared across every chat.

Add → pick the .zip

Click Add and choose the file you just downloaded. Cowork installs it once; no CLAUDE.md, no per-project setup.

Ask in plain language

The skill stays available in every session. Cowork runs it in your own Chrome, riding the session you are already signed into.

Side effectread
Authnone (no login needed; the public web API key is embedded in the extractor)
Needsnavigate · evaluate
Summary

Search Airbnb stays by MAP BOUNDING BOX (reliable geocoding: free-text location in the URL is unreliable).

Params
{"bbox":"string, the querystring fragment \"ne_lat=..&ne_lng=..&sw_lat=..&sw_lng=..\" (or pass the four as separate params)","ne_lat":"float (alternative to bbox)","ne_lng":"float","sw_lat":"float","sw_lng":"float","label":"string (optional, cosmetic name for the area)","checkin":"YYYY-MM-DD (optional)","checkout":"YYYY-MM-DD (optional)","adults":"int (optional, default 1)","price_min":"int, PER NIGHT USD (optional)","price_max":"int, PER NIGHT USD (optional)","zoom":"int (optional, default 11)","count":"int (optional, default 18, max 50)"}
Returns
{"count":"int","bbox":"the box that was applied","outsideBbox":"int (results whose coordinates fell outside the box, see Assertion)","listings":"array of { id, url, name, title, price (stay TOTAL), originalPrice, priceQualifier, rating, reviews, beds, badges, lat, lng, photo }"}
SKILL.md68 lines

airbnb-search-area

Searches stays inside a bounding box you define. Unlike a place name, the box is unambiguous, the reliable option when the exact region matters. Resolves in a single bridge call: it POSTs Airbnb's internal StaysSearch GraphQL query directly, so no search page is opened and the params actually drive the request.

The extractor lives as text in the site's localStorage (key airbnb-search-area) and is rebuilt on each call with synchronous eval. It is stored once and then persists across navigation and browser restarts, so on every run except the first it is already there.

Default action: just call (step 1). The call itself reports NEEDS_STORE when the extractor is missing or out of date, only then do you store it (step 2) and call again.

Versioning. The stored extractor is gated by VER (must match the version in the frontmatter above). Both snippets carry VER = "1", keep them in sync with the frontmatter. When evaluate.js changes, bump version and VER in both snippets: the next call sees the mismatch, returns NEEDS_STORE, and re-stores automatically. Storage stays at two keys (airbnb-search-area and airbnb-search-area:ver), overwritten each store, versions never accumulate.

1. Call (do this every run, params only, single bridge call)

await (() => {
  const KEY = "airbnb-search-area", VER = "1";
  const s = localStorage.getItem(KEY);
  if (!s || localStorage.getItem(KEY + ":ver") !== VER) return 'NEEDS_STORE';
  const fn = eval('(' + s + ')');
  return fn(document, { bbox: "ne_lat=40.7855&ne_lng=-73.9435&sw_lat=40.7185&sw_lng=-74.0170", label: "Manhattan", checkin: "2026-08-10", checkout: "2026-08-12", adults: 2 });
})()

Returns the stays inside the box. If it returns the string 'NEEDS_STORE', the extractor is missing or stale in this browser → do step 2 once, then run this exact call again.

Run this snippet verbatim. Never add an await before the eval, only the outer await is allowed.

Any airbnb tab works, the extractor calls the API itself and never reads the open page. Note that airbnb.com redirects to a country domain by IP (airbnb.com.ar, airbnb.co.uk, ...); the extractor uses whatever origin the tab is on, which is required for the request to be same-origin. The locale is pinned to en for the copy only, geocoding is unaffected and resolves the same from any country domain.

2. Store (only when step 1 returned NEEDS_STORE; persists until localStorage is cleared)

From any airbnb tab, evaluate the following with the bridge, replacing <FN> with the contents of evaluate.js (the ( async (root, params) => { ... } ) function). Keep VER equal to the call snippet's:

(() => {
  const KEY = "airbnb-search-area", VER = "1";
  const fn = (<FN>);
  localStorage.setItem(KEY, String(fn));
  localStorage.setItem(KEY + ":ver", VER);
  return 'stored';
})()

Returns "stored". The extractor passes through the model only here, once. Then go back to step 1.

Assertion

Every result's coordinates are checked against the requested box and counted in outsideBbox. If all of them fall outside, the box was ignored and the call fails with tool-broken rather than handing back listings from another region. A small non-zero outsideBbox is normal (Airbnb pads the edges); outsideBbox equal to count never returns.

Map-search cards carry no title/subtitle, so name falls back to the listing's own headline, expect name and title to read differently here than in airbnb-search-by-place.

Prices

price is the total for the whole stay, not per night, read priceQualifier (e.g. "for 2 nights") before comparing anything. price_min / price_max, in contrast, filter per night. So a 2-night stay filtered at price_max: 150 correctly returns a $249 total ($124.50/night). Do not "fix" this by widening the filter.

Errors

  • NEEDS_STORE → extractor missing or version-mismatched in this browser; run step 2 once, then retry step 1.
  • missing_params: bbox needs ne_lat, ne_lng, sw_lat, sw_lng → an incomplete box; all four corners are required (a partial box would silently search the wrong region).
  • bad_bbox → the northeast corner is not north and east of the southwest one; the corners are swapped.
  • tool-broken: every result fell outside the requested bbox → the box was ignored by the API; see Assertion.
  • tool-broken: HTTP 4xx / PersistedQueryNotFound → the pinned sha256Hash rotated on an Airbnb deploy. Recapture it from the StaysSearch XHR the search page fires, update HASH in evaluate.js, and bump version/VER.
  • request_failed → no airbnb tab, or the call ran cross-origin (see step 1).

Success assertion

{ "type": "json", "jsonPath": "count" }
search-area: the Airbnb skill in Cowork · browser-memory