LinkedIn retargeting for B2B: build the audience and choose the next message
Start with the action
LinkedIn retargeting works when you begin with a recorded action, not a vague group called “warm traffic.” Choose the LinkedIn source that can prove that action. Record what is available now. Exclude people who have already completed the next step. Then choose one message that answers the question their action is likely to raise.
Keep the plan on Hold when the source, permission, audience status, suppression or downstream measurement is missing. A complete plan is ready for an operator to review. It does not create an audience, authorize spend, launch ads or prove results.
Use the source-to-message planner on this page to produce both a readable handoff and a structured record. The same record keeps four states separate:
- Source: Is the qualifying action available and supported by evidence?
- Population: Has LinkedIn processed enough matched member accounts for the audience to be usable?
- Delivery: Did an approved ad set actually serve to this audience?
- Outcome: Did the next action reach the CRM and contribute to a qualified business event?
A signal can exist while the other three states remain Unknown.
Choose the source that matches the action
LinkedIn currently documents website visits, single-image engagement, document engagement, video views, Lead Gen Form activity, Conversation Ad activity, Company Page activity, Event activity and Conversions API signals as retargeting sources. LinkedIn also supports Matched Audiences built from contact lists.
Use that list to choose a source. Finish the plan with current evidence, suppressions, the next message and downstream measurement. Start with the action you can actually observe.
| Observed action | LinkedIn source | Choice you must record | First evidence request |
|---|---|---|---|
| Visited a pricing, product or other specific page | Website | Website Action or URL rule; lookback window | Active Insight Tag, exact page/action rule and current signal state |
| Viewed a video ad | Video | At least 25%, 50%, 75% or 97% viewed; 30, 60, 90, 180 or 365 days | Selected video ad set, threshold, window and current audience status |
| Opened or submitted a Lead Gen Form | Lead Gen Form | Opened versus submitted; 30, 60, 90, 180 or 365 days | Selected form/ad set, action, window and current audience status |
| Interacted with a single-image ad | Single image | Any interaction versus chargeable click; a window from 30 to 365 days | Selected ad set, interaction rule and current audience status |
| Downloaded, interacted with or clicked a document ad | Document | Download/interaction versus chargeable click; a window from 30 to 365 days | Selected ad set, action rule and current audience status |
| Opened a Conversation Ad or clicked one of its CTAs | Conversation | Open versus CTA click; 30, 60, 90, 180 or 365 days | Selected ad set, action rule and current audience status |
| Viewed a Company Page or clicked its header CTA | Company Page | Page view versus CTA action and selected window | Associated Page, account/Page access and current audience status |
| Responded to or engaged with a LinkedIn Event | Event | Clicked to attend, registered and attended, registered but did not attend, or engaged without registering | Selected Event, exact event action and current audience status |
| Appears in an approved first-party list | Contact list | Contact source, use permission and refresh rule | Approved source, permission record, upload owner and current audience status |
Choose one row for the first plan. If your source could mean two different actions, split it. Someone who opened a form and someone who submitted it should not inherit the same message merely because both touched the same form.
What “Matched Audience” proves—and what it does not
A Matched Audience is LinkedIn's audience segment built from source data. Its status is a platform-processing state.
LinkedIn's current status guide includes Building, Ready, Updating, Failed, Expired, Archived, Website: sending signal and Website: no signal. LinkedIn defines Ready as fully processed and ready to use in ad sets. Building and Updating can take up to 48 hours and, in rare cases, 72 hours.
Use those labels for the questions they actually answer:
- Website: sending signal means the audience received a signal event.
- Ready means LinkedIn processed the audience for ad-set use.
- Failed means the uploaded list or data-integration segment has a problem.
- Expired means the segment needs to be reactivated or uploaded again before use.
They do not show whether an ad served, whether a qualified person clicked, whether a CRM event arrived or whether revenue resulted.
LinkedIn says a retargeting audience needs at least 300 matched member accounts before it can be used in ad-set targeting. Treat 300 as a platform-use threshold. It is not a recommended audience size, a forecast, a match-rate promise or evidence that your campaign will deliver.
Do not infer matched population from an upstream count. LinkedIn specifically warns that video views and Lead Gen Form opens or completions do not correspond directly to matched member accounts. The same discipline applies to page visits and engagement totals: ask for the audience's current matched state.
Set up website retargeting without assuming history
Website retargeting needs the LinkedIn Insight Tag. LinkedIn documents two ways to define the audience:
- Use Website Actions collected on tagged pages, such as button clicks, page visits and form submissions.
- Add a URL rule using starts with, equals or contains.
Record the actual action or URL expression. “High-intent website visitors” is too loose to review.
LinkedIn says a website audience starts building on the day you create it. It does not include website activity from before that date. If you create a 30-day audience today, the selected window does not give you the previous 30 days of visitors. The audience fills as new events arrive.
Before calling the source usable, collect:
- The ad account and Insight Tag identity.
- A current active-tag or signal receipt.
- The exact Website Action or URL rule.
- The audience creation date and selected window.
- The current audience status and matched population state.
- The pages that must be excluded from the inclusion rule.
- The owner responsible for the tag, audience and downstream event.
Privacy belongs in the source decision. LinkedIn says the Insight Tag collects the page URL, referrer, IP address, device and browser characteristics and timestamp, with stated pseudonymization and deletion practices. It also says the tag should not be installed on pages that collect or contain sensitive data, including certain consumer health and financial pages. Ask the appropriate privacy or legal owner to approve the page set when that restriction could apply. A media buyer's planned install is not approval.
Choose the window from the decision you need to make
A longer window produces a broader group whose original action may say less about what they need now. A shorter window preserves recency but may never reach a usable matched population.
Use three inputs:
- How specific is the action? A pricing-page visit usually says more than a homepage visit. A 75% video view says more than a 25% view, but it may create a smaller source pool.
- How long is the real evaluation cycle? The audience should have a reason to remain useful for the full window.
- Can LinkedIn populate it? Record the current matched state instead of guessing from visits or views.
If the useful action cannot reach a usable audience, do not dilute the definition immediately. First test whether adjacent actions ask the same next question. You might combine two product-page rules that reflect one evaluation task. Do not combine a broad homepage visit and a form submission merely to reach 300.
When no honest combination exists, keep the narrow plan on Hold, use the source for measurement, or choose another eligible channel. The platform threshold is a constraint. It should not rewrite the meaning of the audience.
Suppress people who should not receive the next ask
Retargeting breaks when acquisition messaging keeps following people who have already completed the step. Define suppressions from auditable sources rather than wishful labels.
Consider four branches:
- Completed step: form submitters, registrants, demo requests or other people who completed the action your ad asks for.
- Customers: current customers when the message is only for prospects.
- Open opportunities: active deals when the paid message would conflict with sales or repeat an offer already in progress.
- Privacy and preference: records that should not be used because permission is absent, revoked or restricted.
Each suppression needs a source, owner, refresh rule and current state. “Exclude customers” is not complete if nobody can identify the list, CRM property, sync or owner.
Do not let a missing suppression vanish behind a general targeting note. Return Hold with a focused request such as:
RevOps owner: confirm the CRM property and synced audience that identifies current open opportunities for this offer. Supply the list/audience reference, refresh rule and current status.
Match the next message to the question behind the action
The strongest competitor pages explain source types. The harder job is deciding what to say next. Use the action to form a hypothesis, then choose one message job.
| Signal | Likely question | Useful message job | Proof or asset | Destination |
|---|---|---|---|---|
| Broad educational visit or 25% video view | “What is changing, and why should I care?” | Orient | Clear category explanation or playbook | Relevant guide |
| Product or use-case page visit | “Can this solve my situation?” | Provide proof | Specific customer story, product walkthrough or before/after process | Case study or product detail |
| Deep video view or document interaction | “How should I evaluate the options?” | Help evaluation | Checklist, comparison or implementation guide | Evaluation resource |
| Pricing visit or repeated high-intent action | “Is the next conversation worth it?” | Invite evaluation | Customer-led session, diagnostic or scoped assessment | Registration or booking page |
| Known completed step | “What happens next?” | Stop acquisition or change branch | Onboarding, follow-up or account-specific information | Owned follow-up path |
This is a planning hypothesis. The public ad examples below show visible message and offer choices. Their Ad Library records do not reveal whether the advertisers used them for retargeting.
Orientation: Microsoft Advertising's playbook
Microsoft Advertising's six-card carousel opens with a change in how shoppers discover and compare products, then offers a playbook. That is an orientation job. It helps a reader name a changing category before asking for a sales conversation.
The adaptation: use an education asset when the source action shows initial attention but does not establish active vendor evaluation. The next message should make the problem easier to understand, not pretend the visitor asked for a demo.
Proof: Canva's franchise customer story
Canva's sponsored post presents the franchise problem, names Brand Kits, locked templates and approval workflows, and uses F45 Training as the customer proof. The quoted turnaround and scale remain Canva's advertiser claims.
The adaptation: follow a relevant product or vertical page visit with proof from the same operating context. Preserve the customer attribution. Do not turn the advertiser's claim into an independent result or assume the ad reached franchise buyers.
Evaluation: Fleetio's buying checklist
Fleetio's nine-card carousel frames government fleet management as a multi-criterion software decision and makes the checklist visible across the cards.
The adaptation: a warm evaluator may need criteria more than another product claim. Offer a checklist when the source action suggests comparison or implementation research. The public record does not prove that government accounts saw or acted on the ad.
Evaluation invitation: Intuit Accountants' customer session
Intuit Accountants' ad invites readers to a customer migration session. Its customer scale and reduction figures remain advertiser claims.
The adaptation: use a customer-led session when the source action justifies a higher-commitment next step and the session actually answers the likely question. A webinar is not automatically the right retargeting offer. The topic, speaker, destination and registration handoff must be real and verified.
Direct next step: Ramp's construction workflow
Ramp's ad names a construction expense workflow, uses “Six job sites. One dashboard.” as the headline and asks the reader to learn more. The captured packet contains the visible ad card, not the playable video. Its “5,500+” statement remains an advertiser claim.
The adaptation: use a direct product step when the source action identifies a concrete workflow and the destination continues that story. A generic homepage would break the handoff.
A useful practitioner view of the next message
In a 2022 Impactable video, the passage from 01:39 to 03:57 treats the first website action as an intent signal and argues that the next message should address timing, familiarity, trust or credibility. It suggests testimonials, case studies, press and other proof formats.
Use the passage as practitioner strategy for choosing the next-message job. Use current LinkedIn Help pages for setup and eligibility. The video does not establish today's LinkedIn source rules, windows, prices or results.
Three worked LinkedIn retargeting plans
Every company, person, account state and result in the following cases is fictional. The examples demonstrate the record and its decisions. They are not Maestro campaigns or client results.
Fictional case 1: website pricing visit to proof
Northstar Security sells access-governance software to mid-market IT teams. Its proposed qualifying action is a visit to /pricing. The fictional ad account has an active Insight Tag receipt and a website audience created on September 10, 2026, using an exact URL rule. The selected window is 90 days. LinkedIn shows Website: sending signal, but the audience is still Building and no matched count receipt has been supplied.
The next message job is proof. The team proposes a customer story about reducing manual access-review work, leading to the full case study. Demo requesters, customers and open opportunities should be suppressed through named CRM audiences.
The plan remains Hold. The source is available, but population is not ready and the open-opportunity suppression has no current audience reference.
| Field | Fictional value |
|---|---|
| Source/action | Website; exact /pricing visit |
| Source evidence | Active tag receipt and sending-signal state supplied |
| Window | 90 days; audience begins from its September 10 creation date |
| Audience state | Building; matched population Unknown |
| Suppressions | Demo requesters and customers identified; open-opportunity audience missing |
| Message job | Provide proof |
| Proof and destination | Fictional customer story → full case-study page |
| Downstream event | case_study_view joined to CRM account by approved first-party key |
| Owner | Paid media: Alex Rivera; RevOps: Jordan Lee |
| Decision | Hold |
| Exact requests | LinkedIn audience Ready/matched-state receipt; open-opportunity suppression reference and refresh rule |
Fictional case 2: deep video view to evaluation checklist
Harbor Systems sells fleet-maintenance software. The proposed source is a video-ad audience containing people who watched at least 75% of a product walkthrough in the past 90 days. The selected video ad set, threshold, window and current Ready receipt are supplied. A Ready receipt proves processing for ad-set use. It does not prove delivery.
The next message job is evaluation. The fictional offer is a maintenance-software checklist leading to a guide with criteria for inspections, parts, work orders and reporting. Existing customers and people who downloaded the checklist are suppressed through documented current sources.
The plan is Ready for operator review. It is not launched. Delivery and outcome remain Unknown until an authorized campaign is created and the evidence arrives.
| Field | Fictional value |
|---|---|
| Source/action | Video; at least 75% viewed |
| Window | 90 days |
| Audience state | Ready receipt supplied; source-event and matched counts kept separate |
| Suppressions | Current customers and checklist downloaders, with named source owners |
| Message job | Help evaluation |
| Proof and destination | Fictional buying checklist → evaluation guide |
| Downstream event | evaluation_guide_download with campaign and contact keys |
| Owner | Paid media: Priya Shah; RevOps: Morgan Lee |
| Decision | Ready for operator review |
| Unknowns that remain | Delivery, qualified actions, opportunity influence and revenue |
Fictional case 3: contact list with missing permission
Summit Analytics proposes uploading a list of contacts who attended an owned workshop. The file exists, but the team has not supplied the collection notice, approved use purpose, opt-out handling or a current owner approval. No LinkedIn upload or audience receipt exists.
The proposed next message is an evaluation invitation to a product diagnostic. That creative idea does not repair the missing permission.
The plan stays Hold. Do not upload the list, include raw identifiers in the planner or treat attendance as permission for every advertising use.
| Field | Fictional value |
|---|---|
| Source/action | Contact list; attended an owned workshop |
| Permission state | Unknown |
| Audience state | Not created; matched population Unknown |
| Suppressions | Customers and open opportunities proposed but unverified |
| Message job | Invite evaluation |
| Owner | Data owner requested; paid-media owner supplied |
| Decision | Hold |
| Exact requests | Collection/notice source, approved advertising use, opt-out treatment, data owner, suppression sources and later LinkedIn status receipt |
Measure the handoff beyond LinkedIn
Do not finish the plan at “website conversion.” Name the event and the system that receives it.
Record:
- The destination URL and its owner.
- The exact downstream event, such as a guide download, registration or qualified demo request.
- The campaign, contact and account keys used to join the event.
- The CRM field or object that stores the result.
- The person who checks the handoff.
- The review trigger and timing.
Keep the evidence chain in order:
- Source action observed.
- Matched Audience processed.
- Approved ad set delivered.
- Destination action received.
- CRM handoff verified.
- Qualified business event observed.
A later step cannot repair a missing earlier receipt. A CRM opportunity does not prove this audience caused it. A Ready audience does not prove an impression.
Build the portable source-to-message handoff
Build the source-to-message handoff
Load a fictional example or start blank. Every edit recomputes the decision and evidence requests. Ready means ready for operator review.
Business and source
Source prerequisite and permission
Audience state
Suppressions
Next message
Measurement
Evidence requests
Readable Markdown handoff
Canonical JSON record
Planning tool. It does not create an audience, upload data, authorize spend or launch a campaign.
Download the LinkedIn retargeting kit
The JSON records and two prompts use the same contract as the planner. The prompts work without this article.
Copy the full build prompt
Contract `maestro.linkedin-retargeting-plan.v1` version `1.1.0`.
Reference summary:
Current first-party summary observed 2026-09-29: LinkedIn documents website, single-image, document, video, Lead Gen Form, Conversation Ad, Company Page, Event and CAPI retargeting sources. Website audiences require the Insight Tag and begin building on creation; they do not backfill earlier site activity. Video thresholds are 25, 50, 75 or 97 percent with documented windows. Source event/view/form counts do not directly equal matched member accounts. Ready means processed for ad-set use, not delivered or commercially successful. EEA/Switzerland matching is subject to member preferences.
Current source: https://www.linkedin.com/help/lms/answer/a427551
Status source: https://www.linkedin.com/help/lms/answer/a795242
Website source: https://www.linkedin.com/help/lms/answer/a420433/
Use only supplied facts. Preserve source facts byte for byte. Never invent audience size, permission, source availability, Ready state, suppression, delivery, outcomes, owner approval or dates. Natural-language denial, pending, planned, requested, expected or unavailable evidence is not affirmative. Ready is only `ready_for_operator_review`; it does not authorize audience creation, upload, spend or launch. Keep source availability, population, delivery and business outcome separate.
Evaluation contract:
- Contract ID `maestro.linkedin-retargeting-plan.v1`, version `1.1.0`. A mismatch emits `REQ-CONTRACT` and cannot be Ready.
- Decision precedence: hold > ready_for_operator_review. This evaluator returns Hold for any request and `ready_for_operator_review` only with none.
- Discard imported derived values before evaluation.
- Source binding: source.route must equal the top-level route. Changing route never reuses evidence gathered for another route.
- Timestamp rule: source.observed_at and audience.status_observed_at must be calendar-valid timezone-aware ISO 8601 timestamps.
- Evidence rule: Evidence references must be present and affirmative. Natural-language denied, pending, planned, requested, expected or unavailable references are not receipts.
- Population rule: usable requires an integer matched_accounts_count of at least 300; zero requires count 0; below_threshold requires an integer count from 0 through 299.
- Unknown, No, numeric Zero and valid Not applicable are distinct. Whole-value aliases Unknown, Not applicable, n/a, na, pending, unavailable, denied, none, null, tbd, to be determined, planned, requested, expected and placeholder are unresolved unless a route explicitly permits the state.
- Broadly missing rule: Three or more incomplete planning groups. Groups are {"scope": ["business.name", "business.offer", "business.buyer_scope"], "source": ["route", "source.route", "source.qualifying_action"], "message": ["message.job", "message.buyer_question", "message.offer", "message.destination_url"], "owners": ["business.owner", "permission.owner", "message.owner", "measurement.owner"], "measurement": ["measurement.downstream_event", "measurement.crm_key", "measurement.review_trigger"]}. In that branch emit only missing PLAN-* requests, in scope/source/message/owners/measurement order, plus REQ-CONTRACT when applicable. Do not ask for account receipts.
- Once fewer than three planning groups are incomplete, validate the selected route, timestamps, affirmative permission, Ready audience and usable count, all four suppressions, message and measurement. Emit requests in contract/source/tag/permission/audience/completed/customer/opportunity/privacy/message/measurement order.
Stable request vocabulary (copy IDs and request text exactly):
- `REQ-CONTRACT`: Use contract maestro.linkedin-retargeting-plan.v1 version 1.1.0 and migrate the record before review.
- `PLAN-SCOPE`: Supply the business, offer and buyer scope for this plan.
- `PLAN-SOURCE`: Choose one supported source route and name the exact qualifying action.
- `PLAN-MESSAGE`: Supply the next-message job, buyer question, offer and destination.
- `PLAN-OWNERS`: Assign owners for the plan, data permission, message and measurement.
- `PLAN-MEASUREMENT`: Supply the downstream event, CRM key and review trigger.
- `REQ-SOURCE`: Supply the current selected source/action reference and observation date.
- `REQ-TAG`: Supply the current active Insight Tag and website signal receipt.
- `REQ-PERMISSION`: Supply explicit approved data-use evidence, owner and opt-out handling.
- `REQ-AUDIENCE`: Supply the current LinkedIn audience status receipt and matched population state.
- `REQ-SUPPRESSION-COMPLETED`: Supply the completed-step suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-CUSTOMER`: Supply the customer suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-OPPORTUNITY`: Supply the open-opportunity suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-PRIVACY`: Supply the privacy/preference suppression source, refresh rule, owner and current read-back.
- `REQ-MESSAGE`: Supply the next-message job, real proof asset, offer and destination.
- `REQ-MEASUREMENT`: Supply the downstream event, CRM key, owner and review trigger.
Route contract:
- `website`: required source.route, source.qualifying_action, source.rule, source.window_days, source.observed_at, source.evidence_ref, source.insight_tag_state, source.insight_tag_evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]
- `video`: required source.route, source.qualifying_action, source.asset_ref, source.video_view_threshold, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; thresholds [25, 50, 75, 97]
- `lead_gen_form`: required source.route, source.qualifying_action, source.asset_ref, source.lead_form_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['opened', 'submitted']
- `single_image`: required source.route, source.qualifying_action, source.asset_ref, source.single_image_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['any_interaction', 'chargeable_click']
- `document`: required source.route, source.qualifying_action, source.asset_ref, source.document_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['download_or_interaction', 'chargeable_click']
- `conversation`: required source.route, source.qualifying_action, source.asset_ref, source.conversation_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['opened', 'cta_click']
- `company_page`: required source.route, source.qualifying_action, source.asset_ref, source.company_page_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['page_view', 'header_cta_click']
- `event`: required source.route, source.qualifying_action, source.asset_ref, source.event_action, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; actions ['clicked_to_attend', 'registered_attended', 'registered_not_attended', 'engaged_not_registered_or_attended']
- `contact_list`: required source.route, source.qualifying_action, source.asset_ref, source.observed_at, source.evidence_ref, permission.state, permission.evidence_ref, permission.owner, audience.status, audience.status_evidence_ref
- `capi`: required source.route, source.qualifying_action, source.asset_ref, source.observed_at, source.evidence_ref, permission.state, permission.evidence_ref, permission.owner, audience.status, audience.status_evidence_ref
Readable Markdown renderer grammar, in this exact section order:
1. H1 `LinkedIn retargeting source-to-message handoff`.
2. `Record and decision`: Contract ID, Contract version, Record ID, Fixture status, Decision, Input SHA-256.
3. `Business and route`: Business, Offer, Buyer scope, Plan owner, Route, Source evidence kind.
4. `Source`: Qualifying action, Source rule, Source asset or list reference, Window days, Observed at, Source evidence reference, Insight Tag state, Insight Tag evidence reference, Video view threshold, Lead Form action, Single-image action, Document action, Conversation action, Company Page action, Event action, Source event count.
5. `Permission`: State, Evidence reference, Owner, Notice reference, Opt-out process.
6. `Audience`: Audience name, Status, Status observed at, Status evidence reference, Matched population state, Matched account count.
7. `Suppressions`: one H3 per supplied suppression, named by `kind`, with State, Source, Refresh rule, Owner and Evidence reference. If no suppression array is supplied, write `No suppression records supplied.`
8. `Next message`: Job, Buyer question, Proof asset, Offer, Destination URL, CTA, Owner.
9. `Measurement`: Downstream event, CRM key, Owner, Review trigger, Delivery state, Delivery evidence reference, Business outcome state, Business outcome evidence reference.
10. `Decision reasons`: one bullet per `derived.reasons` value in evaluator order; if absent, write `Unknown`.
11. `Evidence requests`: one bullet per request as `**ID** — owner: exact request Reason: exact reason`; if none, write `No planning evidence requests remain. Account-specific launch checks still apply.`
12. `Next action`: copy `derived.next_action` exactly.
For every label above, render the corresponding canonical field even when it is null, zero, false, Unknown, No or Not applicable. Render null or missing as `Unknown`; render numeric zero as `0`; never drop an off-route field. Escape literal `|` as `\|`. This readable handoff is complete field-semantic output, not a selected summary.
Canonical JSON renderer rules:
- Preserve every supplied non-derived field and value exactly. Do not silently migrate a mismatched contract.
- Remove imported `derived`, recompute it, then append keys in this order: decision, reasons, requests, next_action, input_sha256.
- Request objects use keys in this order: id, owner, reason, request. Deduplicate by ID and retain evaluator order.
- Compute `input_sha256` from UTF-8 JSON of the non-derived record with recursively sorted object keys, compact separators and unescaped Unicode.
- Ready next action is exactly `An authorized operator reviews the record and performs account-specific launch checks.`
- Hold next action is exactly the first request's `request`.
Normative standalone evaluator implementation follows. This is the complete validation-to-request mapping, reason vocabulary, owner selection, fallback rule, deduplication behavior and hash algorithm. Use the embedded contract JSON as `CONTRACT`. Do not summarize, rename or improvise any request object. If code execution is available, run this implementation. Otherwise apply it line by line.
Embedded contract JSON:
```json
{
"contract_id": "maestro.linkedin-retargeting-plan.v1",
"contract_version": "1.1.0",
"authoritative_input_rule": "Only fields outside derived are source facts. Discard imported derived values and recompute them.",
"unknown_semantics": "Unknown means evidence is absent. No means an observed negative. Zero means an observed numeric zero. Not applicable requires a selected route where the check truly does not apply.",
"decision_precedence": [
"hold",
"ready_for_operator_review"
],
"routes": {
"website": {
"required": [
"source.route",
"source.qualifying_action",
"source.rule",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"source.insight_tag_state",
"source.insight_tag_evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"video": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.video_view_threshold",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"thresholds": [
25,
50,
75,
97
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"lead_gen_form": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.lead_form_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"opened",
"submitted"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"single_image": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.single_image_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"any_interaction",
"chargeable_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"document": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.document_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"download_or_interaction",
"chargeable_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"conversation": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.conversation_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"opened",
"cta_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"company_page": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.company_page_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"page_view",
"header_cta_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"event": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.event_action",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"clicked_to_attend",
"registered_attended",
"registered_not_attended",
"engaged_not_registered_or_attended"
]
},
"contact_list": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.observed_at",
"source.evidence_ref",
"permission.state",
"permission.evidence_ref",
"permission.owner",
"audience.status",
"audience.status_evidence_ref"
]
},
"capi": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.observed_at",
"source.evidence_ref",
"permission.state",
"permission.evidence_ref",
"permission.owner",
"audience.status",
"audience.status_evidence_ref"
]
}
},
"affirmative_evidence": {
"source": "A current non-placeholder reference to the selected source/action and its observed state.",
"permission": "Explicit approved state, named owner and current non-placeholder approval/reference. Planned, requested, expected, pending or inferred language fails.",
"audience": "Current LinkedIn status reference. Ready proves processing only.",
"suppression": "Named current source, refresh rule, owner, observed yes/no state and non-placeholder reference.",
"delivery": "Actual ad-set delivery read-back with object IDs and observation time. Audience Ready does not satisfy it.",
"outcome": "Actual destination/CRM event read-back with join key and observation time. Delivery does not satisfy it."
},
"stable_requests": {
"REQ-CONTRACT": "Use contract maestro.linkedin-retargeting-plan.v1 version 1.1.0 and migrate the record before review.",
"PLAN-SCOPE": "Supply the business, offer and buyer scope for this plan.",
"PLAN-SOURCE": "Choose one supported source route and name the exact qualifying action.",
"PLAN-MESSAGE": "Supply the next-message job, buyer question, offer and destination.",
"PLAN-OWNERS": "Assign owners for the plan, data permission, message and measurement.",
"PLAN-MEASUREMENT": "Supply the downstream event, CRM key and review trigger.",
"REQ-SOURCE": "Supply the current selected source/action reference and observation date.",
"REQ-TAG": "Supply the current active Insight Tag and website signal receipt.",
"REQ-PERMISSION": "Supply explicit approved data-use evidence, owner and opt-out handling.",
"REQ-AUDIENCE": "Supply the current LinkedIn audience status receipt and matched population state.",
"REQ-SUPPRESSION-COMPLETED": "Supply the completed-step suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-CUSTOMER": "Supply the customer suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-OPPORTUNITY": "Supply the open-opportunity suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-PRIVACY": "Supply the privacy/preference suppression source, refresh rule, owner and current read-back.",
"REQ-MESSAGE": "Supply the next-message job, real proof asset, offer and destination.",
"REQ-MEASUREMENT": "Supply the downstream event, CRM key, owner and review trigger."
},
"launch_boundary": "ready_for_operator_review is not audience creation, upload, spend authorization, campaign launch, delivery or business outcome.",
"broadly_missing_rule": {
"threshold": "Three or more incomplete planning groups",
"groups": {
"scope": [
"business.name",
"business.offer",
"business.buyer_scope"
],
"source": [
"route",
"source.route",
"source.qualifying_action"
],
"message": [
"message.job",
"message.buyer_question",
"message.offer",
"message.destination_url"
],
"owners": [
"business.owner",
"permission.owner",
"message.owner",
"measurement.owner"
],
"measurement": [
"measurement.downstream_event",
"measurement.crm_key",
"measurement.review_trigger"
]
},
"behavior": "Return only the missing PLAN-* requests until fewer than three planning groups are incomplete. Do not ask for account receipts in this branch."
},
"source_route_binding": "source.route must equal the top-level route. Changing route never reuses evidence gathered for another route.",
"timestamp_rule": "source.observed_at and audience.status_observed_at must be calendar-valid timezone-aware ISO 8601 timestamps.",
"evidence_reference_rule": "Evidence references must be present and affirmative. Natural-language denied, pending, planned, requested, expected or unavailable references are not receipts.",
"matched_population_rule": "usable requires an integer matched_accounts_count of at least 300; zero requires count 0; below_threshold requires an integer count from 0 through 299."
}
```
Reference implementation:
```python
#!/usr/bin/env python3
import copy,hashlib,json,re,sys
from datetime import datetime
from pathlib import Path
HERE=Path(__file__).resolve().parent
CONTRACT=json.loads((HERE/'contract.json').read_text())
BAD=re.compile(r'^(?:unknown|not applicable|n/?a|na|pending|unavailable|denied|none|null|tbd|to be determined|planned|requested|expected|placeholder)$',re.I)
NEGATIVE_REF=re.compile(r'\b(?:denied|pending|planned|requested|expected|unavailable|not\s+approved|not\s+available|not\s+complete|will\s+be|to\s+be\s+(?:added|provided|confirmed|verified))\b',re.I)
ACTION_PATHS={'lead_gen_form':'source.lead_form_action','single_image':'source.single_image_action','document':'source.document_action','conversation':'source.conversation_action','company_page':'source.company_page_action','event':'source.event_action'}
SUPPRESSION_IDS={'completed_next_step':'REQ-SUPPRESSION-COMPLETED','customer':'REQ-SUPPRESSION-CUSTOMER','open_opportunity':'REQ-SUPPRESSION-OPPORTUNITY','privacy_preference':'REQ-SUPPRESSION-PRIVACY'}
PLAN_IDS={'scope':'PLAN-SCOPE','source':'PLAN-SOURCE','message':'PLAN-MESSAGE','owners':'PLAN-OWNERS','measurement':'PLAN-MEASUREMENT'}
def present(v):
if v is None or v is False:return False
if isinstance(v,str):return bool(v.strip()) and not BAD.fullmatch(v.strip())
return True
def affirmative_ref(v):return present(v) and not (isinstance(v,str) and NEGATIVE_REF.search(v))
def get(obj,path):
cur=obj
for part in path.split('.'):
if not isinstance(cur,dict):return None
cur=cur.get(part)
return cur
def valid_time(v):
if not isinstance(v,str):return False
try:
parsed=datetime.fromisoformat(v.replace('Z','+00:00'))
return parsed.tzinfo is not None
except ValueError:return False
def req(reqs,id,owner,reason):
if any(x['id']==id for x in reqs):return
reqs.append({'id':id,'owner':owner or 'Owner assignment required','reason':reason,'request':CONTRACT['stable_requests'][id]})
def finish(x,requests):
decision='ready_for_operator_review' if not requests else 'hold'
reasons=[q['reason'] for q in requests] if requests else ['Planning, permission, audience, suppression, message and measurement evidence are complete.']
clean=json.dumps(x,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()
x['derived']={'decision':decision,'reasons':reasons,'requests':requests,'next_action':'An authorized operator reviews the record and performs account-specific launch checks.' if not requests else requests[0]['request'],'input_sha256':hashlib.sha256(clean).hexdigest()}
return x
def evaluate(source):
x=copy.deepcopy(source);x.pop('derived',None);requests=[]
if x.get('contract_id')!=CONTRACT['contract_id'] or x.get('contract_version')!=CONTRACT['contract_version']:
req(requests,'REQ-CONTRACT',get(x,'business.owner'),'The contract identity or version is incompatible.')
group_gaps={name:[p for p in paths if not present(get(x,p))] for name,paths in CONTRACT['broadly_missing_rule']['groups'].items()}
incomplete=[name for name,gaps in group_gaps.items() if gaps]
if len(incomplete)>=3:
for name in ('scope','source','message','owners','measurement'):
if name in incomplete:req(requests,PLAN_IDS[name],get(x,'business.owner'),f'The {name} planning group is incomplete: {", ".join(group_gaps[name])}.')
return finish(x,requests)
route=x.get('route')
if route not in CONTRACT['routes']:
req(requests,'REQ-SOURCE',get(x,'business.owner'),'Choose one supported route and supply its source evidence.')
else:
spec=CONTRACT['routes'][route]
if get(x,'source.route')!=route:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source evidence kind does not match the selected route.')
for field in spec['required']:
value=get(x,field);ok=affirmative_ref(value) if field.endswith('evidence_ref') else present(value)
if not ok:
id='REQ-TAG' if 'insight_tag' in field else 'REQ-PERMISSION' if field.startswith('permission.') else 'REQ-AUDIENCE' if field.startswith('audience.') else 'REQ-SOURCE'
req(requests,id,get(x,'permission.owner') or get(x,'business.owner'),f'Missing or non-affirmative {field}.')
if spec.get('allowed_windows') and get(x,'source.window_days') not in spec['allowed_windows']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source window is not valid for the selected route.')
if spec.get('thresholds') and get(x,'source.video_view_threshold') not in spec['thresholds']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The video threshold is not valid for the selected route.')
if spec.get('actions') and get(x,ACTION_PATHS[route]) not in spec['actions']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source action is not valid for the selected route.')
if route=='website' and get(x,'source.insight_tag_state')!='yes':req(requests,'REQ-TAG',get(x,'business.owner'),'The website route lacks an affirmative active Insight Tag state.')
if not valid_time(get(x,'source.observed_at')):req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source observation time is not a calendar-valid timezone-aware ISO 8601 value.')
if get(x,'permission.state')!='approved' or not affirmative_ref(get(x,'permission.evidence_ref')) or not present(get(x,'permission.owner')) or not present(get(x,'permission.opt_out_process')):req(requests,'REQ-PERMISSION',get(x,'permission.owner'),'Explicit current data-use approval is incomplete.')
if get(x,'audience.status')!='ready' or not affirmative_ref(get(x,'audience.status_evidence_ref')) or get(x,'audience.matched_accounts_state')!='usable' or not valid_time(get(x,'audience.status_observed_at')):req(requests,'REQ-AUDIENCE',get(x,'business.owner'),'LinkedIn Ready status and usable matched-population evidence are incomplete.')
state=get(x,'audience.matched_accounts_state');count=get(x,'audience.matched_accounts_count');count_ok=isinstance(count,int) and not isinstance(count,bool)
if (state=='usable' and (not count_ok or count<300)) or (state=='zero' and count!=0) or (state=='below_threshold' and (not count_ok or not 0<=count<300)):req(requests,'REQ-AUDIENCE',get(x,'business.owner'),'Matched population state and numeric count are inconsistent.')
supplied={s.get('kind'):s for s in x.get('suppressions',[]) if isinstance(s,dict)}
for kind,id in SUPPRESSION_IDS.items():
s=supplied.get(kind,{})
if s.get('state')!='yes' or not all(present(s.get(k)) for k in ('source','refresh_rule','owner')) or not affirmative_ref(s.get('evidence_ref')):req(requests,id,s.get('owner'),f'{kind} suppression lacks affirmative current evidence.')
m=x.get('message',{})
if not all(present(m.get(k)) for k in ('job','buyer_question','proof_asset','offer','destination_url','owner')):req(requests,'REQ-MESSAGE',m.get('owner'),'The next-message handoff is incomplete.')
z=x.get('measurement',{})
if not all(present(z.get(k)) for k in ('downstream_event','crm_key','owner','review_trigger')):req(requests,'REQ-MEASUREMENT',z.get('owner'),'The downstream measurement handoff is incomplete.')
return finish(x,requests)
if __name__=='__main__':
source=json.load(open(sys.argv[1])) if len(sys.argv)>1 else json.load(sys.stdin)
print(json.dumps(evaluate(source),indent=2,ensure_ascii=False))
```
Canonical blank shape:
```json
{
"contract_id": "maestro.linkedin-retargeting-plan.v1",
"contract_version": "1.1.0",
"record_id": null,
"fixture_status": "blank",
"business": {
"name": null,
"offer": null,
"buyer_scope": null,
"owner": null
},
"route": null,
"source": {
"qualifying_action": null,
"rule": null,
"asset_ref": null,
"window_days": null,
"observed_at": null,
"evidence_ref": null,
"insight_tag_state": "unknown",
"insight_tag_evidence_ref": null,
"video_view_threshold": null,
"lead_form_action": null,
"single_image_action": null,
"document_action": null,
"conversation_action": null,
"company_page_action": null,
"event_action": null,
"source_event_count": null,
"route": null
},
"permission": {
"state": "unknown",
"evidence_ref": null,
"owner": null,
"notice_ref": null,
"opt_out_process": null
},
"audience": {
"name": null,
"status": "unknown",
"status_observed_at": null,
"status_evidence_ref": null,
"matched_accounts_state": "unknown",
"matched_accounts_count": null
},
"suppressions": [
{
"kind": "completed_next_step",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "customer",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "open_opportunity",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "privacy_preference",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
}
],
"message": {
"job": null,
"buyer_question": null,
"proof_asset": null,
"offer": null,
"destination_url": null,
"cta": null,
"owner": null
},
"measurement": {
"downstream_event": null,
"crm_key": null,
"owner": null,
"review_trigger": null,
"delivery_state": "unknown",
"delivery_evidence_ref": null,
"business_outcome_state": "unknown",
"business_outcome_evidence_ref": null
}
}
```
Task: turn the pasted business facts into one canonical source-to-message record. Ask only the bounded planning questions in the broadly missing branch. Once the plan is coherent, request exact account evidence.
Return exactly: (1) route and decision; (2) concise readable handoff using the renderer grammar; (3) discrepancies for review or focused planning questions for build; (4) exact stable requests; (5) canonical JSON in one code fence; (6) one exact next action.
PASTE FACTS OR A RECORD BELOW:
Copy the independent review prompt
Contract `maestro.linkedin-retargeting-plan.v1` version `1.1.0`.
Reference summary:
Current first-party summary observed 2026-09-29: LinkedIn documents website, single-image, document, video, Lead Gen Form, Conversation Ad, Company Page, Event and CAPI retargeting sources. Website audiences require the Insight Tag and begin building on creation; they do not backfill earlier site activity. Video thresholds are 25, 50, 75 or 97 percent with documented windows. Source event/view/form counts do not directly equal matched member accounts. Ready means processed for ad-set use, not delivered or commercially successful. EEA/Switzerland matching is subject to member preferences.
Current source: https://www.linkedin.com/help/lms/answer/a427551
Status source: https://www.linkedin.com/help/lms/answer/a795242
Website source: https://www.linkedin.com/help/lms/answer/a420433/
Use only supplied facts. Preserve source facts byte for byte. Never invent audience size, permission, source availability, Ready state, suppression, delivery, outcomes, owner approval or dates. Natural-language denial, pending, planned, requested, expected or unavailable evidence is not affirmative. Ready is only `ready_for_operator_review`; it does not authorize audience creation, upload, spend or launch. Keep source availability, population, delivery and business outcome separate.
Evaluation contract:
- Contract ID `maestro.linkedin-retargeting-plan.v1`, version `1.1.0`. A mismatch emits `REQ-CONTRACT` and cannot be Ready.
- Decision precedence: hold > ready_for_operator_review. This evaluator returns Hold for any request and `ready_for_operator_review` only with none.
- Discard imported derived values before evaluation.
- Source binding: source.route must equal the top-level route. Changing route never reuses evidence gathered for another route.
- Timestamp rule: source.observed_at and audience.status_observed_at must be calendar-valid timezone-aware ISO 8601 timestamps.
- Evidence rule: Evidence references must be present and affirmative. Natural-language denied, pending, planned, requested, expected or unavailable references are not receipts.
- Population rule: usable requires an integer matched_accounts_count of at least 300; zero requires count 0; below_threshold requires an integer count from 0 through 299.
- Unknown, No, numeric Zero and valid Not applicable are distinct. Whole-value aliases Unknown, Not applicable, n/a, na, pending, unavailable, denied, none, null, tbd, to be determined, planned, requested, expected and placeholder are unresolved unless a route explicitly permits the state.
- Broadly missing rule: Three or more incomplete planning groups. Groups are {"scope": ["business.name", "business.offer", "business.buyer_scope"], "source": ["route", "source.route", "source.qualifying_action"], "message": ["message.job", "message.buyer_question", "message.offer", "message.destination_url"], "owners": ["business.owner", "permission.owner", "message.owner", "measurement.owner"], "measurement": ["measurement.downstream_event", "measurement.crm_key", "measurement.review_trigger"]}. In that branch emit only missing PLAN-* requests, in scope/source/message/owners/measurement order, plus REQ-CONTRACT when applicable. Do not ask for account receipts.
- Once fewer than three planning groups are incomplete, validate the selected route, timestamps, affirmative permission, Ready audience and usable count, all four suppressions, message and measurement. Emit requests in contract/source/tag/permission/audience/completed/customer/opportunity/privacy/message/measurement order.
Stable request vocabulary (copy IDs and request text exactly):
- `REQ-CONTRACT`: Use contract maestro.linkedin-retargeting-plan.v1 version 1.1.0 and migrate the record before review.
- `PLAN-SCOPE`: Supply the business, offer and buyer scope for this plan.
- `PLAN-SOURCE`: Choose one supported source route and name the exact qualifying action.
- `PLAN-MESSAGE`: Supply the next-message job, buyer question, offer and destination.
- `PLAN-OWNERS`: Assign owners for the plan, data permission, message and measurement.
- `PLAN-MEASUREMENT`: Supply the downstream event, CRM key and review trigger.
- `REQ-SOURCE`: Supply the current selected source/action reference and observation date.
- `REQ-TAG`: Supply the current active Insight Tag and website signal receipt.
- `REQ-PERMISSION`: Supply explicit approved data-use evidence, owner and opt-out handling.
- `REQ-AUDIENCE`: Supply the current LinkedIn audience status receipt and matched population state.
- `REQ-SUPPRESSION-COMPLETED`: Supply the completed-step suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-CUSTOMER`: Supply the customer suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-OPPORTUNITY`: Supply the open-opportunity suppression source, refresh rule, owner and current read-back.
- `REQ-SUPPRESSION-PRIVACY`: Supply the privacy/preference suppression source, refresh rule, owner and current read-back.
- `REQ-MESSAGE`: Supply the next-message job, real proof asset, offer and destination.
- `REQ-MEASUREMENT`: Supply the downstream event, CRM key, owner and review trigger.
Route contract:
- `website`: required source.route, source.qualifying_action, source.rule, source.window_days, source.observed_at, source.evidence_ref, source.insight_tag_state, source.insight_tag_evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]
- `video`: required source.route, source.qualifying_action, source.asset_ref, source.video_view_threshold, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; thresholds [25, 50, 75, 97]
- `lead_gen_form`: required source.route, source.qualifying_action, source.asset_ref, source.lead_form_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['opened', 'submitted']
- `single_image`: required source.route, source.qualifying_action, source.asset_ref, source.single_image_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['any_interaction', 'chargeable_click']
- `document`: required source.route, source.qualifying_action, source.asset_ref, source.document_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['download_or_interaction', 'chargeable_click']
- `conversation`: required source.route, source.qualifying_action, source.asset_ref, source.conversation_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['opened', 'cta_click']
- `company_page`: required source.route, source.qualifying_action, source.asset_ref, source.company_page_action, source.window_days, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; allowed windows [30, 60, 90, 180, 365]; actions ['page_view', 'header_cta_click']
- `event`: required source.route, source.qualifying_action, source.asset_ref, source.event_action, source.observed_at, source.evidence_ref, audience.status, audience.status_evidence_ref; actions ['clicked_to_attend', 'registered_attended', 'registered_not_attended', 'engaged_not_registered_or_attended']
- `contact_list`: required source.route, source.qualifying_action, source.asset_ref, source.observed_at, source.evidence_ref, permission.state, permission.evidence_ref, permission.owner, audience.status, audience.status_evidence_ref
- `capi`: required source.route, source.qualifying_action, source.asset_ref, source.observed_at, source.evidence_ref, permission.state, permission.evidence_ref, permission.owner, audience.status, audience.status_evidence_ref
Readable Markdown renderer grammar, in this exact section order:
1. H1 `LinkedIn retargeting source-to-message handoff`.
2. `Record and decision`: Contract ID, Contract version, Record ID, Fixture status, Decision, Input SHA-256.
3. `Business and route`: Business, Offer, Buyer scope, Plan owner, Route, Source evidence kind.
4. `Source`: Qualifying action, Source rule, Source asset or list reference, Window days, Observed at, Source evidence reference, Insight Tag state, Insight Tag evidence reference, Video view threshold, Lead Form action, Single-image action, Document action, Conversation action, Company Page action, Event action, Source event count.
5. `Permission`: State, Evidence reference, Owner, Notice reference, Opt-out process.
6. `Audience`: Audience name, Status, Status observed at, Status evidence reference, Matched population state, Matched account count.
7. `Suppressions`: one H3 per supplied suppression, named by `kind`, with State, Source, Refresh rule, Owner and Evidence reference. If no suppression array is supplied, write `No suppression records supplied.`
8. `Next message`: Job, Buyer question, Proof asset, Offer, Destination URL, CTA, Owner.
9. `Measurement`: Downstream event, CRM key, Owner, Review trigger, Delivery state, Delivery evidence reference, Business outcome state, Business outcome evidence reference.
10. `Decision reasons`: one bullet per `derived.reasons` value in evaluator order; if absent, write `Unknown`.
11. `Evidence requests`: one bullet per request as `**ID** — owner: exact request Reason: exact reason`; if none, write `No planning evidence requests remain. Account-specific launch checks still apply.`
12. `Next action`: copy `derived.next_action` exactly.
For every label above, render the corresponding canonical field even when it is null, zero, false, Unknown, No or Not applicable. Render null or missing as `Unknown`; render numeric zero as `0`; never drop an off-route field. Escape literal `|` as `\|`. This readable handoff is complete field-semantic output, not a selected summary.
Canonical JSON renderer rules:
- Preserve every supplied non-derived field and value exactly. Do not silently migrate a mismatched contract.
- Remove imported `derived`, recompute it, then append keys in this order: decision, reasons, requests, next_action, input_sha256.
- Request objects use keys in this order: id, owner, reason, request. Deduplicate by ID and retain evaluator order.
- Compute `input_sha256` from UTF-8 JSON of the non-derived record with recursively sorted object keys, compact separators and unescaped Unicode.
- Ready next action is exactly `An authorized operator reviews the record and performs account-specific launch checks.`
- Hold next action is exactly the first request's `request`.
Normative standalone evaluator implementation follows. This is the complete validation-to-request mapping, reason vocabulary, owner selection, fallback rule, deduplication behavior and hash algorithm. Use the embedded contract JSON as `CONTRACT`. Do not summarize, rename or improvise any request object. If code execution is available, run this implementation. Otherwise apply it line by line.
Embedded contract JSON:
```json
{
"contract_id": "maestro.linkedin-retargeting-plan.v1",
"contract_version": "1.1.0",
"authoritative_input_rule": "Only fields outside derived are source facts. Discard imported derived values and recompute them.",
"unknown_semantics": "Unknown means evidence is absent. No means an observed negative. Zero means an observed numeric zero. Not applicable requires a selected route where the check truly does not apply.",
"decision_precedence": [
"hold",
"ready_for_operator_review"
],
"routes": {
"website": {
"required": [
"source.route",
"source.qualifying_action",
"source.rule",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"source.insight_tag_state",
"source.insight_tag_evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"video": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.video_view_threshold",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"thresholds": [
25,
50,
75,
97
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"lead_gen_form": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.lead_form_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"opened",
"submitted"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"single_image": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.single_image_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"any_interaction",
"chargeable_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"document": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.document_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"download_or_interaction",
"chargeable_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"conversation": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.conversation_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"opened",
"cta_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"company_page": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.company_page_action",
"source.window_days",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"page_view",
"header_cta_click"
],
"allowed_windows": [
30,
60,
90,
180,
365
]
},
"event": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.event_action",
"source.observed_at",
"source.evidence_ref",
"audience.status",
"audience.status_evidence_ref"
],
"actions": [
"clicked_to_attend",
"registered_attended",
"registered_not_attended",
"engaged_not_registered_or_attended"
]
},
"contact_list": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.observed_at",
"source.evidence_ref",
"permission.state",
"permission.evidence_ref",
"permission.owner",
"audience.status",
"audience.status_evidence_ref"
]
},
"capi": {
"required": [
"source.route",
"source.qualifying_action",
"source.asset_ref",
"source.observed_at",
"source.evidence_ref",
"permission.state",
"permission.evidence_ref",
"permission.owner",
"audience.status",
"audience.status_evidence_ref"
]
}
},
"affirmative_evidence": {
"source": "A current non-placeholder reference to the selected source/action and its observed state.",
"permission": "Explicit approved state, named owner and current non-placeholder approval/reference. Planned, requested, expected, pending or inferred language fails.",
"audience": "Current LinkedIn status reference. Ready proves processing only.",
"suppression": "Named current source, refresh rule, owner, observed yes/no state and non-placeholder reference.",
"delivery": "Actual ad-set delivery read-back with object IDs and observation time. Audience Ready does not satisfy it.",
"outcome": "Actual destination/CRM event read-back with join key and observation time. Delivery does not satisfy it."
},
"stable_requests": {
"REQ-CONTRACT": "Use contract maestro.linkedin-retargeting-plan.v1 version 1.1.0 and migrate the record before review.",
"PLAN-SCOPE": "Supply the business, offer and buyer scope for this plan.",
"PLAN-SOURCE": "Choose one supported source route and name the exact qualifying action.",
"PLAN-MESSAGE": "Supply the next-message job, buyer question, offer and destination.",
"PLAN-OWNERS": "Assign owners for the plan, data permission, message and measurement.",
"PLAN-MEASUREMENT": "Supply the downstream event, CRM key and review trigger.",
"REQ-SOURCE": "Supply the current selected source/action reference and observation date.",
"REQ-TAG": "Supply the current active Insight Tag and website signal receipt.",
"REQ-PERMISSION": "Supply explicit approved data-use evidence, owner and opt-out handling.",
"REQ-AUDIENCE": "Supply the current LinkedIn audience status receipt and matched population state.",
"REQ-SUPPRESSION-COMPLETED": "Supply the completed-step suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-CUSTOMER": "Supply the customer suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-OPPORTUNITY": "Supply the open-opportunity suppression source, refresh rule, owner and current read-back.",
"REQ-SUPPRESSION-PRIVACY": "Supply the privacy/preference suppression source, refresh rule, owner and current read-back.",
"REQ-MESSAGE": "Supply the next-message job, real proof asset, offer and destination.",
"REQ-MEASUREMENT": "Supply the downstream event, CRM key, owner and review trigger."
},
"launch_boundary": "ready_for_operator_review is not audience creation, upload, spend authorization, campaign launch, delivery or business outcome.",
"broadly_missing_rule": {
"threshold": "Three or more incomplete planning groups",
"groups": {
"scope": [
"business.name",
"business.offer",
"business.buyer_scope"
],
"source": [
"route",
"source.route",
"source.qualifying_action"
],
"message": [
"message.job",
"message.buyer_question",
"message.offer",
"message.destination_url"
],
"owners": [
"business.owner",
"permission.owner",
"message.owner",
"measurement.owner"
],
"measurement": [
"measurement.downstream_event",
"measurement.crm_key",
"measurement.review_trigger"
]
},
"behavior": "Return only the missing PLAN-* requests until fewer than three planning groups are incomplete. Do not ask for account receipts in this branch."
},
"source_route_binding": "source.route must equal the top-level route. Changing route never reuses evidence gathered for another route.",
"timestamp_rule": "source.observed_at and audience.status_observed_at must be calendar-valid timezone-aware ISO 8601 timestamps.",
"evidence_reference_rule": "Evidence references must be present and affirmative. Natural-language denied, pending, planned, requested, expected or unavailable references are not receipts.",
"matched_population_rule": "usable requires an integer matched_accounts_count of at least 300; zero requires count 0; below_threshold requires an integer count from 0 through 299."
}
```
Reference implementation:
```python
#!/usr/bin/env python3
import copy,hashlib,json,re,sys
from datetime import datetime
from pathlib import Path
HERE=Path(__file__).resolve().parent
CONTRACT=json.loads((HERE/'contract.json').read_text())
BAD=re.compile(r'^(?:unknown|not applicable|n/?a|na|pending|unavailable|denied|none|null|tbd|to be determined|planned|requested|expected|placeholder)$',re.I)
NEGATIVE_REF=re.compile(r'\b(?:denied|pending|planned|requested|expected|unavailable|not\s+approved|not\s+available|not\s+complete|will\s+be|to\s+be\s+(?:added|provided|confirmed|verified))\b',re.I)
ACTION_PATHS={'lead_gen_form':'source.lead_form_action','single_image':'source.single_image_action','document':'source.document_action','conversation':'source.conversation_action','company_page':'source.company_page_action','event':'source.event_action'}
SUPPRESSION_IDS={'completed_next_step':'REQ-SUPPRESSION-COMPLETED','customer':'REQ-SUPPRESSION-CUSTOMER','open_opportunity':'REQ-SUPPRESSION-OPPORTUNITY','privacy_preference':'REQ-SUPPRESSION-PRIVACY'}
PLAN_IDS={'scope':'PLAN-SCOPE','source':'PLAN-SOURCE','message':'PLAN-MESSAGE','owners':'PLAN-OWNERS','measurement':'PLAN-MEASUREMENT'}
def present(v):
if v is None or v is False:return False
if isinstance(v,str):return bool(v.strip()) and not BAD.fullmatch(v.strip())
return True
def affirmative_ref(v):return present(v) and not (isinstance(v,str) and NEGATIVE_REF.search(v))
def get(obj,path):
cur=obj
for part in path.split('.'):
if not isinstance(cur,dict):return None
cur=cur.get(part)
return cur
def valid_time(v):
if not isinstance(v,str):return False
try:
parsed=datetime.fromisoformat(v.replace('Z','+00:00'))
return parsed.tzinfo is not None
except ValueError:return False
def req(reqs,id,owner,reason):
if any(x['id']==id for x in reqs):return
reqs.append({'id':id,'owner':owner or 'Owner assignment required','reason':reason,'request':CONTRACT['stable_requests'][id]})
def finish(x,requests):
decision='ready_for_operator_review' if not requests else 'hold'
reasons=[q['reason'] for q in requests] if requests else ['Planning, permission, audience, suppression, message and measurement evidence are complete.']
clean=json.dumps(x,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()
x['derived']={'decision':decision,'reasons':reasons,'requests':requests,'next_action':'An authorized operator reviews the record and performs account-specific launch checks.' if not requests else requests[0]['request'],'input_sha256':hashlib.sha256(clean).hexdigest()}
return x
def evaluate(source):
x=copy.deepcopy(source);x.pop('derived',None);requests=[]
if x.get('contract_id')!=CONTRACT['contract_id'] or x.get('contract_version')!=CONTRACT['contract_version']:
req(requests,'REQ-CONTRACT',get(x,'business.owner'),'The contract identity or version is incompatible.')
group_gaps={name:[p for p in paths if not present(get(x,p))] for name,paths in CONTRACT['broadly_missing_rule']['groups'].items()}
incomplete=[name for name,gaps in group_gaps.items() if gaps]
if len(incomplete)>=3:
for name in ('scope','source','message','owners','measurement'):
if name in incomplete:req(requests,PLAN_IDS[name],get(x,'business.owner'),f'The {name} planning group is incomplete: {", ".join(group_gaps[name])}.')
return finish(x,requests)
route=x.get('route')
if route not in CONTRACT['routes']:
req(requests,'REQ-SOURCE',get(x,'business.owner'),'Choose one supported route and supply its source evidence.')
else:
spec=CONTRACT['routes'][route]
if get(x,'source.route')!=route:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source evidence kind does not match the selected route.')
for field in spec['required']:
value=get(x,field);ok=affirmative_ref(value) if field.endswith('evidence_ref') else present(value)
if not ok:
id='REQ-TAG' if 'insight_tag' in field else 'REQ-PERMISSION' if field.startswith('permission.') else 'REQ-AUDIENCE' if field.startswith('audience.') else 'REQ-SOURCE'
req(requests,id,get(x,'permission.owner') or get(x,'business.owner'),f'Missing or non-affirmative {field}.')
if spec.get('allowed_windows') and get(x,'source.window_days') not in spec['allowed_windows']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source window is not valid for the selected route.')
if spec.get('thresholds') and get(x,'source.video_view_threshold') not in spec['thresholds']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The video threshold is not valid for the selected route.')
if spec.get('actions') and get(x,ACTION_PATHS[route]) not in spec['actions']:req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source action is not valid for the selected route.')
if route=='website' and get(x,'source.insight_tag_state')!='yes':req(requests,'REQ-TAG',get(x,'business.owner'),'The website route lacks an affirmative active Insight Tag state.')
if not valid_time(get(x,'source.observed_at')):req(requests,'REQ-SOURCE',get(x,'business.owner'),'The source observation time is not a calendar-valid timezone-aware ISO 8601 value.')
if get(x,'permission.state')!='approved' or not affirmative_ref(get(x,'permission.evidence_ref')) or not present(get(x,'permission.owner')) or not present(get(x,'permission.opt_out_process')):req(requests,'REQ-PERMISSION',get(x,'permission.owner'),'Explicit current data-use approval is incomplete.')
if get(x,'audience.status')!='ready' or not affirmative_ref(get(x,'audience.status_evidence_ref')) or get(x,'audience.matched_accounts_state')!='usable' or not valid_time(get(x,'audience.status_observed_at')):req(requests,'REQ-AUDIENCE',get(x,'business.owner'),'LinkedIn Ready status and usable matched-population evidence are incomplete.')
state=get(x,'audience.matched_accounts_state');count=get(x,'audience.matched_accounts_count');count_ok=isinstance(count,int) and not isinstance(count,bool)
if (state=='usable' and (not count_ok or count<300)) or (state=='zero' and count!=0) or (state=='below_threshold' and (not count_ok or not 0<=count<300)):req(requests,'REQ-AUDIENCE',get(x,'business.owner'),'Matched population state and numeric count are inconsistent.')
supplied={s.get('kind'):s for s in x.get('suppressions',[]) if isinstance(s,dict)}
for kind,id in SUPPRESSION_IDS.items():
s=supplied.get(kind,{})
if s.get('state')!='yes' or not all(present(s.get(k)) for k in ('source','refresh_rule','owner')) or not affirmative_ref(s.get('evidence_ref')):req(requests,id,s.get('owner'),f'{kind} suppression lacks affirmative current evidence.')
m=x.get('message',{})
if not all(present(m.get(k)) for k in ('job','buyer_question','proof_asset','offer','destination_url','owner')):req(requests,'REQ-MESSAGE',m.get('owner'),'The next-message handoff is incomplete.')
z=x.get('measurement',{})
if not all(present(z.get(k)) for k in ('downstream_event','crm_key','owner','review_trigger')):req(requests,'REQ-MEASUREMENT',z.get('owner'),'The downstream measurement handoff is incomplete.')
return finish(x,requests)
if __name__=='__main__':
source=json.load(open(sys.argv[1])) if len(sys.argv)>1 else json.load(sys.stdin)
print(json.dumps(evaluate(source),indent=2,ensure_ascii=False))
```
Canonical blank shape:
```json
{
"contract_id": "maestro.linkedin-retargeting-plan.v1",
"contract_version": "1.1.0",
"record_id": null,
"fixture_status": "blank",
"business": {
"name": null,
"offer": null,
"buyer_scope": null,
"owner": null
},
"route": null,
"source": {
"qualifying_action": null,
"rule": null,
"asset_ref": null,
"window_days": null,
"observed_at": null,
"evidence_ref": null,
"insight_tag_state": "unknown",
"insight_tag_evidence_ref": null,
"video_view_threshold": null,
"lead_form_action": null,
"single_image_action": null,
"document_action": null,
"conversation_action": null,
"company_page_action": null,
"event_action": null,
"source_event_count": null,
"route": null
},
"permission": {
"state": "unknown",
"evidence_ref": null,
"owner": null,
"notice_ref": null,
"opt_out_process": null
},
"audience": {
"name": null,
"status": "unknown",
"status_observed_at": null,
"status_evidence_ref": null,
"matched_accounts_state": "unknown",
"matched_accounts_count": null
},
"suppressions": [
{
"kind": "completed_next_step",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "customer",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "open_opportunity",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
},
{
"kind": "privacy_preference",
"source": null,
"refresh_rule": null,
"owner": null,
"state": "unknown",
"evidence_ref": null
}
],
"message": {
"job": null,
"buyer_question": null,
"proof_asset": null,
"offer": null,
"destination_url": null,
"cta": null,
"owner": null
},
"measurement": {
"downstream_event": null,
"crm_key": null,
"owner": null,
"review_trigger": null,
"delivery_state": "unknown",
"delivery_evidence_ref": null,
"business_outcome_state": "unknown",
"business_outcome_evidence_ref": null
}
}
```
Task: independently review and correct the pasted source-to-message record. Use the same broadly missing branch. Report every discrepancy before the corrected record.
Return exactly: (1) route and decision; (2) concise readable handoff using the renderer grammar; (3) discrepancies for review or focused planning questions for build; (4) exact stable requests; (5) canonical JSON in one code fence; (6) one exact next action.
PASTE RECORD AND RECEIPTS BELOW:
The planner should produce one record with:
- Source type and qualifying action
- Source prerequisite and observation date
- Window and audience status
- Population state
- Suppressions and their evidence
- Message job, proof asset and offer
- Destination and downstream event
- CRM key and owner
- Delivery and outcome states
- Stable evidence requests
- Decision and next action
The readable Markdown and structured JSON should preserve the same values. Unknown, No, Zero and Not applicable are different values. Zero matched members is an observed number. Unknown means the evidence is missing. Not applicable needs a real route-specific reason.
A complete record can become Ready for operator review. Launch still requires the actual account, permission, audience, destination, suppression and approval checks. If any critical fact is missing, the planner should keep the decision on Hold and state exactly who must supply what.
LinkedIn retargeting checklist
Before you hand the plan to an operator, confirm:
- One observed action maps to one supported LinkedIn source route.
- The source prerequisite and current account evidence are recorded.
- The selected action or rule and window are explicit.
- The audience status and population evidence are current.
- Event, visit, view or form counts are not treated as matched population.
- Completed-step, customer, open-opportunity and privacy suppressions have real sources and owners.
- The next message follows from the action and leads to a continuing destination.
- Public creative examples are treated as message examples, not proof of retargeting or results.
- The downstream event, CRM key, owner and review trigger are named.
- Delivery and business outcome remain Unknown until their own evidence exists.
- Missing facts produce a Hold decision with focused requests.
- Review does not create an audience, authorize spend or launch a campaign.
If you can check every applicable item, the plan is ready for operator review. If you cannot, keep it on Hold. Give the next person a record that states exactly what is known, what is missing and what to do next.