jackwener/OpenCLI · error · CommandExecutionError

LinkedIn services-read could not find stable Services page c

Error message

LinkedIn services-read could not find stable Services page content

What it means

normalizeServices throws this CommandExecutionError when the extracted row exists but lacks the minimal signals of a real Services page: no service_url, or service_url present with none of page_title, overview, or any services_provided entries. It guards against returning fabricated/empty records when the page content is not actually a Services page.

Source

Thrown at clis/linkedin/services-read.js:122

    const description = lines[i + 1] || '';
    if (title) pairs.push(description ? `${title} — ${description}` : title);
  }
  return pairs;
}

function normalizeServices(row) {
  if (!row || typeof row !== 'object') {
    throw new CommandExecutionError('LinkedIn services-read returned malformed extraction payload');
  }
  const services = Array.isArray(row.services_provided) ? row.services_provided.map(normalizeWhitespace).filter(Boolean) : [];
  const mediaItems = pairsToMedia(row.media_lines);
  const publicMedia = [];
  const serviceUrl = normalizeWhitespace(row.service_url);
  const pageTitle = normalizeWhitespace(row.page_title);
  const overview = normalizeWhitespace(row.overview);
  const availability = normalizeWhitespace(row.availability);
  if (!serviceUrl || (!pageTitle && !overview && services.length === 0)) {
    throw new CommandExecutionError('LinkedIn services-read could not find stable Services page content');
  }
  return {
    service_url: serviceUrl,
    page_title: pageTitle,
    overview,
    availability,
    work_locations: Array.isArray(row.work_locations) ? row.work_locations.map((item) => {
      const text = normalizeWhitespace(item);
      const words = text.split(' ');
      if (words.length % 2 === 0) {
        const half = words.length / 2;
        const left = words.slice(0, half).join(' ');
        if (left === words.slice(half).join(' ')) return left;
      }
      return text;
    }).filter(Boolean).join('; ') : '',
    pricing: normalizeWhitespace(row.pricing).replace(/^Pricing,\s*Select one option,\s*/i, '').replace(/,\s*required$/i, ''),
    services_provided: services.join('; '),

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Increase the wait after page.goto(servicesUrl) (currently 5s) so content hydrates before extraction.
  2. Open the servicesUrl in the browser to confirm the Services page still exists and renders.
  3. Update extraction selectors to current LinkedIn markup, then re-run.

Example fix

// before
await page.goto(servicesUrl);
await page.wait(5);
// after
await page.goto(servicesUrl, { waitUntil: 'networkidle' });
await page.waitForSelector('[data-services-content], .services-page', { timeout: 15000 }).catch(() => {});
await page.wait(5);
Defensive patterns

Strategy: retry

Validate before calling

// wait for content before extracting
await page.goto(servicesUrl, { waitUntil: 'networkidle' });
await page.waitForSelector('.services-page, [data-services]', { timeout: 15000 }).catch(() => {});

Type guard

function looksLikeServicesRow(row) {
  return Boolean(row && row.service_url && (row.page_title || row.overview || (row.services_provided || []).length));
}

Try / catch

try {
  services = await readServices(page, servicesUrl);
} catch (e) {
  if (/could not find stable Services page content/.test(e.message)) {
    await page.wait(5); services = await readServices(page, servicesUrl); // one retry
  } else throw e;
}

Prevention

When it happens

Trigger: The extraction ran but matched no meaningful content: service_url blank, or a row with a URL yet empty title/overview/services — typically when the script executed on a wrong or partially loaded page.

Common situations: Navigating to a /services/page/<id>/ URL that was deleted or renamed; a not-fully-rendered page (extracted before content hydration); LinkedIn markup changes causing selectors to match nothing.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/a7e1680dbccedb11. Report an issue: GitHub.