Navigate to a Gilbert Group Property and Inspect Demographics

Site gilbertgrouprealestate.comTask navigate-to-property-pageVersion v3Updated Aug 4, 2026Category real-estate

Open a Gilbert Group Real Estate property listing through its propertyId, enter the embedded Buildout page, and extract demographic metrics plus available broker contact details. This skill was captured from a live agent session on gilbertgrouprealestate.com and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Navigate directly to a Gilbert Group Real Estate property page from a known property identifier, open the external Buildout listing page, verify it loaded, and extract the property's available demographics and broker contact information.

When to Use

Use when the caller provides a Gilbert Group property identifier, such as {property-id}, and needs the corresponding listing, demographic metrics, or contact details inspected.

Workflow

  1. Build the direct listing URL: https://gilbertgrouprealestate.com/property-listings/?propertyId={property-id}-lease#search_listings/.
  2. In one browser-agent call, goto that URL, wait up to 8 seconds for iframe[src*="buildout.com"], then evaluate the following script to navigate to the preferred external Buildout frame:
(() => {
  const frames = [...document.querySelectorAll("iframe[src]")].filter((f) => {
    try {
      return new URL(f.src).host !== location.host;
    } catch {
      return false;
    }
  });
  const preferred = frames.filter((f) => f.src.includes("buildout.com"));
  const pool = preferred.length ? preferred : frames;
  pool.sort(
    (a, b) => b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight,
  );
  if (!pool.length) return { navigated: false, reason: "no-external-listing-frame" };
  window.location.href = pool[0].src;
  return { navigated: true, frameUrl: pool[0].src };
})();
  1. Wait for h1 for up to 10 seconds, then run this self-contained extractor on the loaded Buildout page:
(() => {
  const clean = (value) => (value ? value.replace(/\s+/g, " ").trim() : null);
  const table = document.querySelector(
    '[slug="demographics"] table, .pdt-demographics table',
  );
  const output = {
    url: location.href,
    title: clean(document.title),
    heading: clean(document.querySelector("h1")?.textContent),
    loaded: !!document.querySelector("h1"),
    demographics: [],
    contacts: [],
  };
  if (table) {
    const headers = [...table.querySelectorAll("thead th")].map((th) =>
      clean(th.textContent),
    );
    const radii = headers
      .map((header) => {
        const match = /(?:^|\b)(\d+(?:\.\d+)?)\s*Mile/i.exec(header || "");
        return match ? Number(match[1]) : null;
      })
      .filter((radius) => radius !== null);
    output.demographics = radii.map((radiusMiles) => ({ radiusMiles }));
    [...table.querySelectorAll("tbody tr")].forEach((row) => {
      const cells = [...row.querySelectorAll("td")];
      if (cells.length < 2) return;
      const label = clean(cells[0].textContent)?.toLowerCase() || "";
      const values = cells.slice(1).map((td) => clean(td.textContent));
      radii.forEach((radiusMiles, index) => {
        const item = output.demographics[index];
        const raw = values[index];
        if (!raw || /^0(?:\.0)?%?$/.test(raw)) return;
        if (/total households/i.test(label))
          item.households = parseInt(raw.replace(/[^0-9]/g, ""), 10) || null;
        else if (/total population/i.test(label))
          item.population = parseInt(raw.replace(/[^0-9]/g, ""), 10) || null;
        else if (/average household income/i.test(label))
          item.avgHHIncome = parseFloat(raw.replace(/[^0-9.]/g, "")) || null;
        else if (/per capita income/i.test(label))
          item.perCapitaIncome = parseFloat(raw.replace(/[^0-9.]/g, "")) || null;
      });
    });
  }
  const cards = [...document.querySelectorAll(".pdt-broker, .broker-info")];
  const seen = new Set();
  cards.forEach((card) => {
    const key = clean(card.innerText);
    if (!key || seen.has(key)) return;
    seen.add(key);
    output.contacts.push({
      name: clean(
        card.querySelector('.broker-name, [class*="broker-name"], h3, h4, strong')
          ?.textContent,
      ),
      title: clean(
        card.querySelector('.broker-title, [class*="broker-title"], .title')
          ?.textContent,
      ),
      phones: [...card.querySelectorAll('a[href^="tel:"]')]
        .map((a) => clean(a.textContent))
        .filter(Boolean),
      email: clean(card.querySelector('a[href^="mailto:"]')?.textContent),
      license: clean(
        card.querySelector('.broker-license, [class*="license"]')?.textContent,
      ),
      img: card.querySelector("img")?.src || null,
      text: key,
    });
  });
  if (!output.contacts.length) {
    const fallback = [
      ...document.querySelectorAll(
        '.agent-info, [slug*="broker"], [slug*="contact"], [slug*="agent"], .contact-info, .broker-card, .js-broker, .pdt-contacts',
      ),
    ];
    fallback.forEach((card) => {
      const text = clean(card.innerText);
      if (!text || seen.has(text)) return;
      seen.add(text);
      output.contacts.push({
        name: clean(card.querySelector("h3, h4, strong")?.textContent),
        title: clean(card.querySelector('.title, [class*="title"]')?.textContent),
        phones: [...card.querySelectorAll('a[href^="tel:"]')]
          .map((a) => clean(a.textContent))
          .filter(Boolean),
        email: clean(card.querySelector('a[href^="mailto:"]')?.textContent),
        license: null,
        img: card.querySelector("img")?.src || null,
        text,
      });
    });
  }
  return output;
})();

Site-Specific Gotchas

  • The target route is a single-page property listing shell keyed by the query parameter propertyId; the observed listing form appends -lease to the property identifier.
  • Retain the #search_listings/ fragment in the direct route.
  • The useful property content is embedded in an external Buildout iframe. Prefer an iframe whose src contains buildout.com; otherwise select the largest external iframe and navigate to its src.
  • Because the Buildout frame is external, navigate to its URL before extracting its DOM rather than assuming the parent page can query the frame contents.
  • Do not guess an opaque property identifier. Substitute the caller-provided {property-id} into the URL.
  • Demographics may be absent. The extractor returns demographics: [] when no matching table exists.
  • Broker markup may use .pdt-broker or .broker-info; if neither exists, the extractor checks agent, contact, broker, and pdt-contacts variants and returns raw card text as a fallback.
  • Contact cards may contain duplicate broker representations; the extractor deduplicates them by normalized visible text.
  • Demographic table selectors rely on the Buildout slug="demographics" convention or the .pdt-demographics class; verify these selectors if Buildout changes its markup.

Expected Output

The browser ends on the loaded external Buildout property page. The extractor returns an object containing the final url, document title, first-level heading, a boolean loaded confirmation, demographics as an array of radius objects with available households, population, avgHHIncome, and perCapitaIncome fields, and contacts as an array containing broker names, titles, phone links, email links, licenses, images, and fallback visible text.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=gilbertgrouprealestate.com&task=navigate-to-property-page