Back to all writing
Data Acquisition

One Runner, Many Patterns: An AI-Driven Scraping Framework

Deep technical22 min read

When the data you sell is data you collect, the collection tier is not infrastructure. It is the supply chain. A large research estate typically gets there the same way: several independent collection platforms, each built by a different team in a different era, each a reasonable local decision at the time, and collectively a liability nobody chose.

This is about consolidating that onto one AI-driven collection framework, and about the analysis that came before the build. The interesting result was never the AI. It was finding that a fleet of many hundreds of scrapers is a small set of patterns wearing different configuration, and that once you know the patterns, most of the rest is engineering you can buy or generate.

Many platforms, many specialties, one job

Each business area had solved collection for itself, so the estate grew into parallel platforms with no shared services between them. Different languages. Different orchestration philosophies, from managed state machines to a message bus to a workflow scheduler to hand-rolled script runners. Pipeline shapes that did not agree on how many stages a pipeline has. Output formats and destinations that agreed on nothing at all.

The consequence is easy to state and expensive to live with: nothing transferred. Not code, not monitoring, not people. A fix on one platform needed one language and one message bus; the same class of fix elsewhere needed a different language and a different scheduler. Every platform paid its own maintenance bill for the same problems, and every hire, on-call rotation and tooling decision multiplied by the number of stacks.

Manual effort was the operating model

Underneath the fragmentation sat a deeper issue. A deterministic scraper assumes the source never changes, and modern publishing portals change constantly.

The break-fix loop never closed. Layout changes and format churn broke parsers continuously. Engineers hand-patched selectors, redeployed, and waited for the next change. A meaningful share of sources sat behind bot protection or needed a full browser to render at all.

Housekeeping shipped as code. Certificate renewals, password rotations and schedule changes all went out as deployments through each platform's own release process. Routine credential work consumed engineering capacity and carried release risk, for changes that should have been configuration.

Failures were found downstream. With almost no source-level observability, a silent break was usually noticed in a downstream table, a model output, or by a client. By then the bad data was already in the product. When your customer is your monitoring system, you have a commercial problem rather than an operational one.

The stakes were not back-office

These feeds price trades, track physical infrastructure, and land directly in client-facing products. Refresh cadences ran from sub-minute polling at one end to annual regulatory filings at the other, with everything in between. A late file is a stale price, and a stale price does not queue politely. It shows up on client screens.

The control gaps compounded it. Several distinct authentication patterns, from anonymous endpoints through API keys, OAuth, mutual TLS client certificates and mailbox logins, each managed ad hoc inside scraper code, separately on every platform. Run-to-output lineage existed only in pockets. And the inventories themselves disagreed: different systems of record reported different totals, and one platform's own breakdown did not reconcile with its own stated total.

That last point is the one I would put in front of an executive. When the inventory cannot be reconciled, auditability is not a policy question. The data you would need in order to audit does not exist.

The insight: a scraper is configuration, not a program

Before designing anything we inventoried a representative fleet completely, field by field. The question was not "how do we rewrite these" but "what are these, actually."

Every scraper turned out to be a combination of four things: a transport (how you reach it), a container (what comes back), a discovery rule (how you know which URL or file to fetch) and an auth mode (how you get in). Enumerate the distinct combinations that actually occur and you do not get hundreds. You get a short, closed list, and the long tail you expect to find simply is not there.

A SCRAPER IS FOUR CHOICES Transport https, http, ftp, imap, websocket how you reach it Container bare file, archive, html, attachment what comes back Discovery date template, node list, id lookup, search which one to fetch Auth none, basic, api key, mtls, oauth, mailbox how you get in A short, closed set of combinations actually occurs ONE GENERALISED RUNNER, DRIVEN BY CONFIGURATION url and date template iteration rule auth mode and carrier snapshot and retry
The move that made the estate tractable, and it involved no machine learning at all. Once a scraper is a pattern choice plus configuration, migrating a fleet stops being a fleet of rewrites and becomes a mapping exercise, and the same pattern library becomes the yardstick for every remaining platform.

So a source stops being code and becomes a catalog entry:

A source definition, submitted through the catalog rather than a repository
name:             regional-balancing-error
pattern:          ftp-single-file
transport:
  host:           ftp.example-operator.org
  path:           /reports/balancing/ACE_current.csv
auth:
  mode:           anonymous
discovery:
  kind:           static_path         # publisher overwrites one file in place
  snapshot_on_pull: true              # so keep every pull, history survives
schedule:         rate(5 minutes)
container:        csv
schema_ref:       balancing-error@v3  # the contract, versioned separately
politeness:
  max_rps:        0.5
  respect_robots: true
owner:            grid-analytics

That entry attaches an approval gate, vault-managed credentials, run-level lineage and freshness monitoring automatically, because those are properties of the platform rather than of the scraper. The snapshot_on_pull flag is worth pausing on: the publisher overwrites a single static file on a schedule, so without it you have current state and no history. With it you have a time series, and not one line of bespoke code was written to get it.

The schema is the contract, and it deserves its own lifecycle

The commercial scraping platforms worked this out before most in-house teams did. Look at how the mature ones are built, whether that is Bright Data's studio, an actor-style marketplace or a managed extraction API, and the schema is a first-class object with its own editor, not a byproduct of whatever the parser happened to emit. We copied that deliberately.

A registered source points at a versioned schema rather than carrying its own field list. The schema editor lets an owner set types, nullability, units, formats and value ranges per field, and infers a first draft from a sample so the starting point is close. Three properties matter more than the editing experience.

Schema evolution is governed. Additive changes are allowed by default. Removing a field or changing a type mints a new version, and consumers pin to a version rather than tracking head. A publisher renaming a column becomes a versioning decision made by a named owner, not a silent break in a downstream table.

Extraction is mapped, not hardcoded. Each field records how it was obtained: which selector or strategy produced it, and with what confidence. That per-field provenance is what makes the difference between an alert saying a run failed and an alert saying one field stopped resolving while the rest stayed fine.

Drift is classified, not just detected. Every run infers a schema from what actually arrived and compares it against the contract. A new column, a missing column, a widened type and a changed format are four different events with four different responses. Only some of them should stop a pipeline. Treating them all as parse failures is why so much collection tooling cries wolf until people stop reading the alerts.

A registered schema with per-field extraction strategy and validation
schema: balancing-error@v3
evolution: additive_only          # breaking changes mint v4
fields:
  - name: interval_start
    type: timestamp
    tz: source_local
    required: true
    extract: { strategy: css, selector: "td[data-col='ts']",
               fallbacks: [ xpath, text_anchor ] }
  - name: ace_mw
    type: decimal(12,3)
    unit: MW
    range: [-10000, 10000]        # a value outside this is quarantined
    required: true
    extract: { strategy: column_index, index: 2 }
  - name: revision
    type: int
    required: false               # added in v3, additive, no new version needed
on_drift:
  new_column:     warn_and_capture
  missing_column: fail_if_required
  type_widened:   warn
  type_narrowed:  fail

Where AI belongs, and where it must not go

This is the part most teams get backwards, so I want to be precise. AI sits in two places, and deliberately nowhere else.

At build time, generating the collector

Give the platform a sample page or response and it generates the extraction code, infers the schema and column types, analyses the document structure and proposes selectors. For browser-rendered sources there is a recording mode: drive the page once by hand and the session becomes a replayable script, which is the same authoring pattern the commercial studios use.

Two build-time techniques repaid the effort far beyond the obvious code generation.

API discovery from the network waterfall. Before generating a single selector, capture the page's network traffic and look at what the page itself calls. A large share of modern portals render from a JSON endpoint that is easier to call directly, more stable than the DOM, and often paginated and filterable in ways the visible page is not. The best scraper is frequently no scraper at all, and finding that out is an automated analysis rather than an act of inspiration. When it works, the source gets promoted from an HTML pattern to an API pattern and stops being fragile.

Semantic anchors rather than positional selectors. Generated selectors default to anchoring on stable text, labels and accessibility attributes rather than on position in the tree. A selector written as the third cell of the fourth row breaks the moment a column is inserted. A selector anchored on a header label survives it. This single generation constraint removed a large class of future breakage, and it costs nothing at build time.

At run time, on the exception path only

Run-time AI engages on failure, never by default. The important design detail is that self-healing is not a single attempt at repair. It is an escalation ladder, and each rung is more capable and more expensive than the last.

EXTRACTION FAILS: CLIMB ONLY AS FAR AS NEEDED 1. Deterministic retry backoff, alternate mirror, cached credentials. No model. Cost: nil 2. Fallback selector chain ranked candidates per field: css, xpath, text anchor. Still no model 3. Structural diff against the last good snapshot what changed in the DOM, localised to a subtree 4. Model re-derives selectors, or extracts to schema directly proposed fix is a diff, reviewed and versioned 5. Agentic browser drives the page like a person multi-step flows, and reports what changed in plain language Exhausted: quarantine the run, alert the named owner with the diagnosis attached. Never publish a guess.
Self-healing as a ladder rather than a single attempt. Most breakages are resolved on the first two rungs at zero model cost, which keeps the economics sane on a large fleet, and anything that reaches the top has a written explanation waiting for the owner.

The scheduled hot path stays deterministic. This is the design decision I would defend hardest. If every scheduled pull invokes a model you have bought unpredictable cost and unpredictable output on a fleet running thousands of jobs at cadences down to a minute. Concentrating AI at build time and on the exception path means spend scales with breakage rather than with volume, and the daily behaviour of the fleet is the behaviour you tested.

The unblocking tier is a commodity, so buy it

A minority of sources sit behind bot protection, and that minority consumes a wildly disproportionate share of engineering attention if you try to fight it in-house. Anti-bot detection is an arms race between well-funded specialists, and it is not a race a data business creates value by entering.

The commercial platforms have converged on a small set of primitives here, and they are worth naming because the shape is now standard across vendors:

The framework treats all of these as pluggable transports behind one interface, so a source can move between direct fetch, unblocking API and remote browser by changing configuration rather than code. That also keeps the commercial position honest, because switching providers is a config change rather than a migration.

What no vendor sells is the part that makes an estate like this hard: file transfer sessions, mailbox-sourced feeds, certificate-bound portals, embargo-aware backfills, and landing into your own governed storage with lineage. Buy the commodity, build the differentiator, and be clear-eyed about which is which.

Politeness is a platform control, not a per-scraper habit

Every source carries a rate policy, and the runner enforces it centrally with a per-domain token bucket rather than trusting each collector to behave. Crawl-delay directives are honoured. Repeated rejection responses trigger adaptive backoff and reduce the domain's budget for a cooling-off window rather than retrying into a wall.

This is partly courtesy and mostly self-interest. Aggressive polling is how a public endpoint that has served you for years decides to start blocking you, and recovering that relationship costs far more than the data you gained by hammering it.

The architecture

AI LAYER, ACROSS THE WHOLE FLOW Build time: API discovery, code generation, schema inference, semantic anchors Run time, exception path only: escalation ladder, model extraction, agentic validation 1. SELF-SERVICE DEFINITION pipeline editor and catalog schema editor and registry API mirrors the UI exactly 2. EVENT-DRIVEN SCHEDULER AND ADMISSION cron and rate rules per source per-domain token bucket retries, backoff, dead letter 3. MULTI-PROTOCOL EXECUTION ENGINE HTTPS and APIs headless browser FTP and mailbox crawl frameworks client certs from vault sandboxed code steps record and replay content hashing: unchanged payload skips reprocessing but still records the run Unblocking tier, bought remote browser endpoint unblocking API, proxy tiers fingerprints, sticky sessions pluggable: config, not code 4. LAND, VALIDATE, SERVE raw zone: immutable, versioned contract check, drift classified typed columnar, partitioned Governed by construction: every run writes config version, schema version, run id, inputs and outputs. Credentials from the vault. Alerts to the named owner.
The unblocking tier sits behind one interface so a source can move between direct fetch, unblocking API and remote browser by configuration. Content hashing matters more than it looks: publishers that overwrite a file in place produce identical payloads constantly, and reprocessing them is pure waste.

Four details in there carry more weight than a line each.

The API mirrors the UI exactly. A person registering one source and a migration script registering hundreds use the same contract. This sounds like a minor symmetry and it is the reason a large migration is feasible at all: you are not maintaining a bulk path that drifts from the interactive one.

Retries end somewhere visible. Bounded retries with backoff, then a dead letter queue. That queue is what converts a silent failure into an owned, visible event, which was the single largest operational gap we set out to close.

The raw zone is immutable and versioned. Whatever the publisher sent is preserved exactly as sent, partitioned per run. Every reprocessing question later, and there are always reprocessing questions, is answerable without going back to the source.

The output path is deterministic. Every run lands at a predictable partitioned path carrying its run and schema identifiers, and the record of config version, inputs and outputs is written as a by-product. Lineage is not a feature someone remembered to add. It is the shape of the write.

Validation with an agentic browser

Here is the failure mode that keeps me up at night on a self-healing system. A parser breaks, the platform repairs it automatically, and the repaired selector now points at the adjacent column. Collection succeeds. Monitoring is green. Wrong numbers flow into the product with full lineage and a clean audit trail, which makes them more credible, not less.

Schema and range checks catch some of that, and they do not catch the case where the wrong value is a plausible value. What does catch it is looking at the page the way a person would.

Agentic browser models are the useful new capability here. Something like Amazon Nova Act takes a natural-language instruction and performs it reliably in a real browser, broken into small verifiable steps. We use that in three places, none of which is the scheduled hot path:

The cost discipline is the same as everywhere else: this runs on repair, on a sample, and on the sources that need it, not on every pull.

Governance as the default path

The operating model change matters more than any single component. Onboarding moves from an engineering ticket to a catalog entry with an approval gate. Credential rotation moves from a code change and deployment to a policy-managed operation. Failure detection moves from downstream discovery to source-level freshness, schema and value checks that alert a named owner. Ownership moves from tribal knowledge to a recorded field.

The point is not that these controls exist. It is that the governed path is the only path. Approval, managed credentials, run lineage and monitoring apply because a source was registered, not because someone remembered to configure them. The audit trail is created as a by-product of doing the work, which is the only kind that stays current.

Growing the same spine agentic

The framework has four stages: scrape, extract, curate, vectorize. The extensions that make it agentic are deliberately built as additional step types in the same pipeline executor, recorded in the same run lineage, rather than as a second platform alongside the first.

That last stage is what connects this to the rest of the AI platform, because it turns a collection tier into a retrieval corpus.

Registering it as a custom workflow extension

Rather than wiring a bespoke integration, the collection framework registers on the central AI platform as a workflow extension, the same way the model estate does. It appears to agents through the same gateway and inherits identity, entitlements, tracing, evaluation and human review rather than reimplementing any of them.

Tools exposed by the collection extension
list_sources(filter)
  -> [ { name, pattern, owner, cadence, last_run, health } ]

describe_source(name)
  -> { pattern, schema_ref, auth_mode, cadence, rate_policy,
       destination, owner, required_entitlement }

trigger_collection(name, window)              # async, off-cadence pull
  -> { run_id, accepted_at }

get_run_status(run_id)
  -> { state, records, bytes, drift_events, diagnosis }

get_run_artifacts(run_id, projection)         # reference, not payload
  -> { raw_uri, curated_uri, schema_version, row_count, confidence }

search_collected(query, filters)              # over the vectorized layer
  -> [ { chunk, source, run_id, published_at, score } ]

propose_source(url_or_sample)                 # drafts, never activates
  -> { suggested_pattern, draft_config, inferred_schema,
       discovered_api, policy_check,
       status: "pending_human_approval" }

The last tool is the interesting one and needs the most discipline around it.

Agent runtime Gateway: authz, audit Collection extension rate, robots and terms policy WHAT THE AGENT MAY DO ON ITS OWN Read the catalog and health Trigger an off-cadence pull Search the vectorized layer Draft a source: discover the underlying API, infer the pattern, generate config and schema, run the policy check Human approval gate: a draft is not a collector owner assigned credentials from the vault scheduled and monitored
An agent can do the tedious part of onboarding a source and none of the consequential part. Proposing a configuration is analysis. Activating a collector points recurring traffic at somebody else's servers under your company's name, which is a decision a person signs.

An agent given a URL can do the work that used to take an engineer an afternoon: fetch a sample, check whether a cleaner API sits behind the page, infer which pattern fits, generate the extraction config and draft schema, and submit it. What it cannot do is activate it. The draft lands in the same approval queue a human-authored source lands in, and a person assigns the owner and signs it off.

That separation is not bureaucratic caution. Rate limits, terms of service and robots directives are commitments your business is making, and they belong in platform policy enforced at the extension boundary, not in an agent's judgment about whether a site seems fine to scrape. An agent that can talk itself past a rate policy is a liability with a nice interface.

Evaluating the extension

Because it is registered on the platform, the extension inherits the evaluation system, and the golden set for collection is specific: saved page fixtures with known correct extraction, replayed in continuous integration so a change to the generator is regression-tested without touching the live source. Field-level accuracy is the headline score. Two checks matter more than the headline.

First, that generated extraction produces the expected fields rather than plausible-looking wrong ones. Second, that self-healing has not silently changed a schema. A parser that repairs itself into the wrong column publishes; a broken one alerts. The repaired one is the more dangerous outcome, and it is the reason the agentic validation step earns its cost.

What it changes

Onboarding goes from weeks of engineering backlog to days through a catalog. Detection moves from the client to the source owner. Credential rotation stops requiring a release. Several sets of tooling, on-call rotations and tribal knowledge collapse toward one. And engineers stop spending their week on selector patches.

One thing I would insist on in any programme like this: decommission is the deliverable. Each phase has to end with legacy scope switched off, not with a demo of the new thing. The savings and the risk reduction land only when the old stack stops running, and a consolidation programme that never retires anything has simply added one more platform to the pile.

The closing thought is the same one I reached on the modelling side. The AI is the smallest part of this. What made the estate tractable was the boring inventory work that revealed a short pattern library underneath a large fleet. AI then made the long tail cheap to build and made breakages partly self-correcting, and an agentic browser made silent corruption detectable, all of which are real gains worth having. But if you start with the AI you end up with a very capable generator producing collectors into the same ungoverned mess, and the thing you actually needed, which is knowing what you collect, who owns it, and whether it is right, is still missing.

Read: the model estate this feeds All writing