Show HN: Runo – open-source web scraping that returns typed JSON Developer rhymeswithlimo open-sourced Runo, a web scraping tool that uses LLMs to extract typed JSON from any URL based on a user-defined schema, after initially developing it as a closed-source SaaS. The tool requires a Google Gemini API key and offers CLI, local server, and Python library interfaces. English · 简体中文 /rhymeswithlimo/runo/blob/main/README.zh-CN.md · Español /rhymeswithlimo/runo/blob/main/README.es.md · Français /rhymeswithlimo/runo/blob/main/README.fr.md · Deutsch /rhymeswithlimo/runo/blob/main/README.de.md · 日本語 /rhymeswithlimo/runo/blob/main/README.ja.md Extract structured, typed JSON from any URL using a schema you define. Note I'm a sole maintainer on this project. It started as a closed-source SaaS scrapewithruno.com https://scrapewithruno.com , but I decided to open-source it : . You describe what you want a field name, a type, an example value and Runo fetches the page, renders JavaScript if the site needs it, extracts the data with an LLM, and coerces every value to the type you asked for. You get clean, flat JSON back. No selectors, no XPath, nothing to maintain. Since the LLM reads for meaning instead of DOM position, your schema doesn't break the next time someone redesigns the site. A field it can't find comes back null instead of just vanishing. This is the open-source build you run yourself. You'll need a Google Gemini API key, that's the only main requirement. Typed output : strings, ints, floats, booleans, ISO 8601 dates, typed arrays, all coerced strictly. Plain schema : name, type, example. No DSL. Semantic extraction : reads meaning, not DOM position, so redesigns don't break it. Smart rendering : plain HTTP first, headless browser only if the page needs it. Fast paths : checks JSON-LD, OpenGraph, Twitter Cards, oEmbed before ever calling an LLM. Three modes : extract single URL , batch one schema across many URLs , or crawl follows links from a seed URL . Async built in : extract async , batch async , crawl async for anyone running this inside their own event loop. Three interfaces : CLI, local server, or Python library. Requires Python 3.11+ python 3.14 recommended . pip install -e ". tls,patchright " the extras improve anti-bot fetching playwright install chromium one-time browser download for JS pages IMPORTANT : Runo requires a Gemini API key to function. Get one at https://aistudio.google.com/apikey . cp .env.example .env edit .env and set GEMINI API KEY=... .env is loaded automatically. You can also just export GEMINI API KEY in your shell. The pip install -e . above registers a runo command on your PATH, so you can run it from any directory, no need to cd into the clone. Just keep the cloned folder in place the editable install links back to it and the same Python environment active. .env is read from the current directory you're in , so to run runo from anywhere, export the key globally instead: export GEMINI API KEY=your key Unix/macOS setx GEMINI API KEY your key Windows applies to new terminals The command line is the quickest way to try Runo. Alternatively, reach for the Python library when you're building it into your own code, or the local server when you want a language-agnostic HTTP endpoint. runo serve http://127.0.0.1:8000 curl -X POST http://127.0.0.1:8000/v1/extract \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com","schema": {"field":"title","type":"string","example":"x"} }' { "url": "https://example.com", "status": "success", "render mode": "plain", "data": {"title": "Example Domain"} } No API key or auth header, it's your local server. Endpoints: /v1/extract , /v1/batch , /v1/crawl . Pass per-request settings in an options object see Options options . Runo works as a python library. python from runo import extract data = extract "https://example.com", {"field": "title", "type": "string", "example": "Example Domain"}, {"field": "paragraph", "type": "string", "example": "This domain is..."}, print data {"title": "Example Domain", "paragraph": "..."} batch runs one schema across many URLs; crawl follows links from a seed URL. Each has an async variant extract async , batch async , crawl async for use inside your own event loop. python from runo import batch, crawl rows = batch "https://a.com", "https://b.com" , schema, concurrency=5 site = crawl "https://blog.com", "https://blog.com/posts/ ", schema, max pages=50 You can also run Runo from the command line. single URL schema from a file runo extract https://example.com --schema schema.json inline schema, force the headless browser, write result to a file runo extract https://example.com --schema ' {"field":"title","type":"string","example":"x"} ' \ --render-js always -o out.json many URLs with one schema urls.txt is one URL per line runo batch --urls urls.txt --schema schema.json --concurrency 5 follow links from a seed runo crawl https://blog.com --pattern "https://blog.com/posts/ " --schema schema.json --max-pages 50 run the local HTTP server runo serve --host 127.0.0.1 --port 8000 --schema takes a path to a JSON file or an inline JSON string. Common flags: --render-js auto|always|never , --timeout-ms , --no-cache , and -o out.json to write output to a file instead of stdout --concurrency for batch; --max-pages , --max-depth , --use-sitemap , --ignore-robots for crawl . A schema.json file is just a JSON array of field objects: {"field": "title", "type": "string", "example": "Example Domain"}, {"field": "price", "type": "float", "example": 29.99, "hint": "Use the sale price if present."} Each field has a field name, a type , an example value a one-shot anchor for the LLM , and an optional hint . A good example disambiguates format, for instance 35 vs "35 years old" , or 2024-01-31 vs January 31 . | Type | Coercion | |---|---| string | Always a string | integer | Parsed from text "35 years old" - 35 | float | Parsed from text "$1.2M" - 1200000.0 | boolean | Normalised "✓ Verified" - true | date | ISO 8601 YYYY-MM-DD ; relative dates resolved | array