<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: scrapewright</title>
    <description>The latest articles on DEV Community by scrapewright (@scrapewright).</description>
    <link>https://dev.to/scrapewright</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086208%2F90d1cf48-65ed-41be-aec5-2b6be4d18db4.png</url>
      <title>DEV Community: scrapewright</title>
      <link>https://dev.to/scrapewright</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scrapewright"/>
    <language>en</language>
    <item>
      <title>engineer-local-http-microservice</title>
      <dc:creator>scrapewright</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:53:02 +0000</pubDate>
      <link>https://dev.to/scrapewright/engineer-local-http-microservice-2bi9</link>
      <guid>https://dev.to/scrapewright/engineer-local-http-microservice-2bi9</guid>
      <description>&lt;h1&gt;
  
  
  Every Scrape Becomes a Local Microservice
&lt;/h1&gt;

&lt;p&gt;Most scraping tools end at "here's your JSON in a terminal." &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; ends at "here's your endpoint." Every scraper you build becomes a named HTTP service on &lt;code&gt;localhost&lt;/code&gt;, with a job model, queueing, health checks, and even step-level CRUD for CI. This is a tour of that surface — because the API design is half the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-call pattern
&lt;/h2&gt;

&lt;p&gt;Deployed services live under &lt;code&gt;http://localhost:8765/api/v1&lt;/code&gt; (port and API key configurable; header &lt;code&gt;X-API-Key&lt;/code&gt; on everything except &lt;code&gt;/health&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Submit — returns immediately with a jobId&lt;/span&gt;
&lt;span class="nv"&gt;JOB_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/api/v1/services/my-service/execute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: dev-key"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": {"query": "wireless mouse"}}'&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.jobId'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Wait — blocks until done (timeout up to 300s)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8765/api/v1/jobs/&lt;/span&gt;&lt;span class="nv"&gt;$JOB_ID&lt;/span&gt;&lt;span class="s2"&gt;/wait?timeout=120"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: dev-key"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.job.result'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execute/wait split is the right async contract: submissions are cheap, results are pull-based (&lt;code&gt;/jobs/{id}&lt;/code&gt; for non-blocking status), and both fit cron jobs, CI pipelines, and server handlers equally well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response is a contract, not a blob
&lt;/h2&gt;

&lt;p&gt;Each service declares &lt;strong&gt;JSON Schemas on both ends&lt;/strong&gt; — input and output. The result envelope is stable regardless of what the target site looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"likes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sourcePageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page_0007_a1b2c3d4"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page_0007_a1b2c3d4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;pages[]&lt;/code&gt; and &lt;code&gt;sourcePageId&lt;/code&gt;: every extracted record is stamped with the id of the page it came from, and every page is captured with URL, title, and cleaned HTML. &lt;strong&gt;Provenance is part of the response envelope.&lt;/strong&gt; When a number in your database looks wrong in March, you can trace it back to the exact page state that produced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queueing is explicit
&lt;/h2&gt;

&lt;p&gt;The host runs one execution at a time (a deliberate constraint of the single-browser architecture — see below), and instead of hiding that, the API surfaces it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"queued"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"queuePosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;queuePosition: 0&lt;/code&gt; means executing. Jobs can also be cancelled (&lt;code&gt;POST /jobs/{id}/cancel&lt;/code&gt;) and listed (&lt;code&gt;GET /jobs&lt;/code&gt;). No mysterious 429s — your place in line is a first-class field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors are classified, and some repair themselves
&lt;/h2&gt;

&lt;p&gt;The error taxonomy is small and actionable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ELEMENT_NOT_FOUND&lt;/code&gt; / &lt;code&gt;SCRIPT_ERROR&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;element missing / script failed — &lt;strong&gt;AI attempts auto-repair&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCRIPT_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;step exceeded its budget (default 60s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOGIN_REQUIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the site wants a human login; fail fast with a clear message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That first row is the interesting one: on element-not-found, the host's extension side feeds the failing step plus a DOM snapshot to the LLM and attempts an automatic rewrite before giving up. Your monitoring sees either a success or a classified failure — not a stack trace from a selector library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services are manageable over HTTP too
&lt;/h2&gt;

&lt;p&gt;The API isn't just for calling services — it's for administering them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET    /api/v1/services                       # list services + their I/O schemas
POST   /api/v1/services/{name}/steps          # add a step
PUT    /api/v1/services/{name}/steps/{id}     # update a step's script/flow fields
DELETE /api/v1/services/{name}/steps/{id}     # delete (chain auto-relinks)
GET    /health                                # no-auth liveness, for LB/K8s probes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step CRUD over HTTP means your pipeline tooling can adjust services programmatically — and every mutation re-validates the step graph (edges, no orphans, no dangling ids), so you can't save a broken service even by API.&lt;/p&gt;

&lt;p&gt;One more export worth knowing: each service can emit its own &lt;strong&gt;Markdown API documentation&lt;/strong&gt; — endpoints, schemas, examples — designed to be handed to other AI agents so they can build callers themselves. The docs your scraper needs are generated, not maintained.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint, stated plainly
&lt;/h2&gt;

&lt;p&gt;A single instance is a single browser: jobs serialize, and it depends on your Chrome being alive. For higher throughput the intended answer is horizontal — run multiple host instances on different ports behind a load balancer (the repo ships Docker and Kubernetes manifests plus a manager script), each with &lt;code&gt;/health&lt;/code&gt; for probe-based routing. The architecture is honest about being a &lt;em&gt;client-side&lt;/em&gt; platform: it wins on identity (your logins, your real browser), not on anonymous concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upshot
&lt;/h2&gt;

&lt;p&gt;If you've glued scrapers into systems before — cron + python + output files + prayer — the difference here is that the scrape is a &lt;em&gt;service&lt;/em&gt; from the moment it's deployed: queued, observable, versioned, self-describing, and repair-aware. That's not a scraping feature. That's an integration feature, and it's why this repo is worth an hour of any backend engineer's time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;github.com/singhand-labs/scrapewright&lt;/a&gt; — GPLv3, macOS/Linux/Windows, Node ≥ 18.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>engineer-step-graph-execution-model</title>
      <dc:creator>scrapewright</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:43:30 +0000</pubDate>
      <link>https://dev.to/scrapewright/engineer-step-graph-execution-model-ji3</link>
      <guid>https://dev.to/scrapewright/engineer-step-graph-execution-model-ji3</guid>
      <description>&lt;h1&gt;
  
  
  Stop Writing Spider Scripts. Draw State Machines.
&lt;/h1&gt;

&lt;p&gt;Every scraper you've ever written is a lie. You write it as a script — open page, find element, click, extract — but the moment it meets a real website, it becomes something else: a hidden state machine of retries, waits, fallbacks, and error branches, expressed through the medium of &lt;code&gt;try/except&lt;/code&gt; and vibes. &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; makes the state machine the explicit model, and it changes how scrapers fail, heal, and get generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit of execution is a step, not a script
&lt;/h2&gt;

&lt;p&gt;A Scrapewright service is a directed graph of named steps. Each step is a small script — generated by an LLM, hand-editable — with explicit control-flow edges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait_results&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wait for search results&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return { done: $count('div.result') &amp;gt; 0 }&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract_list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TERMINATE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onSuccess&lt;/code&gt;&lt;/strong&gt; — the next step when this one succeeds (content ready, data extracted), or &lt;code&gt;TERMINATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onFailure&lt;/code&gt;&lt;/strong&gt; — where control goes when the step fails or gives up: condition false, retry budget exhausted, or the script returned &lt;code&gt;{ failed: true }&lt;/code&gt; / &lt;code&gt;{ error: '...' }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;condition&lt;/code&gt;&lt;/strong&gt; — an optional JS expression evaluated in the target tab; false means skip and follow &lt;code&gt;onFailure&lt;/code&gt;. Free branching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxIterations&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;1&lt;/code&gt; (default) is a plain step. &lt;strong&gt;Greater than 1 opts the step into poll/retry semantics.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the design decision I most respect. Polling isn't a special API or a &lt;code&gt;while&lt;/code&gt; loop buried in code — it's a property of the node. A wait step returns &lt;code&gt;{ done: false }&lt;/code&gt; to mean "not ready, run me again"; the orchestrator retries up to &lt;code&gt;maxIterations&lt;/code&gt;, then follows &lt;code&gt;onFailure&lt;/code&gt;. The orchestrator even &lt;strong&gt;auto-boosts&lt;/strong&gt; &lt;code&gt;maxIterations&lt;/code&gt; to the global cap for any step that's the target of a back-edge, so legitimate pagination loops aren't killed by a misremembered default.&lt;/p&gt;

&lt;p&gt;Result signals are a deliberately tiny protocol, inspected only when &lt;code&gt;maxIterations &amp;gt; 1&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Script returns&lt;/th&gt;
&lt;th&gt;Orchestrator does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;{ done: false }&lt;/code&gt;, &lt;code&gt;{ ready: false }&lt;/code&gt;, &lt;code&gt;{ loading: true }&lt;/code&gt; …&lt;/td&gt;
&lt;td&gt;retry same step (poll)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;data, &lt;code&gt;{ done: true }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;follow &lt;code&gt;onSuccess&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;{ failed: true }&lt;/code&gt;, &lt;code&gt;{ error: 'msg' }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;follow &lt;code&gt;onFailure&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A normal step's return value is pure data and always advances via &lt;code&gt;onSuccess&lt;/code&gt; — no accidental infinite loops because someone returned an object that looks vaguely not-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  State flows through injected globals
&lt;/h2&gt;

&lt;p&gt;Steps communicate via &lt;code&gt;__stepResults__&lt;/code&gt; (a map of prior results by step id) and &lt;code&gt;__lastResult__&lt;/code&gt; (the previous step's result), injected into each step's execution context. Since &lt;code&gt;__lastResult__&lt;/code&gt; persists across a step's &lt;em&gt;own&lt;/em&gt; retries, list iteration collapses into a single self-polling step: "open item &lt;em&gt;i&lt;/em&gt; from the list in &lt;code&gt;__lastResult__&lt;/code&gt;, extract, increment, not done yet" — rather than a hand-rolled outer loop in a host language you don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graphs are validated, not just executed
&lt;/h2&gt;

&lt;p&gt;Because control flow is data, it can be checked at save time. Every persistence path — the wizard, service import, the HTTP step-CRUD endpoints — runs chain validation: every &lt;code&gt;onSuccess&lt;/code&gt;/&lt;code&gt;onFailure&lt;/code&gt; target must exist, no orphan steps, no duplicate ids, and no self-loop sentinels (an older &lt;code&gt;onSuccess: 'SELF'&lt;/code&gt; convention was removed precisely because its semantics were a trap). Mutations go through relink helpers so inserting or deleting a step rewires the chain instead of silently dangling it.&lt;/p&gt;

&lt;p&gt;This is the difference between a graph you &lt;em&gt;draw&lt;/em&gt; and a graph you &lt;em&gt;debug&lt;/em&gt;. A misconfigured poll step — say, &lt;code&gt;onSuccess&lt;/code&gt; pointing onward but &lt;code&gt;maxIterations: 1&lt;/code&gt; — runs once and advances without retrying. That's a visible, debuggable failure, not a silent mis-execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than usual in 2026
&lt;/h2&gt;

&lt;p&gt;The step-graph model isn't just tidier than scripts — it's the enabling substrate for &lt;strong&gt;LLM codegen you can trust&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generated code stays small.&lt;/strong&gt; The LLM writes leaf snippets ("wait for this selector", "extract these fields"), not control flow. Edges are structured data the wizard can validate before deploy. Small generation targets mean small blast radii when the model hallucinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-repair has a unit to replace.&lt;/strong&gt; When a deployed service fails, the failure is localized to a step; the repair loop feeds that step's script + error + sanitized DOM snapshot back to the LLM and swaps in the rewrite. You don't regenerate a monolith and pray — you patch a node and re-run the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution is replayable and inspectable.&lt;/strong&gt; The orchestrator returns every step's result and page snapshots per run. "Which step diverged after the redesign?" has a one-glance answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same engine doubles as test automation.&lt;/strong&gt; Click, type, wait, assert, branch — a step graph &lt;em&gt;is&lt;/em&gt; a self-healing replayable web test. The project explicitly positions itself as a lightweight automation tool, not just scraping.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;The steps execute in a sandboxed iframe reached through an offscreen document (MV3 CSP forbids &lt;code&gt;eval&lt;/code&gt; in extension contexts; the declared sandbox page is the sanctioned hole), while &lt;code&gt;$&lt;/code&gt;-prefixed DSL primitives (&lt;code&gt;$click&lt;/code&gt;, &lt;code&gt;$extract&lt;/code&gt;, &lt;code&gt;$wait&lt;/code&gt;, &lt;code&gt;$openTab&lt;/code&gt;, &lt;code&gt;$extractWithHover&lt;/code&gt;, &lt;code&gt;$scrollToBottom&lt;/code&gt; — 19 of them) relay to a content script in the target tab. DOM ops happen in the page; code runs quarantined; the orchestrator never blocks the UI thread of anything you care about.&lt;/p&gt;

&lt;p&gt;Deployed, the whole graph is callable over a local HTTP API: &lt;code&gt;POST /api/v1/services/{name}/execute&lt;/code&gt; → &lt;code&gt;jobId&lt;/code&gt; → &lt;code&gt;GET /jobs/{id}/wait&lt;/code&gt;. And at run time, none of it touches an LLM — generation and repair are build-time or on-failure events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If your scraping codebase has accreted retry loops, sleep-and-pray waits, and per-site "utils", the problem isn't discipline — it's the execution model. Scripts hide the state machine; step graphs make it first-class, validatable, and machine-repairable. &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; is a working, GPLv3, cross-platform implementation of that argument, with the LLM wiring included.&lt;/p&gt;

&lt;p&gt;Clone it, import an example from &lt;code&gt;examples/&lt;/code&gt;, and watch the wizard argue with a live website for a while. You'll know within an hour whether your next scraper should be a graph.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>enthusiast-local-first-ai</title>
      <dc:creator>scrapewright</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:39:41 +0000</pubDate>
      <link>https://dev.to/scrapewright/enthusiast-local-first-ai-d9a</link>
      <guid>https://dev.to/scrapewright/enthusiast-local-first-ai-d9a</guid>
      <description>&lt;h1&gt;
  
  
  An AI Scraper That Lives in Your Browser — and Never Calls the Cloud at Run Time
&lt;/h1&gt;

&lt;p&gt;Most "AI-powered" tools share an uncomfortable design: your data goes to their cloud, their model looks at it, and you pay per interaction forever. &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; inverts that model in a way worth paying attention to: &lt;strong&gt;the AI only helps you &lt;em&gt;build&lt;/em&gt; a scraper. Running it afterwards involves zero AI, zero tokens, and zero cloud.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Scrapewright is an open-source (GPLv3) platform with two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Chrome extension&lt;/strong&gt; that does the scraping — inside the Chrome you already use, with your existing logins and sessions;&lt;/li&gt;
&lt;li&gt;a small &lt;strong&gt;Node.js host service&lt;/strong&gt; that exposes everything as a local HTTP API on your machine, installed as a proper OS background service (systemd on Linux, launchd on macOS, a scheduled task on Windows).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You describe a scraping task in natural language. The built-in wizard's AI opens the target page, analyzes its structure, writes the extraction script, test-runs it in front of you, and deploys it as a reusable HTTP endpoint. From that point on, calling the service is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/api/v1/services/my-service/execute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: dev-key"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": {"query": "hello"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why "local-first" matters here
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your data doesn't leave your machine at run time.&lt;/strong&gt; After deployment, the service never calls the LLM again. The pages it visits, the data it extracts — all of it stays on your box. The LLM only ever saw page structure during the build phase, if you let it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring your own model.&lt;/strong&gt; The wizard works with OpenAI, Anthropic, Moonshot Kimi, GLM, or any OpenAI-compatible endpoint you point it at — including a local gateway. You choose the intelligence; you hold the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The economics are backwards (in a good way).&lt;/strong&gt; Agent-style scrapers that have an LLM drive a browser on every page turn think tokens per page. Scrapewright thinks tokens per &lt;em&gt;scraper&lt;/em&gt;. Build once for a few cents; run it a thousand times for free. For anything you scrape repeatedly, the cost curve crosses zero almost immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No automation fingerprint.&lt;/strong&gt; Because the extension runs in your genuine daily Chrome, requests look like... you. No headless-browser markers, no synthetic fingerprints to maintain. If you can see the page, the scraper can read it — including JavaScript-rendered content, nested iframes, hover popups, and lazy-loaded feeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-healing part is the sleeper feature
&lt;/h2&gt;

&lt;p&gt;Every scraper eventually breaks — sites redesign. The usual outcome is an afternoon of re-debugging someone else's selector logic. Scrapewright's Auto-Fix takes the failure, a snapshot of the new DOM, and its own script, and asks the LLM to rewrite the broken part. Config time or run time, same mechanism: repair beats rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth knowing before you dive in
&lt;/h2&gt;

&lt;p&gt;It's not magic for every job — the README is refreshingly honest about this. One browser means one execution lane: a single instance serializes jobs, and 10,000-URL firehoses are better served by server-side farms. Where it shines is the huge middle ground of scraping: logged-in dashboards, intranets, paid content, list-plus-detail-page drill-downs, and low-frequency high-value lookups — precisely the jobs cloud scrapers fumble because they can't be &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Everything is on GitHub: &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;github.com/singhand-labs/scrapewright&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 60-second quick start gets you from clone to first deployed service in one sitting, and the repo ships importable example services so you can see the shape of a finished scraper before building your own. If you've been waiting for AI tooling that treats your machine as the platform instead of a terminal — this one's worth a weekend.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>stem-students-lab-data</title>
      <dc:creator>scrapewright</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:23:59 +0000</pubDate>
      <link>https://dev.to/scrapewright/stem-students-lab-data-10e7</link>
      <guid>https://dev.to/scrapewright/stem-students-lab-data-10e7</guid>
      <description>&lt;h1&gt;
  
  
  Get the Data for Your Lab Report Before Midnight
&lt;/h1&gt;

&lt;p&gt;Let's be honest about how the data-collection phase of a course project actually goes. You budget two hours. You spend the first on a scraping tutorial, the second on installing a headless browser that doesn't quite launch, and then the site you needed renders everything through JavaScript anyway, and now it's 23:40 and your "dataset" is 40 rows of hand-pasted text with inconsistent date formats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; is an open-source Chrome extension built around a simple observation: the part that's hard for you (writing robust extraction code against a messy page) is exactly the part modern AI is good at, and the part that's easy (knowing which fields you need) is the part you already know. You describe the target — &lt;em&gt;"search this component-supplier site for the part number I input, open each result, return name, price, stock, and specs"&lt;/em&gt; — and its wizard opens the page, figures out the structure, writes the extraction steps, and test-runs them while you watch. Deploy it, and you get a stable local HTTP endpoint any script, notebook, or report pipeline can call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/api/v1/services/parts-lookup/execute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: dev-key"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input": {"part": "LM358"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last part matters more than it looks. Most no-code tools hand you a CSV and stop. Scrapewright hands you an &lt;strong&gt;API&lt;/strong&gt; — JSON in, JSON out, with declared input/output schemas — so your Python/MATLAB/Julia analysis code can call the live source directly instead of importing a stale export. For a lab that means one less manual step between "sample measured" and "supplier data joined". For a thesis it means your data pipeline is re-runnable in April when the professor says "also do last year's parts".&lt;/p&gt;

&lt;p&gt;The realistic failure modes of student scraping are handled rather than hand-waved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript-rendered pages and nested iframes&lt;/strong&gt; — it reads the DOM in your real Chrome, so what you see is what it extracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite scroll / lazy loading&lt;/strong&gt; — handled, including edge cases where sites ignore programmatic scrolling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login-gated course systems and supplier portals&lt;/strong&gt; — it uses your existing browser session, so if you can see the data, it can reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"It worked yesterday"&lt;/strong&gt; — sites redesign; the Auto-Fix button has the AI repair the extraction steps against the new layout instead of you starting over.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Costs, in student terms: you need an API key from some LLM provider (OpenAI, Anthropic, Moonshot Kimi, GLM — student/cheap tiers fine) to power the &lt;em&gt;building&lt;/em&gt; step, which costs cents per service. Running a deployed service costs nothing — it never calls the AI again — and everything runs locally. Setup is: load the extension in Chrome, run one install command for its background service. Node ≥ 18 required.&lt;/p&gt;

&lt;p&gt;One caution worth writing down: it's a single-browser, single-job-at-a-time tool. If your project genuinely needs a 100k-page crawl, use a server-side framework and cite this repo for the prototype phase instead. Scrapewright's sweet spot is &lt;em&gt;structured, repeated queries against pages you personally can access&lt;/em&gt; — which is what most course projects, lab pipelines, and hackathon backends actually are.&lt;/p&gt;

&lt;p&gt;Repo and quick start (60 seconds to read, an evening to first working service): &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;github.com/singhand-labs/scrapewright&lt;/a&gt;. The &lt;code&gt;examples/&lt;/code&gt; folder has importable sample services — useful both as a shortcut and as a template for what a finished one looks like.&lt;/p&gt;

&lt;p&gt;The deadline hasn't moved. But the data part just got a lot shorter.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>agent-builders-stable-tools</title>
      <dc:creator>scrapewright</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:21:11 +0000</pubDate>
      <link>https://dev.to/scrapewright/agent-builders-stable-tools-57g9</link>
      <guid>https://dev.to/scrapewright/agent-builders-stable-tools-57g9</guid>
      <description>&lt;h1&gt;
  
  
  Give Your AI Agent a Stable Tool Instead of a Steering Wheel
&lt;/h1&gt;

&lt;p&gt;The 2026 default for "AI + web" is agentic browsing: give the model a browser, let it look-click-wait-extract per task. Watching it work feels like the future. Putting it in production teaches you its actual economics: every run re-perceives the same pages, every run pays tokens per page-turn, every run is a fresh roll of the dice on whether it clicks the same button it clicked yesterday. For exploration, wonderful. For &lt;em&gt;repeated&lt;/em&gt; extraction — the daily dashboard pull, the per-item lookup, the scheduled monitor — it's the wrong shape entirely.&lt;/p&gt;

&lt;p&gt;The older engineering instinct says: &lt;strong&gt;repeated work belongs in a tool, not in a steering wheel.&lt;/strong&gt; &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;Scrapewright&lt;/a&gt; is that instinct, productized — an open-source platform where an LLM &lt;em&gt;builds&lt;/em&gt; a scraping service once, and everything afterward is a deterministic HTTP call your agent can make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The division of labor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build time (LLM, once):&lt;/strong&gt; you describe the extraction in natural language. The wizard's AI opens the target page in your real browser, researches its structure in rounds (candidate selectors confirmed against actual element HTML, not guessed), generates a step-graph program against a 19-primitive DSL, test-runs it, and self-repairs on failure. Result: a deployed service with declared input/output JSON Schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run time (no LLM, ever):&lt;/strong&gt; your agent — or cron, or your app — calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/services/{name}/execute   {"input": {"query": "..."}}
GET  /api/v1/jobs/{id}/wait?timeout=120
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured JSON comes back, every record stamped with the page it came from. Fast (no model latency), free (no tokens), deterministic (same steps every run).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repair time (LLM, on failure):&lt;/strong&gt; when a site redesigns and the service errors, Auto-Fix feeds the failing step + DOM snapshot back to the model and rewrites it. The tool heals itself without a human or an agent re-learning the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent-facing detail I most want you to notice
&lt;/h2&gt;

&lt;p&gt;Every Scrapewright service can export its own &lt;strong&gt;Markdown API documentation&lt;/strong&gt; — endpoint, schemas, examples, generated from the service definition. The intended workflow is explicit in the README: hand that document to your agent (the docs name Hermes Agent, WorkBuddy, and Lobster as examples), and let the &lt;em&gt;agent&lt;/em&gt; construct its own tool wrapper for calling the service. That's a cleaner pattern than most "agent + scraper" integrations, because it separates two jobs that keep getting conflated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deciding&lt;/strong&gt; what data is needed and when → the agent (good at judgment, bad at repetition).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting&lt;/strong&gt; the data → a stable HTTP service (good at repetition, free at repetition).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent doesn't need to see the target site's DOM, burn context on page perception, or hold a browser session open. It needs one tool call with a schema it was handed. Your context window — and your token bill — stay spent on the reasoning you actually hired the model for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "client-side" makes the tool layer stronger
&lt;/h2&gt;

&lt;p&gt;The service runs in a real Chrome via an extension — your logins, your sessions, your fingerprint. So the tool layer can cover sources that server-side scrapers structurally can't: the SaaS dashboard behind SSO, the paid archive, the internal portal. When your agent needs "yesterday's numbers from the ops dashboard," the honest options used to be (a) agentic browsing with stored credentials, or (b) an integration that doesn't exist. Now there's (c): a deterministic local endpoint that &lt;em&gt;is&lt;/em&gt; your credentialed access, wrapped in a schema. Self-hosted, one API key on localhost, nothing in the runtime path phoning any cloud — LLM or otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary
&lt;/h2&gt;

&lt;p&gt;Compile-time tooling assumes the task repeats. Genuinely exploratory, one-off, every-site-is-different work is still agentic-browsing territory, and the project's own docs say so. The interesting design space is the hybrid — an agent that &lt;em&gt;builds&lt;/em&gt; Scrapewright services on demand for tasks it detects are recurring, then stops thinking about them. The pieces for that are all in the box: a wizard API, service export/import as JSON, and per-service Markdown docs an agent can consume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/singhand-labs/scrapewright" rel="noopener noreferrer"&gt;github.com/singhand-labs/scrapewright&lt;/a&gt; — GPLv3, quick start in the README, whitepaper for the architecture. If you're building agents that touch the web, spend one evening making a service and pointing your agent at its API doc — the drop in latency, cost, and flakiness is the whole argument.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
  </channel>
</rss>
