← all posts

The Pipeline Reported Success. Every Single Result Was Empty.

May 2026


title: "The Pipeline Reported Success. Every Single Result Was Empty."

author: Andrew Katana

status: draft

created: 2026-07-30

tags: [AI, automation, debugging, silent failures, SPA, scrapers, production]


My job pipeline ran this morning. It scraped LinkedIn, fetched thirty job postings, and printed a checkmark next to every single one.

Thirty successes. Zero usable data.

No errors. No warnings. No failed requests. The pipeline did exactly what it was built to do — and every result it produced was worthless. The checkmark was a lie.

Here's how that happens, why it's more dangerous than a crash, and what I changed to make sure it can't silently succeed again.

The failure that looked like a win

The pipeline has stages: scrape search results, fetch full job descriptions, score them against a CV, generate tailored applications, email the digest. Every stage logs its output. When the detail-fetch stage ran, it logged this for each of the thirty jobs:


✓ | 0 chars
        

A checkmark. A company name that was a question mark. A description that was zero characters long.

Thirty checkmarks. Thirty empty records. The pipeline finished, saved its output, and moved on — ranking "jobs" with no descriptions, "tailoring CVs" for companies it couldn't name.

I almost didn't catch it. The run looked successful. Nothing crashed. Nothing timed out. It was only when I opened the output file and saw `0 chars` repeated thirty times that I realized the entire stage was producing garbage.

The first suspect was innocent

I assumed the extraction code broke. The company field had been wrong before — a parsing bug that grabbed "Full-time" as the company name. So I rewrote the extractor to pull from LinkedIn's embedded JSON-LD structured data. `hiringOrganization.name`. Clean, typed, reliable. The industry-standard way to read job data.

The rewrite was pointless. LinkedIn had removed the JSON-LD entirely. There was nothing to read. The standard, documented approach simply no longer existed — with no announcement, no deprecation warning, no migration path. One day it was there. The next it wasn't.

That's the first lesson, and it's worth stating plainly: if your automation depends on an undocumented interface, you don't have a feature. You have a liability with a fuse. It worked until it didn't, and there was no way to know in advance.

The load event that lied

With structured data gone, I fell back to parsing the rendered page text. And here's where it got interesting.

The code navigated to a job page, waited for the browser's `frameStoppedLoading` event, then read the page content. The event fired. The browser declared the page loaded. Then the extractor read... nothing. Empty text. An unrendered shell.

The browser was telling the truth about loading. It was lying about readiness.

LinkedIn is a single-page application. The browser loads the HTML shell, fires `frameStoppedLoading`, and then the JavaScript framework renders the actual content seconds later. My code trusted the load event as a proxy for "content is ready." The proxy broke. `frameStoppedLoading` doesn't mean rendered. It means the network transfer finished. For a React app, those are two completely different moments.

The load event is not the content. I learned this the way everyone learns it: the hard way, in production.

The fix: stop trusting events, start asserting content

I changed the extractor to stop waiting for events and start checking for what it actually needs. After navigating to a job page, it now polls the page text until it sees the marker that signals the real content has rendered:


wait for "About the job" to appear in the page — up to 25 seconds
        

No event. No guess about timing. Just an assertion on the thing we actually depend on: the text we're going to parse. When the marker shows up, the content is there. Extract.

The company name came from a different place entirely — the page title. LinkedIn's job pages have a consistent format: `{job title} | {company name} | LinkedIn`. Split on the pipes, take the middle, done. A format nobody documented but that happens to be stable.

The whole extraction stage went from "thirty successes, zero data" to thirty real results in one run.

The second silent failure, same day

While I was fixing the extraction, I found a second bug in the same pipeline. The email digest stage was supposed to send the ranked jobs to my inbox every morning. It wasn't.

The email code read its credentials from a config file — but it read them *before* loading the config, and the loader used `setdefault`, which refuses to override an empty value. The result: the script started, checked the config too late, found nothing, and logged "Email not configured."

It didn't crash. It didn't fail. It just quietly did nothing. Another silent success.

Two different bugs. Same shape: the system produced a plausible-looking outcome while delivering nothing.

What I'd tell anyone running automation

1. Assert on output, not on completion. "It finished without error" is not the same as "it worked." If your pipeline's value is a description, validate that descriptions exist. Check for zero-length results. A checkmark next to an empty result is worse than a crash — a crash gets your attention.

2. Load events are not content. Whether it's a headless browser, an API, or a database replica, "ready" is not a proxy for "has what I need." Poll for the specific condition you depend on. The extra two seconds are cheap. The silent empty result is expensive.

3. Vendors owe you nothing. The structured data LinkedIn removed was never a contract. Every scraped field, every undocumented endpoint, every "stable" format is a fuse. Design for the format to change — write the extraction so a format change produces an obvious failure, not a quiet empty record.

4. Empty credentials should be loud. A service that can't send email should not log "successfully did nothing." When the config is missing, fail fast with a message that says exactly what's missing. Quiet degradation is how you miss a week of results.

The uncomfortable part

The worst of it: this ran for how long before I noticed? I caught this one because I happened to open the output file. The previous day's run had the same bug and I didn't check — the "successful" run scrolled by, the digest never arrived, and I assumed quiet operation was normal operation.

If you're running automation — scrapers, ETL jobs, CI pipelines, AI agents — go look at your most recent successful run right now. Not the summary. The actual output. Check that the results are real.

A system that lies about success will happily lie to you every single morning.


*Andrew Katana is a cloud transformation and AI infrastructure leader. He writes about the operational reality behind AI adoption — silent failures, production debugging, and what actually works. Follow at [atkatana.com/blog](https://atkatana.com/blog).*

Built on a home lab, powered by local models, and owned by Andrew Katana.

Connect on LinkedIn →