Tabductor
Explore/ Zillow townhouse finder
Public workflow

Zillow townhouse finder

By Fullstacktics · used 1 time · updated 2026-10-18

How it runs

Distilled · v2 Includes a distilled script, so your copy runs it from its first run. If a page has changed, it falls back to the AI agent for that run.

Instructions

Open zillow.com
Find 3 bedroom townhouses with fireplace in Seattle, WA
Review the distilled script

This Python runs in your browser session when you use the workflow. Review it before running.

import hashlibimport jsonimport refrom urllib.parse import parse_qs, urlparse from playwright.sync_api import expect  def run(page, context, browser):    def visible(scope, locator):        try:            expect(locator).to_be_visible(timeout=10000)        except AssertionError:            scope.deopt(reason="Required control is not visible.", evidence={"locator": str(locator)})     def click(scope, locator):        visible(scope, locator)        locator.click()     def apply(scope):        click(scope, page.get_by_role("dialog").get_by_role("button", name="Apply", exact=True))        page.get_by_role("dialog").wait_for(state="hidden", timeout=10000)     def validate_query(scope, response):        if not isinstance(response, dict) or response.get("truncated") is not False:            scope.deopt(reason="Store query is unusable or truncated.", evidence=response)        rows = response.get("rows")        if not isinstance(rows, list):            scope.deopt(reason="Store query did not return rows.", evidence=response)        for row in rows:            if not isinstance(row, dict) or not {                "search_id", "criteria", "result_count", "url"            }.issubset(row):                scope.deopt(reason="Stored summary lacks required columns.", evidence=response)        return rows     with browser.scope(        "open-zillow-search",        goal="Open Zillow, submit Seattle, WA, and verify Seattle search results.",        returns={"type": "object", "properties": {"listing_type": {"type": "string"}},                 "required": ["listing_type"]},    ) as opening:        page.goto("https://www.zillow.com/", wait_until="domcontentloaded")        page.wait_for_load_state("domcontentloaded")        search = page.get_by_placeholder("Enter an address, neighborhood, city, or ZIP code")        visible(opening, search)        expect(search).to_be_editable(timeout=10000)        search.fill("Seattle, WA")        click(opening, page.get_by_role("button", name="Submit Search"))        page.wait_for_load_state("domcontentloaded")        onboarding = page.get_by_role("button", name="For sale", exact=True)        beds = page.get_by_role("button", name="Beds & baths", exact=True).or_(            page.get_by_role("button", name="3 bd, 0+ ba", exact=True)        )        known_state = onboarding.or_(beds).or_(search)        try:            expect(known_state).not_to_have_count(0, timeout=10000)        except AssertionError:            opening.deopt(reason="No supported search state appeared.",                         evidence={"url": page.url})        text = page.locator("body").inner_text()        url = page.url        if not beds.is_visible() and urlparse(url).path in {"", "/"} and not onboarding.is_visible():            if not search.is_visible() or re.search(r"Seattle\s+WA", text):                opening.deopt(reason="The search submission state is ambiguous.",                             evidence={"url": url, "text": text[:4000]})            visible(opening, search)            expect(search).to_be_editable(timeout=10000)            search.fill("Seattle, WA")            click(opening, page.get_by_role("button", name="Submit Search"))            page.wait_for_load_state("domcontentloaded")        try:            expect(onboarding.or_(beds)).not_to_have_count(0, timeout=10000)        except AssertionError:            opening.deopt(reason="Search did not reach onboarding or results.",                         evidence={"url": page.url})        if not beds.is_visible():            click(opening, onboarding)            page.wait_for_load_state("domcontentloaded")        visible(opening, beds)        heading = page.get_by_role("heading", name=re.compile(r"^Seattle WA\b"))        visible(opening, heading)        text = page.locator("body").inner_text()        url = page.url        if not re.search(r"Seattle\s+WA", text) or urlparse(url).hostname not in {            "www.zillow.com", "zillow.com"        }:            opening.deopt(reason="Destination is not verified as Seattle Zillow results.",                         evidence={"url": url, "text": text[:4000]})        sale = page.get_by_role("button", name="For sale", exact=True)        visible(opening, sale)        listing_type = sale.inner_text().strip()        opening.value = {"listing_type": listing_type}     with browser.scope(        "inspect-bedroom-controls",        goal="Choose exactly three bedrooms using Beds Select and exact match.",        returns=None,    ) as bedrooms:        bedroom_control = page.get_by_role("button", name="Beds & baths", exact=True).or_(            page.get_by_role("button", name="3 bd, 0+ ba", exact=True)        )        click(bedrooms, bedroom_control)        dialog = page.get_by_role("dialog")        visible(bedrooms, dialog)        group = dialog.get_by_role("group", name="Beds Select", exact=True)        selected = group.locator('[aria-pressed="true"]')        visible(bedrooms, selected)        selected_value = selected.inner_text().strip()        exact = dialog.get_by_label("Use exact match")        visible(bedrooms, exact)        exact_checked = exact.is_checked()        if selected_value not in {"3", "3+"}:            choice = group.get_by_role("button", name="3+", exact=True).or_(                group.get_by_role("button", name="3", exact=True)            )            click(bedrooms, choice)        if not exact_checked:            exact.check()        try:            expect(selected).to_have_text("3", timeout=10000)            expect(exact).to_be_checked(timeout=10000)        except AssertionError:            bedrooms.deopt(reason="Bedroom controls do not specify exactly three bedrooms.",                           evidence={"selected": selected.inner_text(),                                     "exact_match": exact.is_checked()})        apply(bedrooms)        visible(bedrooms, page.get_by_role("button", name="3 bd, 0+ ba", exact=True))        bedrooms.value = None     with browser.scope(        "select-townhomes",        goal="Deselect all property types, select only Townhomes, and apply.",        returns=None,    ) as types:        property_control = page.get_by_role("button", name="Property type", exact=True).or_(            page.get_by_role("button", name="Property type (1)", exact=True)        )        click(types, property_control)        dialog = page.get_by_role("dialog")        visible(types, dialog)        townhouse = dialog.get_by_label("Townhomes", exact=True)        visible(types, townhouse)        townhouse_id = townhouse.get_attribute("id")        controls = dialog.locator("input").evaluate_all(            '(els)=>els.map(e=>({type:e.type,checked:e.checked,id:e.id,aria:e.getAttribute("aria-label")}))'        )        if not townhouse_id or not controls or not any(            control.get("id") == townhouse_id and control.get("type") == "checkbox"            for control in controls        ):            types.deopt(reason="Property controls do not identify the Townhomes checkbox.",                        evidence={"controls": controls, "townhouse_id": townhouse_id})        checked = [control for control in controls if control.get("checked")]        if any(control.get("id") != townhouse_id for control in checked):            click(types, dialog.get_by_role("button", name="Deselect All", exact=True))            expect(townhouse).not_to_be_checked(timeout=10000)            townhouse.check()        elif not checked:            townhouse.check()        try:            expect(townhouse).to_be_checked(timeout=10000)            expect(dialog.locator("input:checked")).to_have_count(1, timeout=10000)        except AssertionError:            types.deopt(reason="Townhomes is not the only checked property type.",                        evidence={"checked_count": dialog.locator("input:checked").count(),                                  "townhomes_checked": townhouse.is_checked()})        apply(types)        visible(types, page.get_by_role("button", name="Property type (1)", exact=True))        types.value = None     with browser.scope(        "apply-fireplace-keyword",        goal="Apply the fireplace keyword to the Seattle townhouse results.",        returns=None,    ) as keyword_step:        click(keyword_step, page.get_by_role("button", name="Filters", exact=True))        dialog = page.get_by_role("dialog")        click(keyword_step, dialog.get_by_role("button", name="Show more", exact=True))        keywords = dialog.get_by_label("Keywords", exact=True)        visible(keyword_step, keywords)        expect(keywords).to_be_editable(timeout=10000)        keywords.fill("fireplace")        apply(keyword_step)        visible(keyword_step, page.get_by_role("button", name="3 bd, 0+ ba", exact=True))        keyword_step.value = None     with browser.scope(        "verify-applied-filters",        goal="Read back exact bedrooms, Townhomes only, and fireplace; leave results visible.",        returns={            "type": "object",            "properties": {                "criteria": {"type": "object"},                "url": {"type": "string"},                "result_count": {"type": "integer"},                "heading": {"type": "string"},                "examples": {"type": "array"},                "page_text": {"type": "string"},                "property_controls": {"type": "array"},            },            "required": ["criteria", "url", "result_count", "heading", "examples",                         "page_text", "property_controls"],        },    ) as verification:        click(verification, page.get_by_role("button", name="3 bd, 0+ ba", exact=True))        dialog = page.get_by_role("dialog")        selected = dialog.get_by_role("group", name="Beds Select", exact=True).locator(            '[aria-pressed="true"]'        )        visible(verification, selected)        expect(selected).to_have_text("3", timeout=10000)        selected_bedroom = selected.inner_text().strip()        exact = dialog.get_by_label("Use exact match")        visible(verification, exact)        expect(exact).to_be_checked(timeout=10000)        exact_checked = exact.is_checked()        if selected_bedroom != "3" or not exact_checked:            verification.deopt(                reason="Bedroom controls do not specify exactly three bedrooms.",                evidence={"selected": selected_bedroom, "exact_match": exact_checked},            )        apply(verification)         click(verification, page.get_by_role("button", name="Property type (1)", exact=True))        dialog = page.get_by_role("dialog")        townhouse = dialog.get_by_label("Townhomes", exact=True)        visible(verification, townhouse)        expect(townhouse).to_be_checked(timeout=10000)        townhouse_id = townhouse.get_attribute("id")        controls = dialog.locator("input").evaluate_all(            "(els)=>els.map(e=>({id:e.id,checked:e.checked}))"        )        checked = [control for control in controls if control.get("checked")]        if not townhouse_id or len(checked) != 1 or checked[0].get("id") != townhouse_id:            verification.deopt(                reason="Townhomes is not the only selected property type.",                evidence={"controls": controls, "townhouse_id": townhouse_id},            )        property_type = "Townhomes"        apply(verification)         click(verification, page.get_by_role("button", name="Filters", exact=False))        dialog = page.get_by_role("dialog")        keywords = dialog.get_by_label("Keywords", exact=True)        visible(verification, keywords)        keywords.scroll_into_view_if_needed()        expect(keywords).to_have_value("fireplace", timeout=10000)        keyword_value = keywords.input_value()        if keyword_value != "fireplace":            verification.deopt(reason="Fireplace keyword was not retained.",                               evidence={"keywords": keyword_value})        apply(verification)         visible(verification, page.get_by_role("button", name="3 bd, 0+ ba", exact=True))        results_count = page.get_by_text(re.compile(r"^[\d,]+\s+results?$"))        visible(verification, results_count)        text = page.locator("body").inner_text()        url = page.url        count_match = re.search(r"(?m)^\s*([\d,]+)\s+results?\s*$", text)        heading_match = re.search(r"(?m)^Seattle WA[^\n]*", text)        if not count_match or not heading_match:            verification.deopt(reason="Cannot extract the live Seattle result heading and count.",                               evidence={"url": url, "text": text[:6000]})        state_values = parse_qs(urlparse(url).query).get("searchQueryState")        if not state_values:            verification.deopt(reason="Filtered URL has no searchQueryState.", evidence={"url": url})        state = json.loads(state_values[0])        filters = state.get("filterState", {})        if filters.get("beds", {}).get("min") != int(selected_bedroom) or \                filters.get("beds", {}).get("max") != int(selected_bedroom) or \                filters.get("att", {}).get("value") != keyword_value:            verification.deopt(reason="URL filters disagree with verified controls.",                               evidence={"filterState": filters})        examples = []        listing_pattern = re.compile(            r"(?m)^(\$[\d,]+[^\n]*)\n"            r"(\d+)\s*bds?(\d+(?:\.\d+)?)\s*ba([^\n]*)\n"            r"([^\n]*Seattle,\s*WA[^\n]*)"        )        for match in listing_pattern.finditer(text):            examples.append({                "price": match.group(1).strip(),                "beds": int(match.group(2)),                "baths": float(match.group(3)),                "details": match.group(4).strip(),                "address": match.group(5).strip(),            })        verification.value = {            "criteria": {                "location": "Seattle, WA",                "bedrooms": int(selected_bedroom),                "exact_match": exact_checked,                "property_type": property_type,                "keywords": keyword_value,                "listing_type": opening.value["listing_type"],            },            "url": url,            "result_count": int(count_match.group(1).replace(",", "")),            "heading": heading_match.group(0).strip(),            "examples": examples,            "page_text": text,            "property_controls": controls,        }     with browser.scope(        "save-workflow-summary",        goal="Save and query the verified live search summary in the workflow store.",        returns={            "type": "object",            "properties": {"collection": {"type": "string"}, "row": {"type": "object"}},            "required": ["collection", "row"],        },    ) as saving:        tables = browser.store.tables()        if not isinstance(tables, dict) or not isinstance(tables.get("tables"), dict):            saving.deopt(reason="Store table inventory has an unusable shape.", evidence=tables)        table = "zillow_search_workflow"        columns = {            "search_id": {"type": "text"},            "criteria": {"type": "jsonb"},            "result_count": {"type": "int"},            "url": {"type": "text"},        }        if table in tables["tables"]:            existing_definition = tables["tables"][table]            if not isinstance(existing_definition, dict):                saving.deopt(reason="Existing workflow table metadata is unusable.",                             evidence=existing_definition)        browser.store.define_table(table=table, columns=columns, primaryKey=["search_id"])        criteria = verification.value["criteria"]        identity_data = json.dumps(criteria, sort_keys=True, separators=(",", ":"))        record_key = "search_" + hashlib.sha256(identity_data.encode()).hexdigest()        row = {            "search_id": record_key,            "criteria": criteria,            "result_count": verification.value["result_count"],            "url": verification.value["url"],        }        sql = "select * from zillow_search_workflow where search_id='" + record_key + "'"        before = browser.store.query(sql=sql)        prior = validate_query(saving, before)        if len(prior) > 1 or any(item["search_id"] != record_key for item in prior):            saving.deopt(reason="Stored search identity is ambiguous.", evidence=before)        if not prior or prior[0] != row:            key = hashlib.sha256(                json.dumps(row, sort_keys=True, separators=(",", ":")).encode()            ).hexdigest()            browser.store.upsert(table=table, row=row, idempotencyKey=record_key + ":" + key)        after = browser.store.query(sql=sql)        saved = validate_query(saving, after)        if len(saved) != 1 or saved[0] != row:            saving.deopt(reason="Saved workflow summary did not verify.", evidence=after)        saving.value = {"collection": table, "row": saved[0]}     with browser.scope(        "report-search-result",        goal="Report the saved summary and fresh keyword-match findings.",        returns=None,    ) as reporting:        saved = saving.value["row"]        reason = (            f"Verified applied controls and {saved['result_count']} live results; "            f"saved search summary and confirmed URL {saved['url']} by store query."        )        browser.record.outcome(            collection=saving.value["collection"],            recordKey=saved["search_id"],            status="saved",            reason=reason[:1000],        )        browser.done(result={            "criteria": saved["criteria"],            "result_count": saved["result_count"],            "url": saved["url"],            "heading": verification.value["heading"],            "examples": verification.value["examples"],            "evidence": {                "property_controls": verification.value["property_controls"],                "results_text": verification.value["page_text"],            },            "fireplace_verification": (                "Matches use Zillow's fireplace Keywords filter; individual fireplace "                "amenities were not independently inspected."            ),            "destination_save": {                "collection": saving.value["collection"],                "recordKey": saved["search_id"],                "record": saved,            },            "examples_note": "Examples are extracted findings, not individual property saves.",        })        reporting.value = None