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
- Increase the wait after page.goto(servicesUrl) (currently 5s) so content hydrates before extraction.
- Open the servicesUrl in the browser to confirm the Services page still exists and renders.
- 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
- Wait for network idle/content hydration before extraction, not a fixed 5s.
- Verify the /services/page/<id>/ URL still exists before scraping.
- Re-validate selectors after LinkedIn markup releases.
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
- LinkedIn jobs preferences could not find stable preferences
- LinkedIn sent invitation cards were not found; the page stru
- dianping search parser found no result-shaped shop cards
- dianping search parser found result cards without shop_id va
- dianping ${contextHint} did not render expected data${sample
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/a7e1680dbccedb11.
Report an issue: GitHub.