Verify PropertyGuru Malaysia Rental Listing, Rent, and Tenant Preferences

Site propertyguru.com.myTask verify-rental-listing-liveVersion v6Updated Aug 20, 2026Category real-estate

Check whether one or more PropertyGuru Malaysia rental listing URLs are live and extract displayed monthly rent, identifying fields, tenant-preference text, and the primary listing image when present. This skill was captured from a live agent session on propertyguru.com.my 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

Verify the current status of one or more PropertyGuru Malaysia rental listings and, when live, return displayed monthly rent, identifying fields, visible tenant-preference information, and the primary social-preview image when available.

When to Use

Use when the caller provides one or more canonical PropertyGuru listing URLs. Listing URLs have the form https://www.propertyguru.com.my/property-listing/{slug}-{numeric-listing-id}; preserve supplied slugs and opaque IDs rather than guessing either value. If only a numeric {listing-id} and identifying text are supplied, resolve the canonical URL from PropertyGuru search results before verification.

Workflow

  1. If {listing-url} is supplied, navigate directly to it with waitUntil: "domcontentloaded". Do not visit the homepage or use the search UI. If an expected {listing-id} is also supplied, verify that the URL path ends in -{listing-id} before treating the page as the requested listing.
  2. If only {listing-id} plus a name, location, or agent query is supplied, first navigate directly to https://www.propertyguru.com.my/property-for-rent?freetext={url-encoded-query} (or /room-for-rent?freetext={url-encoded-query} for room-only searches). After rendering and solving any challenge, find an anchor matching a[href*="/property-listing/"] whose canonical URL path ends in -{listing-id}. Use that returned canonical URL for the verification navigation; never invent the slug.
  3. Wait for client rendering. If a Cloudflare challenge appears, solve it and wait until listing content replaces the challenge. Do not extract or classify the challenge page.
  4. On the rendered listing page, run this evaluator as-is:
(() => {
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
const body = document.body?.innerText || '';
const title = clean(document.title);
const h1 = clean(document.querySelector('h1')?.textContent);
const lines = body.split(/\r?\n/).map(clean).filter(Boolean);
const monthlyPattern = /RM\s*[0-9][0-9,]*(?:\.[0-9]+)?\s*(?:\/\s*(?:month|mo|bulan)|per\s+month)/i;
const genericPattern = /RM\s*[0-9][0-9,]*(?:\.[0-9]+)?/i;
const monthlyRent = body.match(monthlyPattern)?.[0]?.trim() ||
  document.querySelector('[class*="listing-price"], [class*="price"]')?.textContent?.trim() ||
  body.match(genericPattern)?.[0]?.trim() || null;
const tenantLabel = /^(tenant\s*preferences?|preferred\s*tenants?|suitable\s*for|tenant type)\s*:?/i;
const sectionStop = /^(property details?|facilities|amenities|description|location|contact|agent|photos?|floor plans?|mortgage|nearby|similar properties|about this property)\b/i;
const labelIndex = lines.findIndex(line => tenantLabel.test(line));
let tenantPreferences = null;
if (labelIndex >= 0) {
  const label = lines[labelIndex];
  const inline = label.replace(tenantLabel, '').replace(/^\s*[::-]\s*/, '').trim();
  const values = [];
  if (inline) values.push(inline);
  for (let i = labelIndex + 1; i < lines.length && values.length < 20; i++) {
    if (sectionStop.test(lines[i]) && values.length) break;
    if (tenantLabel.test(lines[i]) && i !== labelIndex) break;
    values.push(lines[i]);
  }
  tenantPreferences = {
    label,
    values: values.filter((v, i, a) => a.indexOf(v) === i),
    text: values.join(' | ') || null
  };
}
const ogImage = document.querySelector('meta[property="og:image"]')?.content?.trim() || null;
const fallbackImage = [...document.querySelectorAll('img')]
  .map(img => img.currentSrc || img.src || img.getAttribute('data-src') || null)
  .find(Boolean) || null;
const lower = `${title} ${h1} ${body}`.toLowerCase();
const challenge = /checking your browser|verify you are human|just a moment|cloudflare|enable javascript and cookies/.test(lower);
const unavailable = /404|page not found|listing not found|no longer available|property is no longer|listing has been removed|does not exist/.test(lower);
const hasListingSignals = !!h1 && (!!monthlyRent || /for rent|rent|rental|bedroom|bathroom|sq\s*ft|sqft|property type/.test(lower));
const state = challenge ? 'blocked' : (unavailable || !hasListingSignals ? 'unavailable' : 'live');
const idMatch = location.pathname.match(/-(\d{6,})(?:\/)?$/);
return {
  live: state === 'live',
  state,
  url: location.href,
  listingId: idMatch ? idMatch[1] : null,
  title: title.slice(0, 200) || null,
  h1: h1 ? h1.slice(0, 200) : null,
  monthlyRent: monthlyRent ? clean(monthlyRent).slice(0, 100) : null,
  tenantPreferences,
  image: ogImage || fallbackImage,
  imageSource: ogImage ? 'og:image' : (fallbackImage ? 'img-fallback' : null),
  signals: { challenge, unavailable, hasListingSignals },
  bodyPreview: body.slice(0, 500)
};
})()
  1. For multiple supplied URLs, verify each independently, or batch direct navigations and evaluations in one browser-agent call where supported. Return one result per listing. When a caller names an expected ID, compare it with the extractor's listingId and report any mismatch rather than silently substituting a different listing.

Site-Specific Gotchas

  • PropertyGuru may present a Cloudflare anti-bot challenge after navigation; solve it before extracting status, rent, tenant preferences, or images.
  • A successful HTTP navigation alone does not prove that a listing is live. Require a meaningful heading plus rental/listing content, ideally including a price.
  • Tenant-preference wording may appear under Tenant Preferences, Preferred Tenants, Suitable For, or Tenant Type; the extractor returns visible lines following the first matching label until a likely next section heading.
  • If no tenant-preference label is present, tenantPreferences is null; do not infer preferences from the listing title, agent name, or room type.
  • Treat 404, not-found, removed, and no-longer-available text as unavailable even if residual page markup is present.
  • Listing IDs are opaque. When only an ID is supplied, resolve it from a direct search result URL and select the anchor whose canonical path ends in -{listing-id}; never fabricate a slug or ID.
  • When both an expected ID and a canonical URL are supplied, verify the numeric suffix exactly. Similar slugs can refer to different opaque listing IDs; do not report a neighboring URL as verification of the requested listing.
  • Monthly rent is normally exposed as an RM amount followed by / month; the extractor prefers that form, then uses price selectors and a generic RM fallback.
  • The primary listing image is commonly exposed through meta[property="og:image"]; opening or scrolling the gallery is unnecessary.
  • If og:image is absent, the first DOM image may be a logo, placeholder, or thumbnail. Label it with imageSource: "img-fallback" and do not assume it is the primary property photo.
  • Image URLs may be CDN URLs and can change independently of the listing URL; return the URL exactly as exposed by the page.
  • Class names such as [class*="listing-price"] and [class*="price"] are fallback selectors and may change; body-text patterns are the primary extraction method.

Expected Output

Return one object per supplied listing containing the loaded canonical url, resolved listingId, title, h1, state, live, displayed monthlyRent, optional tenantPreferences, optional primary image and imageSource, challenge/availability signals, and bounded bodyPreview. A state: "live" result indicates current-looking rental content; state: "unavailable" indicates missing or removed content; and state: "blocked" indicates Cloudflare prevented verification and the page must be retried after solving the challenge. If an expected listing ID does not equal listingId, report the identity mismatch explicitly.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=propertyguru.com.my&task=verify-rental-listing-live