jackwener/OpenCLI · error · ArgumentError
Sales Navigator recipient urn must be urn:li:fs_salesProfile
Error message
Sales Navigator recipient urn must be urn:li:fs_salesProfile:(profileId,authType,authToken)
What it means
An ArgumentError from parseThreadInput (clis/linkedin/salesnav-thread.js:39) raised when the user passes a value that starts with 'urn:li:fs_salesProfile:(' but whose payload does not pass parseSalesProfileUrn validation. A valid URN must be exactly urn:li:fs_salesProfile:(profileId,authType,authToken) with three non-empty, comma-separated parts, none of which are 'undefined', 'null', or 'not_available'. This guards against passing placeholder or truncated URNs that would fail downstream API calls.
Source
Thrown at clis/linkedin/salesnav-thread.js:39
return host === 'linkedin.com' || host.endsWith('.linkedin.com');
}
function parseSalesProfileUrn(value) {
const raw = normalizeWhitespace(value);
const match = raw.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);
if (!match) return '';
const parts = [match[1], match[2], match[3]].map((part) => normalizeWhitespace(part).toLowerCase());
if (parts.some((part) => !part || part === 'undefined' || part === 'null' || part === 'not_available')) return '';
return raw;
}
function parseThreadInput(value) {
const raw = normalizeWhitespace(value);
if (!raw) return ['empty', ''];
if (/^2-[A-Za-z0-9+/=_-]+$/.test(raw)) return ['thread_id', raw];
if (/^urn:li:fs_salesProfile:\(/.test(raw)) {
const urn = parseSalesProfileUrn(raw);
if (!urn) throw new ArgumentError('Sales Navigator recipient urn must be urn:li:fs_salesProfile:(profileId,authType,authToken)');
return ['recipient_urn', urn];
}
try {
const url = new URL(raw);
if (url.protocol !== 'https:' || url.username || url.password || url.port || !isLinkedInHost(url.hostname)) return ['name', raw.toLowerCase()];
const inboxMatch = url.pathname.match(/^\/sales\/inbox\/([^/]+)\/?$/i);
if (inboxMatch) return ['thread_id', decodeURIComponent(inboxMatch[1])];
const leadMatch = url.pathname.match(/^\/sales\/lead\/([^,/]+),([^,/]+),([^/]+)\/?$/i);
if (leadMatch) {
const urn = `urn:li:fs_salesProfile:(${decodeURIComponent(leadMatch[1])},${decodeURIComponent(leadMatch[2])},${decodeURIComponent(leadMatch[3])})`;
if (!parseSalesProfileUrn(urn)) {
throw new ArgumentError('Sales Navigator lead URL must contain resolved profileId, authType, and authToken');
}
return ['recipient_urn', urn];
}
} catch (err) {
if (err instanceof ArgumentError) throw err;
// Fall through to name matching for non-URL text.View on GitHub (pinned to 49907e53dc)
Solutions
- Inspect the URN and ensure it has exactly three comma-separated segments inside parentheses: (profileId,authType,authToken), with no empty or 'undefined' parts.
- Copy the recipient_urn fresh from a salesnav-search output row (its recipient_urn column is already validated) instead of hand-building one.
- If you only have a profileId, use the Sales Navigator lead URL form https://www.linkedin.com/sales/lead/<profileId>,<authType>,<authToken> or the person's exact name and let the command resolve it.
- Strip surrounding whitespace/quotes and re-check for stray characters like nested parentheses or trailing commas before retrying.
Example fix
// before
const urn = `urn:li:fs_salesProfile:(${lead.profileId},undefined,${lead.token})`;
await run('linkedin salesnav-thread', [urn]);
// after (validate parts before building)
const parts = [lead.profileId, lead.authType, lead.authToken];
if (parts.some((p) => !p || p === 'undefined')) throw new Error('incomplete salesProfile parts');
const urn = `urn:li:fs_salesProfile:(${parts.join(',')})`;
await run('linkedin salesnav-thread', [urn]); Defensive patterns
Strategy: validation
Validate before calling
// Validate a recipient URN before calling salesnav-thread:
function isValidSalesProfileUrn(v) {
const m = /^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/.test(v);
const parts = v.slice('urn:li:fs_salesProfile:('.length, -1).split(',');
return parts.length === 3 && parts.every((p) => p && !['undefined','null','not_available'].includes(p));
}
if (!isValidSalesProfileUrn(urn)) throw new Error('recipient_urn must be (profileId,authType,authToken) with resolved values'); Type guard
function isSalesProfileUrn(value) {
if (typeof value !== 'string') return false;
const m = value.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);
if (!m) return false;
return m.slice(1).every((p) => p && !['undefined', 'null', 'not_available'].includes(p));
} Try / catch
try {
await run('linkedin salesnav-thread', [urn]);
} catch (err) {
if (err instanceof ArgumentError && /recipient urn/.test(err.message)) {
// fall back to resolving by exact name via salesnav-search output
return run('linkedin salesnav-thread', [lead.name]);
}
throw err;
} Prevention
- Always copy recipient_urn from a validated salesnav-search output row rather than hand-building URNs.
- Check for 'undefined'/'null' in URN parts — that means a source object field was missing at template time.
- When only a profileId is known, resolve the full URN via search instead of guessing authType/authToken.
When it happens
Trigger: Passing a recipient URN like 'urn:li:fs_salesProfile:(ACoAABC,)' (missing authToken), a URN containing literal 'undefined'/'null'/'not_available' parts copied from unset object fields, or a URN with stray parentheses/extra commas that breaks the strict three-part regex.
Common situations: Copy-pasting a URN from partially rendered JSON or logs where the authToken field was absent; templating a URN from a JS object whose fields were undefined (stringifying to 'undefined'); hand-editing URNs and dropping a segment; older lead_url formats with different segment counts.
Related errors
- --recipient must resolve to a Sales Navigator lead urn
- --recipient must be a Sales Navigator lead URL, Sales Naviga
- Sales Navigator lead URL must contain resolved profileId, au
- LinkedIn messaging API returned a conversation without threa
- --limit must be an integer between ${MIN_LIMIT} and ${MAX_LI
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/7df82a0af6569324.
Report an issue: GitHub.