Marketing

When the Ad Schema Comes Back Empty: A Prompt That Skips a Row

Antonio Evans10 min read

A three-step ladder diagram: an ad row with empty fields at the bottom, a skip decision box with a sentinel and reason code in the middle, and a human reviewer with a queue of skipped rows at the top.
Key points

Key points

  1. 1Write an explicit skip instruction into the extraction prompt: when the required ad schema fields are absent, the model must return a fixed sentinel object with a reason code instead of an empty object or a guessed value.
  2. 2Set a confidence threshold and force a skip below it, so low-certainty extractions reach a reviewer rather than a scoring job.
  3. 3Place the skip in the fallback ladder as a third branch alongside auto-accept and auto-reject, and route every skip to a human worklist.
  4. 4Log the reason code, the raw model output, the source URL and the fetched HTML at the skip point, because that record is what tells you whether to fix a selector, the schema or the fetch.

Why an empty object is worse than no row at all

An extraction prompt for ad data usually asks for a set of fields: headline, body, call to action, price, landing page URL, and the audience or offer the ad promises. Message match auditing depends on exactly those fields, because the review asks whether a destination page continues the same promise across product, offer, proof and next action that the ad made the message match checklist. If the extractor returns an empty object, downstream code has a row with a schema shape but no content.

That row is dangerous in three ways. A scoring job that compares ad promise to page headline will compute a similarity against an empty string. A deduplication step may treat all empty rows as one cluster. A dashboard counting coverage will show 100 percent extraction success because every input produced valid JSON.

None of those failures announces itself. The extraction reports success at the syntactic level and fails at the semantic level. This is a known pattern with human-oriented JSON contracts: when a schema contains ambiguous or incomplete specifications, models struggle with entity boundaries and hallucinate values or fail to adhere to the schema the PARSE paper. The paper's response is to treat the schema itself as an LLM contract and to add reflection-based extraction with static and LLM-based guardrails, reporting that combined framework improvements reduce extraction errors within the first retry the PARSE paper. The skip instruction is the cheapest version of that guardrail you can ship today.

There is a second reason to care. Any audit built on ad copy is making factual claims about what advertisers promised, and a guessed price or a guessed discount contaminates that record. Regulators treat omissions as material when they leave a consumer with a misimpression the FTC advertising guide. If your extractor fills a missing price with a plausible number, your downstream analysis is now making claims that no page supports. A skip is the honest output.

The three parts of a skip instruction

A skip instruction has three moving parts. Write them separately, then assemble.

1. A fixed sentinel. Choose one exact JSON object that can only mean "no row." It must be syntactically distinct from a legitimate extraction, so a validator can branch on it without heuristics. Keep the key set small and frozen.

{"status":"skip","reason":"<reason_code>","missing":["price"],"confidence":0.0}

2. A confidence threshold. Ask the model to report a confidence for the whole row, not per field, and give it a number below which it must skip. Thresholds are only meaningful if you calibrate them, and calibration requires reviewed samples. Start with a value you can defend, then adjust using your own reviewer verdicts rather than a rule of thumb copied from a blog.

3. A reason code from a closed list. Closed lists are what make the skip data countable. If the model may invent a reason string, you get three hundred spellings of "missing price" and no way to group them. Compare this to the field-extraction discipline used in bulk copy analysis, where each ad is broken into fixed enumerated fields with an explicit unknown value and models are told not to invent values the ad clustering walkthrough.

A practical starting list:

Reason codeMeaningTypical root cause
missing_requiredOne or more required ad fields absent from the pageSelector drift or a layout the schema does not cover
low_confidenceFields present but the model is below thresholdAmbiguous markup, split elements, unusual copy
ambiguous_valueTwo conflicting candidates for one fieldMultiple price elements, A/B variants in the same HTML
blocked_or_emptyPage body empty or a bot wallFetch failure, consent gate, client-side render
schema_gapField exists in human terms but the schema has no slotOffer types the model does not describe, such as "free trial plus setup fee"
off_domainDestination redirected somewhere unrelatedAd URL expired or cloaked

The distinction between blocked_or_empty and missing_required is the single most useful split in the table. One is an infrastructure problem, the other is a modeling problem, and they route to different people.

Wiring the instruction into the prompt

Write the skip rule as a hard constraint with a negative example. Models follow rules best when the forbidden output is shown next to the required one. The Perplexity agent prompt guide makes the same point in a different context: put role, formatting and grounding rules in the instructions parameter and keep hard constraints out of prose where a request parameter can enforce them the Perplexity prompt guide. Your equivalent of a request parameter is a schema-validated response format, which should reject anything that is not a valid extraction or a valid sentinel.

A minimal prompt block:

You extract ad creative from one landing page.

Required fields: headline, body, cta_text, price, destination_url.
Every required field must be supported by text visible on the page.

Rules:
1. If any required field cannot be supported by visible text,
   return exactly:
   {"status":"skip","reason":"<code>","missing":[...],"confidence":0.0}
   and nothing else.
2. reason must be one of: missing_required, low_confidence,
   ambiguous_value, blocked_or_empty, schema_gap, off_domain.
3. If your overall confidence is below 0.7, skip with
   reason "low_confidence".
4. Never guess a price, discount, or URL. Never return an empty
   string for a required field. Never return an empty object.

Forbidden: {"headline":"","price":null}
Required when price is absent: {"status":"skip","reason":"missing_required","missing":["price"],"confidence":0.0}

Three details matter more than they look. First, "supported by text visible on the page" gives the reviewer something to check against. Second, the forbidden example is the exact failure you have probably already seen in production. Third, rule 3 puts the threshold in the prompt rather than in a post-processing step, so skips are labeled with the reason the model itself believed, which turns out to be more diagnostic than a post-hoc score.

Validate the sentinel in code, not just in the prompt:

SKIP = {"status": "skip"}

def classify(raw: dict, schema_required: set[str]) -> str:
    if raw.get("status") == "skip":
        return "human"
    if not schema_required.issubset(raw.keys()):
        return "human"
    if any(raw[k] in (None, "", []) for k in schema_required):
        return "human"
    return "accept"

Returning human for a malformed non-sentinel output is deliberate. A response that is neither a valid extraction nor a valid skip is a fourth state the prompt did not anticipate, and it belongs in the same queue, tagged separately.

Placing the skip in the fallback ladder

Most extraction pipelines have two branches: auto-accept above threshold, auto-reject below it. That ladder has a hole. Auto-reject silently discards a row that a human could resolve in seconds, and it destroys the evidence that something changed on the page.

The three-branch ladder fixes the hole:

BranchTriggerActionWho owns it
Auto-acceptValid extraction, confidence at or above threshold, all required fields presentWrite to the scored tableNobody, until sampling
Auto-rejectOff-domain, duplicate, or blocked page that fails twiceDrop and countData engineering
HumanSentinel returned, malformed output, or confidence below thresholdQueue with full contextReview team

The decision rule for where a case lands is not confidence alone. Ask one question: can a person with the page in front of them produce the correct row in under a minute? If yes, route to human. If no, because the page is gone or the ad was pulled, auto-reject and count it. Getting this wrong is how a review queue fills with unresolvable items and reviewers stop trusting it.

Human review as a formal step is not unusual in extraction systems. Appian's extraction documentation describes a user reconciliation task as part of the extraction process, where a person manually confirms the data was extracted correctly the Appian extraction docs. The same docs advise giving the model a lot of context, comparing prompt design to onboarding a new employee with examples and guidelines the Appian extraction docs. That framing is useful for the skip instruction too: a reviewer needs the model's examples of what "missing" looks like, not just the word.

What to log at the skip point

The skip record is the product of this whole design. Log enough at the skip point that a reviewer can act without opening a terminal, and that an engineer can debug without asking the reviewer what they saw.

For the reviewer: the source URL, the reason code, the list of missing fields, a screenshot or the fetched HTML, the model's raw output, and the ad identifier. That is a complete worklist entry.

For the engineer: the extractor build, the prompt hash, the selector version, the HTTP status and final URL after redirects, and the byte size of the fetched HTML. The last two catch the most common silent failure, which is a redirect to a consent page that returns 200 and no ad.

The pydantic human review documentation offers a pattern worth copying: an annotation queue gives reviewers a curated list of runs to inspect one after another rather than asking them to find runs manually, and tagging verdicts with labels such as tool error or hallucination helps reviewers find related cases the Logfire human review guide. Your reason codes are those tags. Keep the queue sorted by reason code and by frequency, not by arrival time, so reviewers see runs of similar problems and get faster at them.

One caution from the same source: comments on reviewed runs can contain user data and should be treated like other production data the Logfire human review guide. Ad copy and page HTML can include personal data in testimonials and review widgets. Store the fetched HTML under the same retention rules as any other scraped content.

The numbers: what a skip rate tells you

Suppose a batch of 10,000 ad URLs and landing pages. The extractor returns 7,400 clean rows, 2,100 skips, and 500 malformed outputs that get queued as well. That is a 26 percent human rate, which sounds alarming until you break it down by reason code:

ReasonCountShare of skipsOwnerFirst fix
missing_required1,15044%Extraction engineeringSelector update for the price block
blocked_or_empty59023%Data engineeringRetry with rendering, rotate fetch
low_confidence26010%Prompt ownerTighten field definitions
ambiguous_value1405%Extraction engineeringAdd a tie-break rule
schema_gap1205%Schema ownerExtend the offer model
off_domain34013%Campaign opsPrune dead ad URLs

Read this table and the priority is obvious. The single largest bucket is one selector family, and fixing it removes about 11.5 percent of the batch from the queue. The schema_gap bucket is small but the most strategically interesting, because each entry is a promise the model cannot express. Schema.org's Offer type carries dozens of properties for exactly this reason, including availability windows, advance booking requirements and delivery methods the Schema.org Offer type. If your schema has three price fields and the market uses twelve, the gap bucket is telling you where to extend.

Two derived rates are worth putting on a dashboard: skips per 1,000 URLs (tracks extractor health over time) and human resolution rate within 24 hours (tracks whether the queue is actually working). The first moving up without a corresponding rise in blocked_or_empty almost always means a source site changed its markup.

Google's own guidance on structured data is a useful reminder that machine-readable claims about a page should be validated rather than assumed, and it points to the Rich Results Test and the Schema Markup Validator as the tools for checking markup Google Search Central on structured data. Apply the same logic to your extracted rows: validate before you score.

Where this fits with running many models

Extraction at ad-library scale means a lot of calls across a lot of pages, and the temptation is to standardize on one model and one prompt. The skip instruction argues for the opposite in one narrow respect: you want the freedom to swap extractors and compare skip rates. If the sentinel and reason codes are frozen, an A/B between two models becomes a comparison of reason-code distributions, which is a far more useful signal than an aggregate accuracy number.

If you are running several extractors and a reviewer queue, Quanta Inference puts text, image and embedding models behind one OpenAI-compatible API key, which keeps the model swap a config change rather than a re-integration.

And when the skip data starts pointing at schema decisions rather than selectors, that is an analysis job. Quanta Experts reads your context and hands back finished deliverables, which suits the "read three months of skip records and tell me what to change" task better than another dashboard.

A checklist before you ship

Run through this on the prompt, the validator and the queue before the next batch.

CheckPass condition
Sentinel is exactOne frozen object, no optional keys, documented next to the schema
Reason codes are closedEnumerated in the prompt and rejected in the validator if unknown
Threshold is stated in the promptRule 3 in the instruction, not only in post-processing
Negative example presentThe empty-object failure you have already seen is shown as forbidden
Malformed output routes to humanNot dropped, not retried into the same failure
Skip record carries URL and raw outputReviewer can act without a terminal
Reason codes tagged in the queueReviewers can sort by cause, not arrival
Skip rate on a dashboardSplit by reason code, not a single number

The last row is the one teams skip. A single skip rate is a mood. A skip rate broken out by reason code is a work plan, and it is the only part of this pipeline that gets cheaper to run each month.

FAQ

FAQ

What should the sentinel object contain?

A fixed status key, a reason code from a closed list, the names of the missing fields, and a confidence value. Keep it minimal and freeze it, because downstream validators branch on its exact shape. Never include a guessed value inside the sentinel, even in a hint field, because someone will eventually read that field as data.

Why not just return an empty object and filter it downstream?

Because every step between extraction and scoring treats the row as real. Similarity scores get computed against empty strings, coverage dashboards report full success, and the evidence that a page changed is discarded. A sentinel preserves the fact that extraction was attempted and why it failed the PARSE paper, which is the information you need to fix the cause.

How do I pick the confidence threshold?

Calibrate it against reviewed rows rather than guessing. Take a sample the reviewers have already judged, plot confidence against their verdicts, and set the threshold where the false-accept rate is a level you can defend. Until you have that sample, keep the threshold conservative and expect a larger queue, which is cheaper than a wrong scored table.

Should skips go to the same queue as low-confidence extractions?

Same queue, different tags. A single queue with reason-code tags lets reviewers work in runs of similar problems, which is what makes them fast. Separate queues fragment the picture and usually end with one of them unread the Logfire human review guide.

Does the skip instruction replace schema work?

No, it buys time while you do it. Repeated schema_gap skips are the signal that your ad schema cannot describe what advertisers actually promise, and offer structures vary enough that you will keep finding gaps the Schema.org Offer type. Treat the skip log as the requirements document for the next schema revision.

Sources

Sources

  1. Ad-to-Landing-Page Message Match Checklist
  2. PARSE: LLM Driven Schema Optimization for Reliable ...
  3. Advertising FAQ's: A Guide for Small Business
  4. Feed 300 Competitor Ads to a Model and You Get Four ...
  5. Prompt Guide
  6. Extraction Models DocCenter v3.1
  7. Review AI outputs by hand | Pydantic Docs
  8. Offer - Schema.org Type
  9. Schema Markup Testing Tool | Google Search Central
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles