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 is published here as a reusable recipe for agents.

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.

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.

Use Cases

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.

Automation Flow

  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;
})();

Possible Friction Points

  • 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.

Call it

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