Resolve B2B targeting facets into LinkedIn targetingCriteria URNs - #25
Merged
Conversation
An LLM extracts free-text facet names from a brief (seniorities, job
functions, industries, titles, skills, company sizes); LinkedIn's
targetingCriteria needs entity URNs. TargetingResolver bridges the two,
choosing a resolution path per facet from what the API exposes:
* closed enums (seniorities, jobFunctions) come from the standardized
/seniorities and /functions endpoints, matched by display name;
* open taxonomies (industries, titles, skills) resolve via the
typeahead finder, preferring an exact name match else the top hit;
* company_sizes map statically to staffCountRange tuple URNs
(urn:li:staffCountRange:(min,max) — not the SIZE_* enum the facet
listing returns);
* locations come from the static ISO geo map.
Names that match no real LinkedIn URN are surfaced in
ResolvedTargeting.unresolved for a manual Campaign Manager step, never
fabricated. Verified live against a DRAFT campaign and torn down.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds end-to-end resolution of free-text B2B audience facets into LinkedIn targetingCriteria URNs, and wires that resolver into the LinkedIn publish flow while preserving the “no-guess” contract (unmatched values are surfaced, not fabricated).
Changes:
- Introduces
TargetingResolverto resolve seniorities/job functions via standardized lists and industries/titles/skills via typeahead, plus static staffCountRange and ISO→geo mapping. - Updates LinkedIn publish flow to use resolved
targetingCriteriaand report unresolved facet values as publish notes. - Adds LinkedIn client helpers for typeahead + standardized list endpoints and a focused integration test suite for resolver behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/integrations/test_linkedin_targeting.py | Adds resolver integration tests for enum vs typeahead paths, no-guess behavior, staffCountRange mapping, and caching. |
| src/yieldagent/integrations/linkedin/targeting.py | New resolver module that converts Audience facets into LinkedIn targeting URNs with unresolved surfacing. |
| src/yieldagent/integrations/linkedin/server.py | Switches publish flow to use TargetingResolver output and updates unresolved targeting notes. |
| src/yieldagent/integrations/linkedin/mapping.py | Trims audience_to_targeting down to the geo-only baseline and updates its docstring accordingly. |
| src/yieldagent/integrations/linkedin/client.py | Adds read-only client methods for typeahead targeting entities and standardized seniority/function taxonomies. |
| briefs/linkedin_live_test_brief.md | Adds a live-test campaign brief used for manual verification/reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2 tasks
th0rz05
added a commit
that referenced
this pull request
Jun 9, 2026
An LLM extracts free-text facet names from a brief (seniorities, job
functions, industries, titles, skills, company sizes); LinkedIn's
targetingCriteria needs entity URNs. TargetingResolver bridges the two,
choosing a resolution path per facet from what the API exposes:
* closed enums (seniorities, jobFunctions) come from the standardized
/seniorities and /functions endpoints, matched by display name;
* open taxonomies (industries, titles, skills) resolve via the
typeahead finder, preferring an exact name match else the top hit;
* company_sizes map statically to staffCountRange tuple URNs
(urn:li:staffCountRange:(min,max) — not the SIZE_* enum the facet
listing returns);
* locations come from the static ISO geo map.
Names that match no real LinkedIn URN are surfaced in
ResolvedTargeting.unresolved for a manual Campaign Manager step, never
fabricated. Verified live against a DRAFT campaign and torn down.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TargetingResolver, which turns the LLM-extracted free-text B2B facet names from a brief (seniorities, job functions, industries, titles, skills, company sizes) into the entity URNs LinkedIn'stargetingCriteriarequires./senioritiesand/functionslists by display name. Lists are fetched once and cached per publish.urn:li:staffCountRange:(min,max)tuple URNs (not theSIZE_*enum the facet listing returns; the top bucket uses INT_MAX as its upper bound).ResolvedTargeting.unresolved(caller flags it as a manual Campaign Manager step) and is never fabricated into a URN.server.py), replacing the static geo-only targeting.mapping.audience_to_targetingis trimmed to the geo-only baseline it now is, retained for inspection/tests.typeahead_targeting_entities,list_seniorities,list_functions.Verification
targetingCriteria(staffCountRange tuple URNs + seniorities, jobFunctions, industries, skills, locations) was accepted by LinkedIn and read back from the API. All test resources and a prior-session orphan were deleted; account confirmed clean. Nothing can spend — everything stayed DRAFT.Test plan
pytest -qgreenruff checkclean