
Crawl4ai
- 847 installs
- 42 repo stars
- Updated June 16, 2026
- brettdavies/crawl4ai-skill
crawl4ai is a Claude Code skill that teaches the Crawl4AI CLI to convert public webpages into clean markdown, structured JSON, or LLM-ready data for developers who need agent-grade web extraction without custom scrapers.
About
crawl4ai is a skill from brettdavies/crawl4ai-skill documenting the Crawl4AI command-line interface for web crawling and extraction. The readme covers installation, basic usage, browser configuration, crawler configuration, extraction configuration, and advanced features including LLM Q&A and structured data extraction. Developers reach for crawl4ai when they need repeatable CLI commands to turn arbitrary public URLs into markdown or JSON suitable for RAG pipelines, eval datasets, or agent context—without maintaining bespoke BeautifulSoup or Playwright scripts. The guide is organized as a Tier 2 CLI reference with configuration sections for browser, crawler, and extraction behavior.
- CLI command `crwl` for instant web crawling and markdown conversion
- Supports structured data extraction, LLM Q&A, and content filtering
- Multiple output formats including markdown, JSON, and cleaned HTML
- Advanced configuration for browser, crawler, and extraction behavior
- Built-in caching, cache-bypass, and verbose logging options
Crawl4ai by the numbers
- 847 all-time installs (skills.sh)
- +31 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,282 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/brettdavies/crawl4ai-skill --skill crawl4aiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 847 |
|---|---|
| repo stars | ★ 42 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 16, 2026 |
| Repository | brettdavies/crawl4ai-skill ↗ |
How do you scrape webpages into LLM-ready markdown?
Turn any public webpage into clean markdown, structured JSON, or LLM-ready data without writing custom scrapers.
Who is it for?
Developers building RAG ingestion, agent context pipelines, or data extraction jobs who want Crawl4AI CLI patterns instead of custom scrapers.
Skip if: Teams crawling authenticated private apps or needing full browser-test automation rather than public-page extraction CLI workflows.
When should I use this skill?
A developer needs to crawl a public URL into markdown or JSON using Crawl4AI CLI with browser, crawler, or extraction configuration.
What you get
Clean markdown files, structured JSON extracts, and configured Crawl4AI CLI crawl outputs from target URLs.
- Markdown crawl output
- Structured JSON extracts
Files
Crawl4AI
Verified against `crawl4ai` `VERSION`. PEP 723 pins in scripts/*.py and tests/*.py floor at that version.
Overview
Crawl4AI wraps a headless browser (Playwright) plus a markdown-aware content pipeline. Use it when defuddle/curl can't reach the content — JavaScript-rendered pages, login-gated content, infinite scroll, multi-URL concurrency, repeatable schema-based extraction.
This skill exposes both interfaces of the underlying library:
- CLI (
crwl) — quick, scriptable commands: CLI Guide - Python SDK — full programmatic control: SDK Guide
Invoked with a URL argument
When the user runs /crawl4ai <url> with a single URL and no further qualifier, treat it as the JS-heavy fetch case and default to:
crwl <url> -c "wait_until=networkidle,page_timeout=60000" -o markdownwait_until=networkidle waits for the network to be quiet for ~500ms post-load — the right default when the user hasn't named a specific element on a JS-rendered page. (Avoid wait_for=css:body: <body> exists at t=0 on every HTML response, so it's satisfied before JS renders content.) Then return the markdown to the agent context. Adjust to wait_for=css:<selector> if the user named a specific element. Skip the default and route to the relevant section below for any task that names extraction, batch / multi-URL, login / session, screenshot / PDF, or URL discovery — those each have their own pipeline. If the URL is clearly static (a docs page, a blog post), route the user to /fetch-web instead per the "When NOT to use" section below.
When NOT to use this skill
- Static HTML pages (most documentation sites, blog posts, news articles, tweets) — use
/fetch-webordefuddle
directly. Static extraction is ~0ms cold start; crawl4ai pays a ~2s browser startup tax.
- Local file conversion (
.pdf,.docx,.pptx,.epub) — use/markdown-convert. - One-URL agent-context reads (the agent just needs to read this page) — use
/fetch-weband let it route to
defuddle.
- Mutating UI flows (form fills, multi-step clicks, login + navigation) —
/browse(gstack's persistent headless
Chromium) is built for that.
When stuck
For unknown crwl/SDK flags, scrape failures, or extraction edge cases the references don't cover, see references/escalation.md for the lookup order (qmd solutions → upstream docs → GitHub issues → ask the user) and worked examples.
---
Quick Start
Installation
pip install crawl4ai
crawl4ai-setup
# Verify installation
crawl4ai-doctorCLI (Recommended)
# Basic crawling - returns markdown
crwl https://example.com
# Get markdown output
crwl https://example.com -o markdown
# JSON output with cache bypass
crwl https://example.com -o json -v --bypass-cache
# See more examples
crwl --examplePython SDK
import asyncio
from crawl4ai import AsyncWebCrawler
async def main():
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://example.com")
print(result.markdown[:500])
asyncio.run(main())For SDK configuration details: SDK Guide - Configuration.
---
Core Concepts
Configuration Layers
Both CLI and SDK use the same underlying configuration:
| Concept | CLI | SDK |
|---|---|---|
| Browser settings | -B browser.yml or -b "param=value" | BrowserConfig(...) |
| Crawl settings | -C crawler.yml or -c "param=value" | CrawlerRunConfig(...) |
| Extraction | -e extract.yml -s schema.json | extraction_strategy=... |
| Content filter | -f filter.yml | markdown_generator=... |
Key Parameters
Browser Configuration:
headless: Run with/without GUIviewport_width/height: Browser dimensionsuser_agent: Custom user agentproxy_config: Proxy settings
Crawler Configuration:
page_timeout: Max page load time (ms)wait_for: CSS selector or JS condition to wait forcache_mode: bypass, enabled, disabledjs_code: JavaScript to executecss_selector: Focus on specific element
For complete parameters: CLI Config | SDK Config
Output Content
Every crawl returns:
- markdown - Clean, formatted markdown
- html - Raw HTML
- links - Internal and external links discovered
- media - Images, videos, audio found
- extracted_content - Structured data (if extraction configured)
---
Markdown Generation (Primary Use Case)
Crawl4AI excels at generating clean, well-formatted markdown.
CLI
crwl https://docs.example.com -o markdown # raw markdown
crwl https://docs.example.com -o markdown-fit # filtered (noise removed)
crwl https://docs.example.com -f templates/filter_bm25.yml -o markdown-fit # BM25-relevance filter
crwl https://docs.example.com -f templates/filter_pruning.yml -o markdown-fit # quality-based filterFilter templates: `templates/filter_bm25.yml` (relevance-scored against a query), `templates/filter_pruning.yml` (no query, prunes low-quality blocks).
Python SDK
from crawl4ai.content_filter_strategy import BM25ContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
bm25_filter = BM25ContentFilter(user_query="machine learning", bm25_threshold=1.0)
md_generator = DefaultMarkdownGenerator(content_filter=bm25_filter)
config = CrawlerRunConfig(markdown_generator=md_generator)
result = await crawler.arun(url, config=config)
print(result.markdown.fit_markdown) # Filtered
print(result.markdown.raw_markdown) # OriginalFor filter selection and config field reference, see Content Filters.
---
Data Extraction
1. Schema-Based CSS Extraction (Most Efficient)
No LLM required at extract time — fast, deterministic, cost-free. One-time LLM cost to derive the schema, then reuse indefinitely. The bundled scripts split the pipeline by responsibility:
./scripts/generate_schema.py https://shop.example.com "products with name, price, image" shop_schema.json
./scripts/extract_with_schema.py https://shop.example.com shop_schema.json products.jsonOr via the CLI with the YAML strategy template + the saved schema:
crwl https://shop.example.com -e templates/extract_css.yml -s shop_schema.json -o jsonSchema skeleton: `templates/css_schema.json`. Strategy YAML: `templates/extract_css.yml`.
2. LLM-Based Extraction
For one-off / irregular content where a CSS schema is too brittle:
./scripts/extract_with_llm.py https://news.example.com "Extract headlines, dates, summaries" news.jsonOr via the CLI with the strategy template:
crwl https://news.example.com -e templates/extract_llm.yml -o jsonStrategy YAML: `templates/extract_llm.yml`. Pays an LLM call per URL — for repeat extraction, prefer the schema pipeline above.
For extraction strategy reference: Extraction Strategies.
---
Advanced Patterns
Dynamic Content (JavaScript-Heavy Sites)
crwl https://example.com -c "wait_for=css:.ajax-content,scan_full_page=true,page_timeout=60000"
crwl https://example.com -C templates/crawler.yml # all options in a YAML fileCrawler config template: `templates/crawler.yml`.
Multi-URL Processing
./scripts/batch_crawl.py urls.txt --max-concurrent 5 --out batch_markdown/
./scripts/batch_extract.py urls.txt shop_schema.json --max-concurrent 5 --out products.jsonThe two scripts split on responsibility: batch_crawl.py returns markdown per URL; batch_extract.py returns schema-extracted JSON per URL. Python equivalent uses arun_many():
urls = ["https://site1.com", "https://site2.com", "https://site3.com"]
results = await crawler.arun_many(urls, config=config)For batch processing reference: arun_many() Reference.
URL Discovery Before Crawl
When the URL list comes from a sitemap / domain rather than a known list, do discovery first, then feed the result into batch_crawl.py / batch_extract.py. See URL Discovery for the full surface; quick shape:
from crawl4ai import AsyncUrlSeeder, SeedingConfig
seeds = await AsyncUrlSeeder().urls("example.com", SeedingConfig(
source="sitemap+cc", pattern="*/blog/*", query="machine learning", score_threshold=0.3, live_check=True,
))
urls = [s["url"] for s in seeds]AsyncUrlSeeder is best when you want BM25-scored filtering against a query; DomainMapper is best when you want maximum coverage of one domain.
Session & Authentication
Fill the login template, then reuse the session id on subsequent crawls:
crwl https://site.com/login -C templates/login_crawler.yml
crwl https://site.com/protected -c "session_id=user_session"Login template: `templates/login_crawler.yml` (fill in the field-id selectors and the post-login wait condition before use).
For session management reference: Advanced Features.
Anti-Detection & Proxies
crwl https://example.com -B templates/browser.ymlBrowser config template: `templates/browser.yml` (uncomment proxy_config and init_scripts as needed). For pre-page-load script injection (fingerprint patches that must fire before any site script), populate init_scripts: rather than js_code: (which fires after the page loads). proxy_config works with both the browser strategy and the non-browser HTTPCrawlerStrategy — the latter is the cheap path for static fetches behind a corporate proxy.
Full surface (CDP attachment, undetected mode, init script patterns): Anti-Detection.
Rendering Cached HTML (raw: / file://)
If the agent already has HTML in hand (e.g., from defuddle or a previous crawl) and only needs a screenshot, PDF, or MHTML render, skip the network fetch and pass the HTML directly. base_url controls relative-link resolution:
result = await crawler.arun(
url="raw:" + html_string,
config=CrawlerRunConfig(base_url="https://example.com", screenshot=True, pdf=True),
)result = await crawler.arun(
url="file:///path/to/page.html",
config=CrawlerRunConfig(screenshot=True),
)---
Common Use Cases
Eight worked end-to-end flows (docs page, JS-heavy SPA, e-commerce product extraction, news aggregation, topic-bound domain crawl, login-required content, render existing HTML, Q&A) live in Recipes. Pick the recipe closest to the task at hand and adapt.
---
Resources
Provided Scripts
| Script | Responsibility |
|---|---|
scripts/basic_crawler.py <url> | One URL → markdown + screenshot |
scripts/batch_crawl.py <urls.txt> | Many URLs → markdown files |
scripts/batch_extract.py <urls.txt> <schema.json> | Many URLs + schema → JSON |
scripts/generate_schema.py <url> "<instruction>" | Derive a reusable CSS schema (one-time LLM call) |
scripts/extract_with_schema.py <url> <schema.json> | Apply a saved schema (no LLM) |
scripts/extract_with_llm.py <url> "<instruction>" | Per-request LLM extraction (expensive; one-off only) |
Templates
YAML and JSON skeletons users copy and fill. All sit at the skill root under templates/:
| Template | Used for |
|---|---|
templates/browser.yml | BrowserConfig (headless, proxy, user agent, init scripts) |
templates/crawler.yml | CrawlerRunConfig (cache, wait, timeout, JS) |
templates/extract_css.yml | JsonCssExtractionStrategy declaration |
templates/extract_llm.yml | LLMExtractionStrategy declaration |
templates/filter_bm25.yml | BM25 content filter (relevance-scored) |
templates/filter_pruning.yml | Pruning content filter (quality-based, no query) |
templates/login_crawler.yml | Session-establishing login flow |
templates/css_schema.json | CSS schema skeleton |
Reference Documentation
| Document | Purpose |
|---|---|
| CLI Guide | Command-line interface reference |
| SDK Guide | Python SDK quick reference |
| Recipes | Eight worked end-to-end flows |
| URL Discovery | AsyncUrlSeeder, SeedingConfig, DomainMapper |
| Content Filters | BM25 vs Pruning vs LLMContentFilter — when to use which |
| Anti-Detection | init_scripts, proxy_config, undetected mode, CDP attachment |
| Troubleshooting | Symptoms, causes, fixes; what to try before escalating |
| Complete SDK Reference | Full API documentation (5900+ lines) |
| Escalation | Lookup order, iron rule, halt-vs-continue, worked examples |
---
Best Practices
1. Start with CLI for quick tasks, SDK for automation 2. Use schema-based extraction - 10-100x more efficient than LLM 3. Enable caching during development - --bypass-cache only when needed 4. Set appropriate timeouts - 30s normal, 60s+ for JS-heavy sites 5. Use content filters for cleaner, focused markdown 6. Respect rate limits - Add delays between requests
---
Troubleshooting
For symptom → cause → fix tables (JS not loading, bot detection, empty extracted content, session not persisting, slow crawl, schema generation nonsense, post-upgrade regressions), see Troubleshooting. For unknown surface the references don't cover, follow Escalation.
---
For comprehensive API documentation, see Complete SDK Reference.
License
Dual-licensed under MIT OR Apache-2.0 at your option (SPDX: MIT OR Apache-2.0). See LICENSE for the explainer + the carve-out for the upstream-mirrored references/complete-sdk-reference.md.
Summary
<!-- Provide a brief overview of the changes in this PR. What feature/fix/improvement does this introduce?
SCOPE: Describe the net diff only — what the merged result looks like compared to the base branch. NOT commit history, intermediate state, or how the cherry-picks were assembled.
EXCLUDE all verification artifacts:
- Triple-diff output / stats (A, B, C blocks)
- Leak-check output ("no guarded paths leaked", "guard-main-docs runs clean")
- Patch-id cherry-check counts
- Pre-push gate results, CI status, prose-scrub findings
- Any "I ran X and it returned Y" narration
Anomalies get fixed before push, not audit-trailed in the body. -->
Changelog
<!-- CRITICAL: This section is the source of truth for CHANGELOG.md. generate-changelog.py extracts these categorized bullets verbatim into the release changelog. Write carefully — this IS the changelog.
AUDIENCE: Users and operators. Write from their perspective.
INCLUDE: new features, changed behavior, breaking changes, fixed bugs, new/removed config, new dependencies users need to know about.
EXCLUDE: internal refactors, test additions, code cleanup, CI changes, regenerated files, implementation details (unreachable!() arms, import reordering, cargo_bin migration, cfg gates, etc.). Document those in the PR body text or Files Modified section — NOT here.
RULES:
- 1-5 bullets per PR. Fewer is better. One-line fixes get one bullet.
- Delete empty ### sections entirely — don't leave blank categories.
- Each bullet starts with a verb: Add, Fix, Change, Remove, Deprecate.
- Don't duplicate the PR title — expand on it or provide context.
- If the PR has NO user-facing changes (pure refactor, test-only, CI), leave this section empty or omit it. The PR still
appears in git history; it just won't clutter the changelog. -->
Added
-
Changed
-
Fixed
-
Documentation
-
Type of Change
<!-- Check the type that applies to this PR -->
- [ ]
feat: New feature (non-breaking change which adds functionality) - [ ]
fix: Bug fix (non-breaking change which fixes an issue) - [ ]
refactor: Code refactoring (no functional changes) - [ ]
perf: Performance improvement - [ ]
docs: Documentation update - [ ]
test: Adding or updating tests - [ ]
chore: Maintenance tasks (dependencies, config, etc.) - [ ]
ci: CI/CD configuration changes - [ ]
style: Code style/formatting changes - [ ]
build: Build system changes - [ ]
BREAKING CHANGE: Breaking API change (requires major version bump)
Related Issues/Stories
<!-- Link to related issues, stories, or documentation -->
- Story:
- Issue:
- Architecture:
- Related PRs:
Testing
<!-- Describe the testing approach and results -->
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing completed
- [ ] All tests passing
Test Summary:
- Unit tests: X passing
- Integration tests: Y passing
- Coverage: Z%
Files Modified
<!-- List the main files modified in this PR -->
Modified:
Created:
Renamed:
Deleted:
Key Features
<!-- Optional: Highlight key features or capabilities introduced -->
-
Benefits
<!-- Optional: Describe the benefits (performance, security, compliance, UX, etc.) -->
-
Breaking Changes
<!-- If this PR contains breaking changes, describe them and the migration path -->
- [ ] No breaking changes
- [ ] Breaking changes described below:
Deployment Notes
<!-- Any special deployment considerations, migrations, or configuration changes needed -->
- [ ] No special deployment steps required
- [ ] Deployment steps documented below:
Screenshots/Recordings
<!-- Optional: Add screenshots or recordings for UI changes -->
Checklist
- [ ] Code follows project conventions and style guidelines
- [ ] Commit messages follow Conventional Commits
- [ ] Self-review of code completed
- [ ] Tests added/updated and passing
- [ ] No new warnings or errors introduced
- [ ] Changes are backward compatible (or breaking changes documented)
Additional Context
<!-- Optional: Add any additional context, screenshots, or information -->
---
<!-- PR Title Format: <type>(<scope>): <description>
Examples:
- feat(auth): add OAuth2 authentication provider
- fix(api): resolve rate limiting edge case
- docs(readme): update installation instructions
- refactor(db): optimize query performance
- chore(deps): upgrade to bun 1.3.1
-->
**/.DS_Store
/.cursor
# Script outputs written to cwd by scripts/ templates.
# Ignored regardless of which subdir the scripts are invoked from.
output.md
screenshot.png
batch_results.json
batch_extracted.json
batch_markdown/
generated_schema.json
extracted_data.json
manual_extracted.json
llm_extracted.json
# Global markdownlint-cli2 configuration
# Canonical version: 2026.06.09
# Symlinked to ~/.markdownlint-cli2.yaml via stow
#
# Per-repo copies should preserve the `Canonical version` line above and bump it on resync;
# a stale calver compared to this file is the signal that the per-repo copy has drifted.
#
# Documentation: https://github.com/DavidAnson/markdownlint-cli2
# Rules: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
# Configure markdownlint rules
config:
# Use all default rules
default: true
# MD003: Heading style - use ATX style (#)
MD003:
style: "atx"
# MD004: Unordered list style - use dashes
MD004:
style: "dash"
# MD007: Unordered list indentation - 2 spaces
MD007:
indent: 2
# MD009: Trailing spaces - allow 2 spaces for line breaks
MD009:
br_spaces: 2
# MD013: Line length - 120 chars (more reasonable for code docs)
MD013:
line_length: 120
code_blocks: false # Don't check code blocks
tables: false # Don't check tables
headings: false # Don't check headings
# MD024: Allow duplicate headings in different sections
MD024:
siblings_only: true
# MD025: Single top-level heading - allow multiple (for changelogs, etc.)
MD025: false
# MD033: Allow inline HTML for specific elements
MD033:
allowed_elements:
- "br"
- "img"
- "a"
- "details"
- "summary"
- "sub"
- "sup"
- "kbd"
# MD034: Bare URLs - allow (common in docs)
MD034: false
# MD036: Emphasis used as heading - allow (stylistic choice)
MD036: false
# MD041: First line should be top-level heading - disable (not always needed)
MD041: false
# MD046: Code block style - fenced
MD046:
style: "fenced"
# MD048: Code fence style - backticks
MD048:
style: "backtick"
# Ignore patterns
ignores:
- "node_modules/**"
- "**/node_modules/**"
- "vendor/**"
- "target/**" # Rust build artifacts; harmless for non-Rust projects
- ".git/**"
- "*.min.md"
- "docs/solutions/**" # Symlink to the shared solutions-docs repo; lint upstream, not here
# Fix automatically when --fix is used
fix: true
AGENTS.md
This repo is a portable agent skill in the Anthropic SKILL.md format. The bundle layout, install paths, and consumer-facing contract live in README.md. The skill itself, with all triggers, defaults, and routing, lives in SKILL.md. The contributor flow (issues, branches, PR conventions) lives in CONTRIBUTING.md. Read those before making changes.
Bundle conventions
- The skill is verified against the Crawl4AI library version pinned in `VERSION`. Bumping that version
requires verifying every reference doc, every script, and every eval still works against the new release.
- Defaults are settled. The current JS-render wait is
wait_until=networkidle. Do not regress towait_for=css:body;
the <body> element exists at t=0 on every HTML response, so it adds no real wait.
- Bundled scripts use PEP 723 inline metadata so consumers can run them via
uv run. Keep the dependency floor in step
with VERSION.
- The schema-generation fixture under
fixtures/is the contract forscripts/generate_schema.py. If you change the
schema format, update the fixture and the corresponding test in tests/test_fixtures.py.
Repo conventions
- Branch discipline:
mainis protected. Code changes go through afeat/...orfix/...branch and a PR. Doc-only
edits (README, AGENTS, CHANGELOG) may land directly on main.
- Conventional Commits in commit subjects. Use
feat:orfix:for anything user-observable (default behavior, script
names, reference content, bundle layout). chore:, style:, test:, ci:, build: are excluded from the changelog by the parser this repo targets.
- License: dual Apache-2.0 OR MIT (SPDX
MIT OR Apache-2.0). Contributions are accepted under this license.
Testing
cd tests
python run_all_tests.pyThe suite covers basic crawling, markdown generation, extraction, advanced patterns, and the schema-generation fixture.
Quality bar
- Markdown is linted via
markdownlint-cli2against.markdownlint-cli2.yaml. - Reference docs are the contract for the skill's behavior. Touch
complete-sdk-reference.mdsurgically; it tracks the
upstream library API.
When NOT to edit
- Do not edit
LICENSE,LICENSE-APACHE, orLICENSE-MITindependently of a deliberate relicense. - Do not bump
VERSIONwithout verifying the bundle still works against the new library version. - Do not add
wait_for=css:bodyto any new script or example.
Changelog
All notable changes to this skill will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.0.1] - 2026-06-16
Docs-only patch. Repositions the repo as a portable agent skill (not Claude-specific) and adds project-level contributor documentation.
Added
AGENTS.mdat the repo root: project-level instructions for agents working inside the repo. YAML frontmatter (name,
description, repository, license, verified-against), bundle conventions (VERSION pin discipline, wait_until=networkidle default, PEP 723 scripts, schema-generation fixture contract), repo conventions (main-protected branch discipline, Conventional Commits, dual-license acceptance), testing command, quality bar, and a "When NOT to edit" block.
CONTRIBUTING.mdat the repo root: minimal contributor flow covering issue filing, branch naming, testing, PR
conventions, and scope bounding (upstream Crawl4AI library bugs route to the upstream repo, not this one).
Changed
README.mdoverhauled for portable agent-skill framing. Title becomesCrawl4AI Agent Skill. Lead paragraph names
the agent-host audience explicitly (Claude Code, Codex, Cursor, OpenCode, Cline, and other SKILL.md-format hosts).
- Install section: single host-agnostic
git clone <repo> <host-skills-dir>/crawl4aipath, illustrated with the Claude
Code path. Claude Desktop staging-dir zip flow removed.
- Documentation section now links all nine reference guides.
- Adds a Bundle layout table documenting
SKILL.md,references/,scripts/,templates/,evals/,fixtures/,
tests/, and the VERSION pin.
- Marketplace and Support sections removed.
[2.0.0] - 2026-06-16
Major release. Verified against the Crawl4AI library at version 0.8.9 (recorded in VERSION). The repo is now the skill bundle: SKILL.md and the skill directories live at the repo root and load directly with git clone <repo> ~/.claude/skills/crawl4ai.
Added
VERSIONfile pinning the crawl4ai library version the skill is verified against.templates/directory carrying browser, crawler, content-filter, and extraction YAML configs the scripts and SKILL.md
reference (browser.yml, crawler.yml, extract_css.yml, extract_llm.yml, filter_bm25.yml, filter_pruning.yml, login_crawler.yml, css_schema.json).
evals/directory carrying four eval scenarios for verifying skill behavior, with a README.fixtures/directory carrying the schema-generation reference fixture (sample product HTML, expected schema, expected
JSON output), with a README.
- Reference guides under
references/:anti-detection.md,content-filters.md,escalation.md,recipes.md,
troubleshooting.md, url-discovery.md.
- Focused scripts under
scripts/:batch_crawl.py,batch_extract.py,generate_schema.py,
extract_with_schema.py, extract_with_llm.py.
- Test coverage for fixtures under
tests/test_fixtures.py. LICENSE-APACHEandLICENSE-MITcarrying the full license texts;LICENSEsummarizes the dual-license model (SPDX
MIT OR Apache-2.0).
Changed
- Layout: the skill bundle lives at the repo root. The previous
crawl4ai/wrapper directory is gone.SKILL.md
and its companion directories are direct children of the repo root.
- Default JS-render wait switches from
wait_for=css:bodytowait_until=networkidleacrossSKILL.mdand every
bundled script. wait_for=css:body was satisfied at t=0 on every HTML response and added no real wait; wait_until=networkidle waits for ~500ms of network quiet post-load.
- License: dual Apache-2.0 OR MIT (was MIT-only). Existing MIT consumers retain MIT terms.
- CLI guide drift fixes: the deprecated
anthropic/claude-3-sonnetLLM provider reference is replaced with a
pointer to the LiteLLM provider list, the LLM-extraction example no longer passes a stray -s llm_schema.json (the schema lives inside extract_llm.yml), and the filter_pruning.yml snippet drops the query key (pruning filters do not take one) and adds the required threshold_type: fixed.
SKILL.md, the existing reference guides, the scripts, the tests, and the evals all refresh against Crawl4AI 0.8.9.README.mdinstall section: singlegit cloneinto~/.claude/skills/crawl4aifor Claude Code; staging-dir zip
snippet for Claude Desktop that includes templates/, evals/, fixtures/, and VERSION. Helper-scripts list and E-commerce Product Monitoring example now match the new script names. License section reflects the dual-license model.
.gitignoreextended with the script-output patterns (output.md,screenshot.png,batch_results.json,
batch_extracted.json, batch_markdown/, generated_schema.json, extracted_data.json, manual_extracted.json, llm_extracted.json) so script invocations from the repo root no longer pollute the working tree.
Removed
marketplace/metadata.json. No Claude consumer reads it; the data it carried lives on SKILL.md frontmatter, GitHub
topics, README, and LICENSE.
- The prebuilt
crawl4ai.zipartifact. README documents how to build the zip from the tree on demand. - The monolithic
scripts/extraction_pipeline.py. Its responsibilities split acrossscripts/generate_schema.pyand
scripts/extract_with_schema.py.
scripts/batch_crawler.py. Replaced byscripts/batch_crawl.py.
Breaking Changes
- Symlinks pointing into the previous
crawl4ai-skill/crawl4ai/no longer resolve; repoint at the repo root. - Script callers referencing
scripts/extraction_pipeline.pyswitch toscripts/generate_schema.pyplus
scripts/extract_with_schema.py. Callers of scripts/batch_crawler.py switch to scripts/batch_crawl.py.
- License changes from MIT-only to dual Apache-2.0 OR MIT.
[1.0.0] - 2025-12-02
Added
- Initial release of Crawl4AI Claude Skill
- Complete skill documentation in
SKILL.md - CLI guide with comprehensive command-line interface reference
- SDK guide with Python SDK quick reference
- Complete SDK reference documentation (5900+ lines)
- Helper scripts:
basic_crawler.py- Simple markdown extractionbatch_crawler.py- Multi-URL processing with concurrencyextraction_pipeline.py- Schema generation and extraction pipeline- Test suite with comprehensive coverage:
- Basic crawling tests
- Markdown generation tests
- Data extraction tests
- Advanced patterns tests (sessions, proxies, batch crawling)
Features
- Web crawling with full JavaScript support
- Schema-based CSS extraction (LLM-free, 10-100x more efficient)
- LLM-based extraction for complex content
- Markdown generation with content filtering
- Session management for authenticated crawling
- Batch/concurrent URL processing
- Both CLI and Python SDK interfaces
- Comprehensive documentation and examples
Documentation
- Complete skill instructions in
SKILL.md - CLI reference guide
- SDK reference guide
- Full API documentation
- Usage examples for common scenarios
- Troubleshooting guide
Contributing
Contributions are welcome under the dual Apache-2.0 OR MIT license that the rest of the repo carries (SPDX MIT OR Apache-2.0). Opening a pull request signals you agree to license your contribution under those same terms.
Project-level conventions for agents and humans working inside the repo live in AGENTS.md; the consumer- facing surface (install paths, bundle layout, scripts) lives in README.md; the skill itself lives in SKILL.md. Read whichever is relevant before you start.
Filing issues
Open an issue at <https://github.com/brettdavies/crawl4ai-skill/issues>. Include reproducer steps, the agent host you ran the skill in (Claude Code, Codex, Cursor, OpenCode, Cline, or other), and the Crawl4AI library version on your system (pip show crawl4ai or check the value pinned in `VERSION`).
Proposing changes
1. Cut a branch from main named feat/<topic> or fix/<topic>. 2. Make the change. Run the tests under tests/ and confirm they still pass. 3. Open a PR against main. The repo standard PR template loads automatically; fill it in.
Conventions
- Commit subjects: Conventional Commits (
type(scope): description). Usefeat:orfix:for anything
user-observable (default behavior, script names, reference content, bundle layout). chore:, style:, test:, ci:, build: are excluded from the changelog.
- Prose: keep README and
SKILL.mdtight. Avoid em-dash density above 3 per 1000 words, "It's not X, it's Y"
constructions, and filler openers.
- Markdown: passes
markdownlint-cli2against the repo's.markdownlint-cli2.yaml. - Library pin: if your change requires a different Crawl4AI library version, update `VERSION` and verify
every reference doc, every script, and every eval against the new release before opening the PR.
Scope
This bundle wraps Crawl4AI for agent hosts. Upstream library bugs belong at the Crawl4AI repo, not here. Open issues here for: bundle layout, SKILL.md routing, reference doc accuracy, helper script behavior, fixture / eval coverage, or skill-format compatibility with a specific agent host.
Eval 01 — Get markdown from a JS-heavy page
You are a fresh Claude Code agent. Workdir: /tmp/crawl4ai-eval-01-<timestamp>/.
Task
A user comes to you with this URL: https://app.example.com/dashboard. They say:
"This page is built in React and curl just returns the empty shell. I need therendered markdown of what's actually on the page. The content I care about lives
inside an element with class .results-grid. Get it for me."Plan the approach, then execute. Capture the markdown to <workdir>/dashboard.md.
Required artifacts in your workdir
PLAN.md— your routing decision: which tool/skill, why, and which configuration knobs you turned and why.INVOCATION.sh— the exact shell command(s) you ran (or would run, in dry-run form if you can't actually reach the
URL).
dashboard.md— the captured markdown. If you couldn't actually fetch the URL, a stub explaining what the output
shape would be is acceptable; mark this explicitly in FINAL-REPORT.md.
FINAL-REPORT.md— your self-assessment against the success criteria below, plus any dead-ends documented.
Success criteria (score 0-10 each in FINAL-REPORT.md)
1. Discovery — Did you pick the right skill / tool from the user-task phrasing without being told its name? (0 if you asked the user; 10 if the trigger keywords clearly routed you to a single right choice.) 2. Wait strategy — Did you set a wait condition that targets the .results-grid element specifically, OR did you justify NOT doing so? (0 if you ran with default timing; 10 if you set wait_for=css:.results-grid or a defensible JS predicate variant.) 3. Timeout — Did you set page_timeout to a value appropriate for a JS-rendered page (≥30s, typically 60s)? (0 if you used the default 30s without consideration; 10 if you chose a value and explained why.) 4. Output format — Did you request markdown (not HTML, not JSON)? (0 if you returned the wrong format; 10 if you picked -o markdown or the SDK equivalent.) 5. Skill routing — Did you correctly identify that this is a job for the JS-capable scraper, NOT a static-HTML extractor? (0 if you tried defuddle first; 10 if you correctly skipped the static path.)
Document dead-ends
If you tried a wait strategy that didn't work, name it in FINAL-REPORT.md § "Dead ends" with the symptom and your hypothesis for why. This is how the next eval round avoids re-deriving the same wrong answer.
What NOT to do
- Don't ask the user which tool to use — the trigger keywords should route you.
- Don't fall back to
WebFetchorcurl— both fail on JS-rendered content, which is the entire point of this eval. - Don't skip the
wait_for— a JS-heavy page with no wait condition is a guaranteed empty result.
Eval 02 — Extract structured products from an e-commerce site
You are a fresh Claude Code agent. Workdir: /tmp/crawl4ai-eval-02-<timestamp>/.
Task
A user comes to you with this URL: https://shop.example.com/category/widgets. They say:
"I need to pull all the products on this page into a JSON list. Each product
should havename,price, and alinkto the detail page. This is a one-off
right now, but I want to repeat it across maybe a dozen URLs later this week
without paying per-page LLM costs."
Plan the approach, then execute.
Required artifacts in your workdir
PLAN.md— your routing decision. Specifically: did you choose (a) LLM extraction per-URL, (b) a CSS schema generated
once and reused, or (c) a hand-written schema? Justify with the user's stated constraint ("dozen URLs", "without paying per-page LLM costs").
INVOCATION.sh— exact commands.shop_schema.json— the schema you derived (LLM-generated or hand-written).products.json— the extracted result. If you can't reach the URL, a stub with the expected shape is acceptable; mark
this in FINAL-REPORT.md.
FINAL-REPORT.md— self-assessment.
Regression-test prior fixes
This eval runs after eval-01. Verify that the eval-01 fixes are still in place:
- Wait strategy is documented somewhere — the recipes / troubleshooting references should still cover JS-rendered
pages. (worked / regressed / not-touched.)
Classify in FINAL-REPORT.md § 4. Any regressed finding is blocking.
Success criteria (score 0-10 each)
1. Pipeline choice — Did you choose the schema-generation + schema-reuse pipeline (option b), based on the user's stated "repeat across a dozen URLs without per-page LLM costs"? (0 if you chose option a; 10 if option b with reasoning.) 2. Schema derivation — Did you use the dedicated schema-generation script (one LLM call), OR did you hand-write the schema? Either is acceptable IF you noted the trade-off; 0 if you re-used LLM extraction for every URL instead. 3. Schema validation — Did you sanity-check the derived schema against ONE URL before declaring it done? A schema that returns [] on the first real page is a silent failure mode. (0 if you skipped validation; 10 if you ran a single-URL extract first and inspected the result.) 4. Batch-ready — Did your invocation handle the "dozen URLs later this week" use case (i.e. did you point at a script / config that takes a URL list as input)? (0 if your output only handles one URL; 10 if the next call is <batch-tool> urls.txt schema.json.) 5. Schema field names — Do the field names in your schema match what the user asked for (name, price, link)? (0 if you renamed; 10 if exact match.)
Document dead-ends
If the schema-generation step produced a schema with the wrong baseSelector, name the symptom in FINAL-REPORT.md § "Dead ends" and your fix.
What NOT to do
- Don't pay an LLM call per URL when the user explicitly said "without paying per-page LLM costs."
- Don't hand-write a schema before checking whether the bundled schema-generation script exists.
- Don't return JSON without validating the schema produces non-empty output on a real page first.
Eval 03 — Topic-bound crawl of a whole domain
You are a fresh Claude Code agent. Workdir: /tmp/crawl4ai-eval-03-<timestamp>/.
Task
A user says:
"Get me clean markdown from every blog post on example.com that's aboutmachine learning. I don't have the URL list — start from the domain. I care
about relevance, not coverage; skip pages that mention ML once in passing."
Plan the approach, then execute.
Required artifacts in your workdir
PLAN.md— describe the two distinct phases (URL discovery, then content crawl), and which tool/approach you picked
for each.
INVOCATION.sh— exact commands (or dry-run equivalents).urls.txt— the discovered, filtered URL list.ml_markdown/— directory of.mdfiles, one per URL. Stub acceptable if you can't reach the network.FINAL-REPORT.md— self-assessment.
Regression-test prior fixes
After eval-01 and eval-02. Verify each by file + section + expected substance:
- Schema-generation pipeline (eval-02).
references/recipes.md§ 3 ("E-commerce product list across many URLs")
still names scripts/generate_schema.py → scripts/extract_with_schema.py → scripts/batch_extract.py in that order. Classify worked / regressed / not-touched.
- JS-rendered page handling (eval-01).
references/troubleshooting.md§ "JavaScript content not loading" still
documents wait_for=css:<selector> and the JS-predicate fallback. Classify same.
- Default routing is not the placebo wait (eval-01).
SKILL.md§ "Invoked with a URL argument" uses
wait_until=networkidle (or stricter), not wait_for=css:body. Classify same.
Any regressed is blocking.
Success criteria (score 0-10 each)
1. Two-phase architecture — Did you split URL discovery from page crawl, rather than trying to do both in one pass? (0 if you tried to combine; 10 if discovery → urls.txt → batch crawl was your shape.) 2. Discovery tool choice — Did you pick the relevance-scored URL discovery surface (not the maximum-coverage one), based on the user's stated preference for relevance over coverage? (0 if you picked the coverage tool and tried to filter post-hoc; 10 if you picked the BM25-scored seeder with query + score_threshold.) 3. Pattern filter — Did you also constrain the URL discovery by URL path pattern (e.g. */blog/*) to skip non-blog pages? (0 if no path filter; 10 if you added a sensible glob.) 4. Score threshold — Did you set a score_threshold to drop URLs that mention ML only in passing? (0 if you took everything; 10 if you set a non-trivial threshold and explained the reasoning.) 5. Batch composition — Did the discovery output flow naturally into the batch crawl (i.e. urls.txt is the input to the next step, not a manual copy-paste)? (0 if you reformatted between steps; 10 if the handoff is clean.)
Dry-run gate
If you can't actually reach the network, you still must execute the discovery call against a stub or short-circuit fixture — not just print the planned command. Capture stdout + stderr + exit code from python -c "..." (or ./scripts/...) into dryrun-output.txt. The point is to surface API-shape bugs (wrong SeedingConfig field name, score_threshold type mismatch) that "printing the plan" hides. Score caps at 5 if dryrun-output.txt is missing or only contains a planned-command echo with no actual interpreter output.
What NOT to do
- Don't try to manually enumerate blog URLs by guessing patterns. The skill has URL-discovery surfaces for exactly this
case.
- Don't run an LLM call against every page just to check "is this about ML?" — the discovery layer's BM25 scoring solves
this cheaper.
- Don't skip the path pattern. Without
pattern="*/blog/*", you'll get the about page, the contact page, and every
other URL in the sitemap.
Eval 04 — Render a screenshot from HTML I already have
You are a fresh Claude Code agent. Workdir: /tmp/crawl4ai-eval-04-<timestamp>/.
Task
A user says:
"I already pulled this page's HTML into a variable — it's about 80KB. I just
need a screenshot of how it would render. I don't want to make another network
fetch. The page uses relative image URLs against
https://example.com/ for resolution. And I need the full page, not justwhat fits in the default viewport — it's a long article."
The HTML string is supplied as an environment variable HTML_CONTENT (~80KB). For this eval, assume it represents a real rendered page.
Plan the approach, then execute.
Required artifacts in your workdir
PLAN.md— the routing decision. Specifically, explain why you do or do not do a network fetch.INVOCATION.py(orINVOCATION.sh) — the exact code/command. Inline the HTML loading fromHTML_CONTENTrather than
fetching from network.
screenshot.png— the captured render. Stub acceptable if the workdir can't actually run crawl4ai; mark in
FINAL-REPORT.md.
FINAL-REPORT.md— self-assessment.
Regression-test prior fixes
After evals 01-03. Verify each by file + section + expected substance:
- Schema pipeline (eval-02).
references/recipes.md§ 3 still names thegenerate_schema.py→
extract_with_schema.py → batch_extract.py flow. Classify worked / regressed / not-touched.
- URL discovery (eval-03).
references/url-discovery.mdstill surfacesAsyncUrlSeeder+SeedingConfigwith the
query + score_threshold fields. Classify same.
- JS-rendered page handling (eval-01).
references/troubleshooting.md§ "JavaScript content not loading" still
documents wait_for=css:<selector> and the JS-predicate fallback. Classify same.
Any regressed is blocking.
Success criteria (score 0-10 each)
1. No-network choice — Did you choose a code path that does NOT fetch https://example.com/ again? (0 if you fetched; 10 if you used the raw: or file:// URL form.) 2. `raw:` vs `file://` — Did you pick raw: (HTML in hand as a string) over file:// (you'd have to write the HTML to disk first)? (0 for unnecessary disk I/O; 10 for raw: direct.) 3. `base_url` set — Did you set base_url="https://example.com/" so the relative image URLs in the HTML resolve correctly? (0 if you forgot; 10 if set and explained.) 4. Screenshot requested — Did you configure the crawler to actually emit the screenshot (e.g. screenshot=True)? (0 if you got markdown / HTML back without a screenshot; 10 if result.screenshot is the payload.) 5. Skill routing — Did you correctly identify this as a job for the browser-render tool (not a static-HTML extractor; not a markdown converter)? (0 if you tried defuddle or markdown-convert; 10 if you correctly routed to the browser-render path.) 6. Full-page screenshot escalation — Did you escalate to find the full-page-screenshot field name rather than guess it? (0 if you guessed and were wrong, 5 if you guessed and happened to be right, 10 if you verified the field name via the complete reference, upstream docs, or library introspection before using it.) 7. Full-page dimensional proof — Report the rendered PNG's (width, height) to FINAL-REPORT.md § "Dimensions" (one-line PIL or identify invocation is fine). A viewport-only screenshot is height = viewport_height (default 1080); a full-page screenshot is materially taller. Score 0 if screenshot.png is missing or you reported only "the call succeeded" without dimensions; 5 if the call succeeded but height ≤ viewport_height (you got the viewport, not the full page); 10 if height >> viewport_height AND you reported the numbers.
Forced escalation
The user's "full page, not just the viewport" requirement is not covered by the skill's references — recipes.md shows screenshot=True but does not surface the full-page vs viewport-only field name. You are required to escalate to find it: check complete-sdk-reference.md for CrawlerRunConfig screenshot-related fields, fall back to upstream docs, and finally introspect the installed library if neither resolves it (python -c "from crawl4ai import CrawlerRunConfig; help(CrawlerRunConfig.__init__)" | grep -i screenshot).
Document in FINAL-REPORT.md § "Escalation" the lookup path you took and the actual field name you used. Per the escalation iron rule: do not guess a field name like full_page=True or screenshot_full_page=True from training data without verification. Failing to escalate (and guessing) caps the score at 5 even if the guess happens to be right.
What NOT to do
- Don't fetch the URL again. The user explicitly said they have the HTML and don't want another fetch.
- Don't write the HTML to a temp file just to pass
file://—raw:exists exactly to skip that step. - Don't ignore
base_url. Relative image URLs render as broken images without it; the screenshot will look wrong even
if the call "succeeds."
Evals
Each eval-*.md is a self-contained prompt for a fresh agent: no project context, no skill name, no underlying tool name. The agent must discover the right tool from the user-task phrasing alone — that's the discovery test.
Workdirs land in /tmp/crawl4ai-eval-<id>/, never committed.
| Eval | Tests |
|---|---|
| eval-01-spa-markdown.md | Discovery + wait_for strategy for JS-rendered pages |
| eval-02-extract-products.md | Schema generation pipeline + CSS extraction routing |
| eval-03-topic-domain-crawl.md | URL discovery → filtering → batch crawl composition |
| eval-04-render-cached-html.md | raw: URL rendering for screenshot from existing HTML |
Running an eval
Dispatch the prompt to a fresh agent (e.g. via Claude Code in a new session, or the Agent tool with isolation: "worktree"). The eval should:
1. Discover the relevant skill from its frontmatter description. 2. Read the skill's references as needed (escalation, recipes, the topic-specific reference). 3. Produce the required artifacts in its workdir. 4. Self-score against the success criteria. 5. Document any dead-ends in FINAL-REPORT.md.
A passing eval scores ≥7/10 on each numbered criterion AND surfaces no silent-failure modes the prompt names. Any score below 7 OR any regression vs the previous eval round is a blocking finding.
When to re-run
- After bumping
VERSIONand the PEP 723 pins (catches regressions from the upstream library upgrade). - After substantive SKILL.md or references edits (catches discoverability or routing regressions).
- Before declaring a major refactor done.
Fixtures
Deterministic before/after pair for scripts/extract_with_schema.py. The runner lives at tests/test_fixtures.py and ships in the standard test suite — invoke directly or via tests/run_all_tests.py.
./tests/test_fixtures.py # exit 0 on match, 1 on diff
./tests/run_all_tests.py # runs this fixture pair plus the othersPass means the wrapper script + the installed library still produce the documented output shape against fixed HTML. A diff means either the upstream library's scraping fidelity drifted (regression), the schema is wrong, or the fixture HTML changed shape. Bump VERSION only after this passes against the new library version.
| File | Role |
|---|---|
sample-products.html | Fixed HTML input. Three .product-card items. |
sample-products-schema.json | The JsonCssExtractionStrategy schema. |
sample-products-expected.json | Expected extracted output. |
[
{"title": "Widget A", "price": "$10.00", "link": "/products/widget-a"},
{"title": "Widget B", "price": "$20.00", "link": "/products/widget-b"},
{"title": "Widget C", "price": "$30.00", "link": "/products/widget-c"}
]
{
"name": "products",
"baseSelector": ".product-card",
"fields": [
{"name": "title", "selector": ".title", "type": "text"},
{"name": "price", "selector": ".price", "type": "text"},
{"name": "link", "selector": "a.link", "type": "attribute", "attribute": "href"}
]
}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Fixture Shop</title></head>
<body>
<main>
<article class="product-card">
<h2 class="title">Widget A</h2>
<span class="price">$10.00</span>
<a class="link" href="/products/widget-a">Details</a>
</article>
<article class="product-card">
<h2 class="title">Widget B</h2>
<span class="price">$20.00</span>
<a class="link" href="/products/widget-b">Details</a>
</article>
<article class="product-card">
<h2 class="title">Widget C</h2>
<span class="price">$30.00</span>
<a class="link" href="/products/widget-c">Details</a>
</article>
</main>
</body>
</html>
# License
This skill is dual-licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT License ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option. SPDX identifier: `MIT OR Apache-2.0`.
Every script (`scripts/*.py`, `tests/*.py`) and YAML template (`templates/*.yml`) carries a
`# SPDX-License-Identifier: MIT OR Apache-2.0` line at the top so the dual choice travels with
the file when it gets copied out of the bundle. `templates/css_schema.json` is the one
exception — JSON has no comment syntax, so the license metadata for that file lives in this
LICENSE file alone.
## What this means in practice
You can use this skill — the SKILL.md, references, templates, scripts, fixtures,
and evals — in your own projects (commercial or open source) under whichever of
the two licenses suits you better. You only need to comply with one, not both.
Both licenses require attribution: keep the copyright notice somewhere in your
distribution. Apache-2.0 adds an explicit patent grant; MIT is shorter. Pick the
one your project ecosystem prefers (Apache-2.0 is conventional in Java / Go /
larger corporate contexts; MIT is conventional in JavaScript / smaller projects).
## Third-party content
- `references/complete-sdk-reference.md` is a verbatim mirror of upstream
`unclecode/crawl4ai/docs/md_v2/complete-sdk-reference.md` (pinned to commit
`3a75dd3`). Provenance and refresh instructions are in the file's leading HTML
comment. That file inherits its upstream project's license; check the
Crawl4AI repository for terms before redistributing it in isolation. Every
other file under `crawl4ai/` is original work covered by the dual license
above.
## Contribution
Any contribution submitted for inclusion in this skill — including via pull
request, issue, or any other means — is offered under the same dual license
terms, without any additional terms or conditions per Apache-2.0 § 5.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Support. While redistributing the Work or
Derivative Works thereof, You may choose to offer, and charge a
fee for, acceptance of support, warranty, indemnity, or other
liability obligations and/or rights consistent with this License.
However, in accepting such obligations, You may act only on Your
own behalf and on Your sole responsibility, not on behalf of any
other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by,
or claims asserted against, such Contributor by reason of your
accepting any such warranty or support.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Brett Davies
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
MIT License
Copyright (c) 2026 Brett Davies
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Crawl4AI Agent Skill
Scrape JavaScript-heavy sites and extract structured data via reusable CSS schemas. A portable agent skill that wraps the Crawl4AI CLI and Python SDK, written in the Anthropic SKILL.md format and consumable by any agent host that loads SKILL.md-format bundles (Claude Code, Codex, Cursor, OpenCode, Cline, and others).
Verified against Crawl4AI library version 0.8.9 (pinned in `VERSION`).
Features
- JS-aware crawling: full headless-browser rendering with
wait_until=networkidledefaults - Schema-based extraction: derive a CSS selector schema once via LLM, apply it forever with no further LLM cost
- LLM extraction: per-request structured extraction when a schema is not worth deriving
- Content filtering: BM25 relevance filter and quality-based pruning, plain markdown or markdown-fit output
- Concurrent batch crawling: multi-URL processing with per-job concurrency caps
- Session management: persistent sessions for authenticated, multi-step flows
- CLI and SDK: both the
crwlcommand-line tool and thecrawl4aiPython SDK
Installation
Clone the repo into the skills directory your agent host loads from:
# Claude Code
git clone https://github.com/brettdavies/crawl4ai-skill.git ~/.claude/skills/crawl4aiFor other agent hosts (Codex, Cursor, OpenCode, Cline, custom agents), clone into whichever directory your host scans for SKILL.md-format bundles. Refer to your host's documentation for the skills directory location. The bundle root contains SKILL.md, so the skill registers automatically once the directory is on the host's skills search path.
Prerequisites
The skill calls into the Crawl4AI Python library, which must be installed in the runtime your agent uses:
pip install crawl4ai
crawl4ai-setup
crawl4ai-doctorcrawl4ai-doctor validates the install and confirms a headless browser is available.
Quick start
CLI:
crwl https://example.com -c "wait_until=networkidle,page_timeout=60000" -o markdown
crwl https://example.com -o json -v --bypass-cachePython SDK:
import asyncio
from crawl4ai import AsyncWebCrawler
async def main():
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://example.com")
print(result.markdown[:500])
asyncio.run(main())Bundle layout
| Path | Contents |
|---|---|
SKILL.md | Entry point: trigger conditions, defaults, routing to specialized pipelines |
references/ | Nine reference guides for CLI, SDK, extraction, filtering, anti-detection, URL discovery, escalation |
scripts/ | Six PEP 723 helper scripts for crawl / extract / batch workflows |
templates/ | Reusable YAML/JSON templates for browser, crawler, filters, and extraction strategies |
evals/ | Four eval scenarios for verifying skill behavior end-to-end |
fixtures/ | Schema-generation reference fixture (sample HTML, expected schema, expected JSON output) |
tests/ | Pytest suite covering basic crawling, markdown generation, extraction, advanced patterns, and fixtures |
VERSION | Pinned Crawl4AI library version the skill is verified against |
LICENSE-APACHE, LICENSE-MIT, LICENSE | Dual license texts and summary (SPDX MIT OR Apache-2.0) |
Documentation
- SKILL.md: complete skill documentation with examples
- CLI Guide: command-line interface reference
- SDK Guide: Python SDK quick reference
- Complete SDK Reference: full API documentation (5900+ lines)
- Recipes: end-to-end task recipes (login flow, sitemap crawl, paginated extraction)
- Content Filters: BM25 vs pruning vs LLMContentFilter trade-offs
- URL Discovery: sitemap, robots.txt, link-graph traversal
- Anti-Detection: init scripts, proxy config, undetected mode, CDP attachment
- Troubleshooting: symptoms, causes, fixes
- Escalation: lookup order, halt-vs-continue criteria, worked examples
Common use cases
Documentation to markdown
crwl https://docs.example.com -o markdown > docs.mdE-commerce product monitoring
# Derive the schema once (uses LLM)
./scripts/generate_schema.py https://shop.example.com "products with name, price, image" shop_schema.json
# Apply the saved schema (no LLM cost per request)
./scripts/extract_with_schema.py https://shop.example.com shop_schema.json products.jsonNews aggregation with relevance filtering
for url in news1.com news2.com news3.com; do
crwl "https://$url" -f templates/filter_bm25.yml -o markdown-fit
doneScripts
| Script | Purpose |
|---|---|
scripts/basic_crawler.py <url> | One URL → markdown + screenshot |
scripts/batch_crawl.py <urls.txt> | Many URLs → markdown files |
scripts/batch_extract.py <urls.txt> <schema.json> | Many URLs + schema → JSON |
scripts/generate_schema.py <url> "<instruction>" | Derive a reusable CSS schema (one-time LLM call) |
scripts/extract_with_schema.py <url> <schema.json> | Apply a saved schema (no LLM) |
scripts/extract_with_llm.py <url> "<instruction>" | Per-request LLM extraction (expensive; one-off only) |
Testing
cd tests
python run_all_tests.pyLicense
Dual-licensed under Apache License 2.0 (LICENSE-APACHE) or MIT License (LICENSE-MIT) at your option. SPDX identifier: MIT OR Apache-2.0. See LICENSE for the full notice.
Contributing
Contributions welcome. Open a pull request.
Changelog
See CHANGELOG.md.
Anti-Detection
When a site rejects the crawl with a Cloudflare interstitial, a 403, an empty response, or a "human verification" page, the question is which layer is detecting you. Four layers, in increasing order of expense.
| Layer | Mechanism | Mitigation |
|---|---|---|
| Header / fingerprint | Server checks User-Agent, header order, missing browser headers | Browser strategy with user_agent_mode: "random" |
| JavaScript fingerprint | Page-side JS checks navigator.webdriver, canvas, WebGL, font metrics | BrowserConfig.init_scripts to patch detection surfaces before site JS runs |
| Behavioural | Click rate, scroll patterns, mouse-move absence | Undetected mode (Patchright) with random pacing |
| IP reputation | Rate limits or blocklists on hosting / datacentre IPs | proxy_config routed through a residential proxy |
Apply mitigation in this order — each layer up adds cost and complexity.
Layer 1 — Header / fingerprint
The browser strategy already sets realistic headers. For sites that block known datacentre User-Agent strings, rotate with user_agent_mode: "random":
# templates/browser.yml
headless: true
user_agent_mode: "random"
viewport_width: 1920
viewport_height: 1080crwl https://example.com -B templates/browser.ymlLayer 2 — JavaScript fingerprint
BrowserConfig.init_scripts runs before any site JavaScript. This is the right place for patches like masking navigator.webdriver. Compare to CrawlerRunConfig.js_code, which runs after the page loads — too late for fingerprint detection that fires at load time.
from crawl4ai import BrowserConfig
browser_config = BrowserConfig(
headless=True,
init_scripts=[
# Mask the webdriver flag
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})",
# Spoof common automation indicators
"window.chrome = window.chrome || {runtime: {}}",
"Object.defineProperty(navigator, 'plugins', {get: () => [1,2,3,4,5]})",
],
)The CLI accepts the same field in YAML form:
# templates/browser.yml (uncomment the init_scripts: section)
init_scripts:
- "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"Layer 3 — Undetected mode (Patchright)
When fingerprint patches are not enough, Patchright (an undetected Chromium fork) handles a broader set of evasions.
The bundled crawl4ai-setup warns about Patchright install when it can't run apt-get (needs sudo for OS-level dependencies). If undetected mode is required, install with:
sudo "$(uv tool dir)/crawl4ai/bin/python3" -m patchright install --with-depsSkip if you don't need it — Patchright is a separate ~150MB Chromium binary; the default Playwright Chromium handles most cases when paired with init_scripts.
Layer 4 — Proxies
proxy_config routes traffic through an upstream proxy. Works with both the browser strategy AND the non-browser HTTPCrawlerStrategy (the latter is the cheap path for static fetches behind a corporate proxy).
browser_config = BrowserConfig(
headless=True,
proxy_config={
"server": "http://proxy.example.com:8080",
"username": "user",
"password": "pass",
},
)YAML form:
# templates/browser.yml
proxy_config:
server: "http://proxy.example.com:8080"
username: "user"
password: "pass"For rotating residential proxies, configure your provider's endpoint; the library does not bundle a rotation manager.
CDP attachment (use the browser that already exists)
If a long-lived Chromium daemon is already running (gstack's /browse uses one; a manually-launched Chrome with --remote-debugging-port also works), connect crawl4ai to it via CDP instead of spawning a new browser. Use when:
- Iterating on a scrape and the cold-start cost matters
- The crawl needs to inherit existing session state (cookies, logged-in tabs)
- You want one fewer Chromium binary on disk
browser_config = BrowserConfig(
headless=True,
use_managed_browser=True,
# cdp_url passed via the underlying Playwright connection; verify the
# exact field name against the installed library before relying on it
)The exact cdp_url / ws_endpoint field name evolved across 0.8.x; before relying on it for a production flow, check the field name in the installed library:
python -c "from crawl4ai import BrowserConfig; help(BrowserConfig.__init__)" | head -50Verification
After applying any mitigation, verify the crawl actually rendered the protected content rather than the interstitial:
crwl https://example.com -B templates/browser.yml -o all -v
# Check `result.cleaned_html` for the real page content vs a challenge pageA successful crawl that returned the Cloudflare verification page is a silent failure — result.success is True but the markdown is challenge-page boilerplate.
When stuck
For an evasion that still trips a known fingerprinting surface, or a cdp_url / ws_endpoint field name that's drifted in your installed version, see escalation.md. The iron rule applies double in this file: do not invent init_scripts patches or proxy_config field names from training data — verify against the installed library first.
Crawl4AI CLI Guide
Command-line interface for the Crawl4AI library. Pairs with SDK Guide for programmatic use and the deeper Complete SDK Reference.
Table of Contents
- Installation
- Basic Usage
- Configuration
- Browser Configuration
- Crawler Configuration
- Extraction Configuration
- Advanced Features
- LLM Q&A
- Structured Data Extraction
- Content Filtering
- Output Formats
- Complete Examples
- Best Practices & Tips
---
Installation
The Crawl4AI CLI (crwl) is installed automatically with the library:
pip install crawl4ai
crawl4ai-setup---
Basic Usage
The crwl command provides a simple interface to the Crawl4AI library:
# Basic crawling - returns markdown
crwl https://example.com
# Specify output format
crwl https://example.com -o markdown
# Verbose JSON output with cache bypass
crwl https://example.com -o json -v --bypass-cache
# See usage examples
crwl --exampleQuick Example - Advanced Usage:
# Extract structured data using CSS schema
crwl "https://www.infoq.com/ai-ml-data-eng/" \
-e docs/examples/cli/extract_css.yml \
-s docs/examples/cli/css_schema.json \
-o json---
Configuration
Browser Configuration
Browser settings via YAML file or command line:
# browser.yml
headless: true
viewport_width: 1280
user_agent_mode: "random"
verbose: true
ignore_https_errors: true# Using config file
crwl https://example.com -B browser.yml
# Using direct parameters
crwl https://example.com -b "headless=true,viewport_width=1280,user_agent_mode=random"Key Parameters:
| Parameter | Description |
|---|---|
headless | Run without GUI (true/false) |
viewport_width | Browser width in pixels |
viewport_height | Browser height in pixels |
user_agent_mode | "random" or specific UA string |
For all browser parameters: BrowserConfig Reference.
Crawler Configuration
Control crawling behavior:
# crawler.yml
cache_mode: "bypass"
wait_until: "networkidle"
page_timeout: 30000
delay_before_return_html: 0.5
word_count_threshold: 100
scan_full_page: true
scroll_delay: 0.3
process_iframes: false
remove_overlay_elements: true
magic: true
verbose: true# Using config file
crwl https://example.com -C crawler.yml
# Using direct parameters
crwl https://example.com -c "css_selector=#main,delay_before_return_html=2,scan_full_page=true"Key Parameters:
| Parameter | Description |
|---|---|
cache_mode | bypass, enabled, disabled |
wait_until | networkidle, domcontentloaded |
page_timeout | Max page load time (ms) |
css_selector | Focus on specific element |
scan_full_page | Enable infinite scroll handling |
For all crawler parameters: CrawlerRunConfig Reference.
Extraction Configuration
Two extraction types supported:
1. CSS/XPath-based extraction:
# extract_css.yml
type: "json-css"
params:
verbose: true// css_schema.json
{
"name": "ArticleExtractor",
"baseSelector": ".article",
"fields": [
{
"name": "title",
"selector": "h1.title",
"type": "text"
},
{
"name": "link",
"selector": "a.read-more",
"type": "attribute",
"attribute": "href"
}
]
}2. LLM-based extraction:
# extract_llm.yml
type: "llm"
provider: "openai/gpt-4"
instruction: "Extract all articles with their titles and links"
api_token: "your-token"
params:
temperature: 0.3
max_tokens: 1000For extraction strategies: Extraction Strategies.
---
Advanced Features
LLM Q&A
Ask questions about crawled content:
# Simple question
crwl https://example.com -q "What is the main topic discussed?"
# View content then ask questions
crwl https://example.com -o markdown # See content first
crwl https://example.com -q "Summarize the key points"
crwl https://example.com -q "What are the conclusions?"
# Combined with advanced crawling
crwl https://example.com \
-B browser.yml \
-c "css_selector=article,scan_full_page=true" \
-q "What are the pros and cons mentioned?"First-time setup:
- Prompts for LLM provider and API token
- Saves configuration in
~/.crawl4ai/global.yml - Any LiteLLM-supported provider works; pick a current model identifier from
LiteLLM Providers. ollama/* providers need no token.
Structured Data Extraction
# CSS-based extraction
crwl https://example.com \
-e extract_css.yml \
-s css_schema.json \
-o json
# LLM-based extraction (schema lives inside extract_llm.yml; no -s needed)
crwl https://example.com \
-e extract_llm.yml \
-o jsonContent Filtering
Filter content for relevance:
# filter_bm25.yml (relevance-based)
type: "bm25"
query: "target content"
threshold: 1.0
# filter_pruning.yml (quality-based — no query; heuristic block pruning)
type: "pruning"
threshold: 0.48
threshold_type: "fixed"crwl https://example.com -f filter_bm25.yml -o markdown-fitFor content filtering: Content Processing.
---
Output Formats
| Format | Flag | Description |
|---|---|---|
all | -o all | Full crawl result including metadata |
json | -o json | Extracted structured data |
markdown | -o markdown or -o md | Raw markdown output |
markdown-fit | -o markdown-fit or -o md-fit | Filtered markdown |
---
Complete Examples
1. Basic Extraction:
crwl https://example.com \
-B browser.yml \
-C crawler.yml \
-o json2. Structured Data Extraction:
crwl https://example.com \
-e extract_css.yml \
-s css_schema.json \
-o json \
-v3. LLM Extraction with Filtering:
crwl https://example.com \
-B browser.yml \
-e extract_llm.yml \
-f filter_bm25.yml \
-o json4. Interactive Q&A:
# First crawl and view
crwl https://example.com -o markdown
# Then ask questions
crwl https://example.com -q "What are the main points?"
crwl https://example.com -q "Summarize the conclusions"---
Best Practices & Tips
1. Configuration Management:
- Keep common configurations in YAML files
- Use CLI parameters for quick overrides
- Store sensitive data (API tokens) in
~/.crawl4ai/global.yml
1. Performance Optimization:
- Use
--bypass-cachefor fresh content - Enable
scan_full_pagefor infinite scroll pages - Adjust
delay_before_return_htmlfor dynamic content
1. Content Extraction:
- Use CSS extraction for structured content (faster, no API costs)
- Use LLM extraction for unstructured content
- Combine with filters for focused results
1. Q&A Workflow:
- View content first with
-o markdown - Ask specific questions
- Use broader context with appropriate selectors
---
Recap
The Crawl4AI CLI provides:
- Flexible configuration via files and parameters
- Multiple extraction strategies (CSS, XPath, LLM)
- Content filtering and optimization
- Interactive Q&A capabilities
- Various output formats
---
See Also
- Python SDK Guide - Programmatic Python interface
- Complete SDK Reference - Full API documentation
- Escalation - What to do when an unknown flag, an empty extraction, or a version-drift surface
surprises you
Content Filters
The markdown generator can route through a content filter before producing fit_markdown. Three filter types, different selection criteria.
| Filter | When to use | Cost | Templates |
|---|---|---|---|
PruningContentFilter | You want clean markdown without a topic. Quality-based block filtering. | Free | `templates/filter_pruning.yml` |
BM25ContentFilter | You have a query / topic; want only relevant blocks. Lexical relevance. | Free | `templates/filter_bm25.yml` |
LLMContentFilter | The query is fuzzy and BM25 misses semantically related content. | LLM call per page | n/a |
Every crawl returns both result.markdown.raw_markdown (always populated) and result.markdown.fit_markdown (populated only when a filter is configured). When no filter is configured, fit_markdown is None.
PruningContentFilter (default-good)
Removes low-quality blocks (boilerplate, navigation residue, short fragments) based on a heuristic score. No query needed.
from crawl4ai.content_filter_strategy import PruningContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
pruning_filter = PruningContentFilter(
threshold=0.48, # 0.0 keeps everything, 1.0 prunes aggressively
threshold_type="fixed", # "fixed" or "dynamic"
)
md_generator = DefaultMarkdownGenerator(content_filter=pruning_filter)
config = CrawlerRunConfig(markdown_generator=md_generator)threshold_type="dynamic" adjusts per-page based on content density; useful across heterogeneous corpora. Start with fixed at 0.48 and tune up if too much boilerplate survives, down if real content disappears.
BM25ContentFilter
Lexical relevance scoring against a query string. Blocks below bm25_threshold are dropped.
from crawl4ai.content_filter_strategy import BM25ContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
bm25_filter = BM25ContentFilter(
user_query="machine learning tutorials",
bm25_threshold=1.0, # higher = stricter
)
md_generator = DefaultMarkdownGenerator(content_filter=bm25_filter)BM25 is lexical, not semantic — it matches token overlap, weighted by IDF. It will miss "deep learning" when the query is "neural networks" unless the page also uses that vocabulary. For semantic match, use LLMContentFilter.
LLMContentFilter
LLM-judged relevance per block. Most expensive; use when:
- The query is conceptual and BM25 misses too much
- The page is long enough that the LLM cost is worth it vs scraping the noise
- You're piloting a corpus before settling on a cheaper filter
from crawl4ai.content_filter_strategy import LLMContentFilter
from crawl4ai import LLMConfig
llm_filter = LLMContentFilter(
llm_config=LLMConfig(provider="openai/gpt-4o-mini"),
instruction="Keep only blocks discussing the company's revenue or growth metrics",
)
md_generator = DefaultMarkdownGenerator(content_filter=llm_filter)Filter selection heuristic
1. Try PruningContentFilter with threshold=0.48, threshold_type="fixed" first. If output looks clean, stop. 2. If output still contains topic-irrelevant content AND you have a query/topic, switch to BM25ContentFilter with bm25_threshold=1.0. 3. If BM25 over-prunes (real content scored low because vocabulary doesn't match), tune bm25_threshold down to ~0.5. 4. If after BM25 tuning the filter still misses semantically related content, switch to LLMContentFilter and acknowledge the per-page cost.
CLI equivalents
Each Python filter has a YAML form for crwl -f filter.yml:
crwl https://docs.example.com -f templates/filter_bm25.yml -o markdown-fit
crwl https://docs.example.com -f templates/filter_pruning.yml -o markdown-fitLLMContentFilter is SDK-only at the CLI level — crwl does not currently expose it as a YAML strategy. Use the Python SDK when LLM-filtered markdown is required.
Accessing both filtered and raw markdown
result = await crawler.arun(url, config=config)
print(result.markdown.raw_markdown) # always present
print(result.markdown.fit_markdown) # filtered, or None if no filterraw_markdown is useful as a fallback when the filter produces empty output (e.g. the threshold was too aggressive). Log both during tuning.
When stuck
For a filter constructor that raises on a kwarg this page doesn't name, or fit_markdown that comes back unexpectedly empty, see escalation.md for the lookup order and worked examples.
When stuck — escalation guidance
The references in this skill cover the common cases the CLI and SDK are built around. When the question goes further — an obscure crwl flag, an extraction strategy that returns empty, a Playwright timing edge case the docs don't name — follow the lookup order below before guessing. Guessing about library API shape is the failure mode this section exists to prevent.
Iron rule — what NOT to guess about
Never invent crawl4ai API surface from training data. The skill is verified against the version recorded in `VERSION`; confirm against that version before committing to a name. Specifically: do not fabricate
- function/method names (
crawler.run,extract_json,set_proxy, etc.) without confirming via
complete-sdk-reference.md or upstream docs
- field names on
BrowserConfig,CrawlerRunConfig,LLMConfig,JsonCssExtractionStrategy, or schema dicts - CLI flags for
crwl(usecrwl --helpandcrwl --exampleto enumerate the real surface) - content-filter constructor kwargs (
PruningContentFilter,BM25ContentFilter,LLMContentFilter) - output shape of
CrawlResult(e.g.result.markdownis aStringCompatibleMarkdown, not a plainstr)
Carve-out — these are always fine without confirmation:
- Running
crwl --help,crwl --example,crwl --version - Running
crawl4ai-doctorto diagnose the local install - Reading any file under this skill (SKILL.md, references, scripts, tests)
- Running
python -c "import importlib.metadata; print(importlib.metadata.version('crawl4ai'))"(orcrwl --version)
to check the installed version. Note: crawl4ai.__version__ is itself a module in 0.8.x, so the obvious print(crawl4ai.__version__) form prints the module repr, not the version string. Use importlib.metadata.version instead.
- Running the bundled
tests/againstexample.com— they're read-only smoke checks
The prohibition is on inventing names; verifying via --help, reading source, or running a non-mutating probe never qualifies.
Lookup order
1. `qmd query --collection solutions "<problem statement>"` — the team's internal solutions corpus. Always first. Use 2-3 focused queries varying angle (e.g. "crawl4ai schema generation", "JsonCssExtractionStrategy empty output", "playwright wait_for selector timeout"). Most past gotchas with this library — version drift, schema constructors, filter combinations — have an entry there. 2. This skill's references — cli-guide.md, sdk-guide.md, complete-sdk-reference.md in that order. The complete reference is 5900+ lines; jump to the anchor (e.g. #extraction-strategies, #content-processing) rather than scanning linearly. 3. Upstream documentation — https://docs.crawl4ai.com/ for current docs. Confirm the installed version with python -c "import importlib.metadata; print(importlib.metadata.version('crawl4ai'))" (or crwl --version) and check against the skill's `VERSION` file if surface names look wrong. 4. Upstream source + issues — https://github.com/unclecode/crawl4ai. Search Issues for the error message verbatim before re-deriving a fix. The codebase is small enough to grep when an API rename is suspected. 5. `qmd query --collection stars` — community blog posts, tutorials, and third-party examples mirrored in stars. Useful for non-obvious patterns (e.g. handling sites that detect headless Chrome via canvas fingerprinting). 6. Ask the user — only after the above. The question to ask is concrete: "I tried wait_for=css:.foo and the page timed out at 30s; the network panel suggests the selector is correct but content arrives via shadow DOM. Do you want me to try wait_for=js: instead, or do you know the actual rendering trigger?"
Halt-vs-continue
Continue (make a defensible choice, verify with a run):
- Choosing between schema-based and LLM extraction when the page is borderline structured.
- Picking
wait_forstrategy (networkidlevsdomcontentloadedvs selector) for an unfamiliar site — test, observe,
refine.
- Selecting a content filter (
BM25vsPruningContentFilter) — try one, look atfit_markdownlength, swap if poor.
Halt and ask:
- The user mentioned credentials, a private API, or a paid SaaS source — confirm authorization scope before sending
traffic.
- A
wait_fororjs_codesnippet would interact with destructive UI (delete buttons, transfer flows). Even in QA,
ask.
- The library raised an error message that explicitly names a config field you didn't set — likely a version bump
renamed your surface. Verify the actual version (importlib.metadata.version('crawl4ai')) and the field name in the installed source before patching.
- The site's robots.txt or ToS would plausibly forbid scraping — ask the user about authorization before crawling.
Worked examples
Example 1 — wait_for selector never resolves
Symptom: crwl https://app.example.com -c "wait_for=css:.results" times out at 30s, but the page renders fine in a browser.
What I did: queried qmd query --collection solutions "playwright wait_for selector timeout shadow DOM" (no hit), then re-ran crwl https://app.example.com -o html | rg -i "results" | head to confirm the selector exists in raw HTML. It did not — the content is rendered inside a shadow root the CSS selector can't pierce. Switched to wait_for="js:document.querySelector('app-root').shadowRoot.querySelector('.results') !== null" per complete-sdk-reference.md#advanced-features. Continue (verified by re-run, content extracted cleanly).
Example 2 — JsonCssExtractionStrategy returns [] despite valid schema
Symptom: schema-driven extraction returns empty list even though crwl ... -o markdown clearly shows the target elements.
What I did: queried qmd query --collection solutions "JsonCssExtractionStrategy empty output baseSelector". Found a solution doc that pointed at a common pitfall: baseSelector matching zero elements (e.g. .product-card when the site uses [data-product]). Verified by crwl ... -o html | rg "product-card\b" → zero hits. Updated schema selector. The schema-generation script scripts/generate_schema.py would have caught this on the first pass. Continue (verified by re-run).
Example 3 — crwl flag named in a third-party tutorial doesn't exist
Symptom: a blog post says crwl --depth 3 https://example.com for recursive crawling, but crwl --help shows no --depth flag.
What I did: ran crwl --version to confirm installed version, then re-ran the query against the upstream README (https://github.com/unclecode/crawl4ai) for the actual recursive-crawl entry point. Recursive crawling is an SDK-only feature using arun_many() with a discovered URL list, not a crwl flag. The blog post was for an older fork. Halted the CLI approach, switched to scripts/batch_crawl.py with a URL file. Halt → ask if the new approach was OK before proceeding with credentials.
Recipes — worked end-to-end flows
Each recipe is a complete walkthrough from "I have a URL" to "I have the data I wanted." Pick the one closest to your situation and adapt.
1. One static documentation page → markdown file
Goal: clean markdown of a single docs page.
crwl https://docs.example.com/guide -o markdown > guide.mdIf the page has navigation residue, add a pruning filter:
crwl https://docs.example.com/guide -f templates/filter_pruning.yml -o markdown-fit > guide.mdPair with the `/fetch-web` skill for static pages where browser cold-start is not worth paying — defuddle returns ~0ms vs crawl4ai's ~2s.
2. A JS-heavy SPA → markdown
Goal: a Next.js / Vue / React app that doesn't render in curl.
crwl https://app.example.com -c "wait_for=css:.results,page_timeout=60000" -o markdownIf wait_for=css: times out, try a JS predicate:
crwl https://app.example.com -c "wait_for=js:document.querySelector('.results') !== null,page_timeout=60000"For aggressive bot detection, layer templates/browser.yml (Layer 1: random UA) and init_scripts (Layer 2: fingerprint patches) — see Anti-Detection.
3. E-commerce product list across many URLs
Goal: extract {name, price, link} for products across many shop URLs.
# 1. Derive a schema from one URL (one LLM call)
./scripts/generate_schema.py https://shop.example.com/category "products with name, price, link" shop_schema.json
# 2. Sanity check the schema on a single page (no LLM)
./scripts/extract_with_schema.py https://shop.example.com/category shop_schema.json /tmp/check.json
jaq '.products[0:3]' /tmp/check.json
# 3. Run across the full URL list (no LLM, concurrent)
./scripts/batch_extract.py shop_urls.txt shop_schema.json --max-concurrent 5 --out products.jsonIf step 2 returns an empty list, the LLM-derived baseSelector doesn't match. Inspect the page HTML and adjust the schema by hand (see Escalation example 2).
4. News aggregation by topic
Goal: collect markdown from many news sites, filtered by topic relevance.
# Copy the filter template, set the topic-specific query, then crawl per URL
cp templates/filter_bm25.yml /tmp/news_filter.yml
$EDITOR /tmp/news_filter.yml # set query: to your topic; tune threshold
for url in $(cat news_urls.txt); do
slug=$(echo "$url" | sed 's|https://||; s|/|_|g')
crwl "$url" -f /tmp/news_filter.yml -o markdown-fit > "news/$slug.md"
doneEdit the copy, not the bundled templates/filter_bm25.yml — the template is a skeleton meant to seed many filters, not to be mutated in place.
For semantic relevance (e.g. when the topic vocabulary doesn't match the article's vocabulary), switch to LLMContentFilter per Content Filters § LLMContentFilter.
5. Topic-bound crawl of a single domain
Goal: every blog post on example.com about machine learning.
# 1. Discover URLs from sitemap + Common Crawl, BM25-filter by topic, validate live
python3 -c "
import asyncio, sys
from crawl4ai import AsyncUrlSeeder, SeedingConfig
async def main():
seeds = await AsyncUrlSeeder().urls('example.com', SeedingConfig(
source='sitemap+cc',
pattern='*/blog/*',
query='machine learning',
score_threshold=0.3,
live_check=True,
))
for s in seeds:
print(s['url'])
asyncio.run(main())
" > ml_urls.txt
# 2. Crawl them
./scripts/batch_crawl.py ml_urls.txt --out ml_markdown/For maximum-coverage discovery (no query / topic), substitute DomainMapper:
from crawl4ai import DomainMapper
urls = await DomainMapper(include_subdomains=False).map_domain("example.com")See URL Discovery for the full surface.
6. Login-required content
Goal: scrape content behind a login.
# 1. Fill in templates/login_crawler.yml with selectors + post-login wait condition
# 2. Login (cookies persist under the session_id)
crwl https://site.com/login -C templates/login_crawler.yml
# 3. Access protected pages, reusing the session
crwl https://site.com/dashboard -c "session_id=user_session" -o markdown
crwl https://site.com/profile -c "session_id=user_session" -o markdownFor credentials: never inline them in templates/login_crawler.yml. Use environment variables and substitute at invocation, or pass via a generated YAML file deleted after use.
7. Render existing HTML → screenshot / PDF
Goal: you already have HTML in hand (from defuddle, a previous crawl, or a database); only need crawl4ai's render.
result = await crawler.arun(
url="raw:" + html_string,
config=CrawlerRunConfig(
base_url="https://example.com", # for relative-link resolution
screenshot=True,
pdf=True,
),
)
# result.screenshot is base64; result.pdf is bytesFor local files:
result = await crawler.arun(
url="file:///path/to/page.html",
config=CrawlerRunConfig(screenshot=True),
)This is the cheap path when the network fetch was already paid by another tool.
8. Q&A over a page
Goal: ask questions of crawled content via the LLM CLI.
crwl https://example.com -o markdown # preview
crwl https://example.com -q "What are the main conclusions?" # ask
crwl https://example.com -q "Summarize in 3 bullets"First-time setup prompts for the LLM provider and API token; stored in ~/.crawl4ai/global.yml. Any LiteLLM-supported provider works — pick a current model identifier from <https://docs.litellm.ai/docs/providers>.
Crawl4AI Python SDK Guide
Programmatic Python interface for the Crawl4AI library. Pairs with CLI Guide for command-line use and the deeper Complete SDK Reference.
Quick Start
Installation
pip install crawl4ai
crawl4ai-setupBasic First Crawl
import asyncio
from crawl4ai import AsyncWebCrawler
async def main():
async with AsyncWebCrawler() as crawler:
result = await crawler.arun("https://example.com")
print(result.markdown[:500])
asyncio.run(main())With Configuration
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
browser_config = BrowserConfig(
headless=True,
viewport_width=1920,
viewport_height=1080
)
crawler_config = CrawlerRunConfig(
page_timeout=30000,
screenshot=True,
remove_overlay_elements=True
)
async with AsyncWebCrawler(config=browser_config) as crawler:
result = await crawler.arun(
url="https://example.com",
config=crawler_config
)
print(f"Success: {result.success}")
print(f"Markdown length: {len(result.markdown)}")For complete API reference: AsyncWebCrawler.
---
Configuration
BrowserConfig
Controls the browser instance (global settings):
from crawl4ai import BrowserConfig
browser_config = BrowserConfig(
browser_type="chromium", # chromium, firefox, webkit
headless=True, # Run without GUI
viewport_width=1280,
viewport_height=720,
user_agent="custom-agent", # Custom user agent
proxy_config={ # Proxy settings
"server": "http://proxy:8080",
"username": "user",
"password": "pass"
}
)Key Parameters:
| Parameter | Description |
|---|---|
headless | Run with/without GUI |
viewport_width/height | Browser dimensions |
user_agent | Custom user agent string |
cookies | Pre-set cookies |
headers | Custom HTTP headers |
proxy_config | Proxy server settings |
For all parameters: BrowserConfig Reference.
CrawlerRunConfig
Controls each crawl operation (per-crawl settings):
from crawl4ai import CrawlerRunConfig, CacheMode
config = CrawlerRunConfig(
# Timing
page_timeout=30000, # Max page load time (ms)
wait_for="css:.content", # Wait for element
delay_before_return_html=0.5,
# Content selection
css_selector=".main-content",
excluded_tags=["nav", "footer"],
# Caching
cache_mode=CacheMode.BYPASS,
# JavaScript
js_code="window.scrollTo(0, document.body.scrollHeight);",
# Output
screenshot=True,
pdf=True
)Key Parameters:
| Parameter | Description |
|---|---|
page_timeout | Max page load/JS time (ms) |
wait_for | CSS selector or JS condition |
cache_mode | ENABLED, BYPASS, DISABLED |
js_code | JavaScript to execute |
session_id | Persist session across crawls |
screenshot | Capture screenshot |
For all parameters: CrawlerRunConfig Reference.
---
CrawlResult
Every arun() call returns a CrawlResult:
result = await crawler.arun(url, config=config)
# Status
result.success # bool - crawl succeeded
result.status_code # HTTP status code
result.error_message # Error details if failed
# Content
result.html # Raw HTML
result.cleaned_html # Sanitized HTML
result.markdown # MarkdownGenerationResult object
result.markdown.raw_markdown # Full markdown
result.markdown.fit_markdown # Filtered markdown (if filter used)
# Media & Links
result.media["images"] # List of images
result.media["videos"] # List of videos
result.links["internal"] # Internal links
result.links["external"] # External links
# Extras
result.screenshot # Base64 screenshot (if requested)
result.pdf # PDF bytes (if requested)
result.metadata # Page metadata (title, description)For complete fields: CrawlResult Reference.
---
Content Processing
Markdown Generation
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
md_generator = DefaultMarkdownGenerator(
options={
"ignore_links": False,
"ignore_images": False,
"body_width": 80
}
)
config = CrawlerRunConfig(markdown_generator=md_generator)Content Filtering
Filter content for relevance before markdown generation:
from crawl4ai.content_filter_strategy import PruningContentFilter, BM25ContentFilter
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
# Option 1: Pruning (removes low-quality content)
pruning_filter = PruningContentFilter(
threshold=0.4,
threshold_type="fixed"
)
# Option 2: BM25 (relevance-based)
bm25_filter = BM25ContentFilter(
user_query="machine learning tutorials",
bm25_threshold=1.0
)
md_generator = DefaultMarkdownGenerator(content_filter=bm25_filter)
config = CrawlerRunConfig(markdown_generator=md_generator)
result = await crawler.arun(url, config=config)
print(result.markdown.fit_markdown) # Filtered content
print(result.markdown.raw_markdown) # Original contentFor filters and generators: Content Processing.
---
Data Extraction
CSS-Based Extraction (No LLM)
Fast, deterministic extraction using CSS selectors:
from crawl4ai import JsonCssExtractionStrategy
schema = {
"name": "articles",
"baseSelector": "article.post",
"fields": [
{"name": "title", "selector": "h2", "type": "text"},
{"name": "date", "selector": ".date", "type": "text"},
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
]
}
extraction_strategy = JsonCssExtractionStrategy(schema=schema)
config = CrawlerRunConfig(extraction_strategy=extraction_strategy)
result = await crawler.arun(url, config=config)
data = json.loads(result.extracted_content)LLM-Based Extraction
For complex or irregular content:
from crawl4ai import LLMExtractionStrategy, LLMConfig
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(description="Product name")
price: str = Field(description="Product price")
extraction_strategy = LLMExtractionStrategy(
llm_config=LLMConfig(
provider="openai/gpt-4o-mini",
api_token="your-token"
),
schema=Product.model_json_schema(),
extraction_type="schema",
instruction="Extract product information"
)
config = CrawlerRunConfig(extraction_strategy=extraction_strategy)For extraction strategies: Extraction Strategies.
---
Multi-URL Crawling
Concurrent Processing with arun_many()
urls = ["https://site1.com", "https://site2.com", "https://site3.com"]
config = CrawlerRunConfig(
cache_mode=CacheMode.BYPASS,
stream=True # Enable streaming
)
async with AsyncWebCrawler() as crawler:
# Streaming mode - process as they complete
async for result in await crawler.arun_many(urls, config=config):
if result.success:
print(f"Completed: {result.url}")
# Batch mode - wait for all
config = config.clone(stream=False)
results = await crawler.arun_many(urls, config=config)URL-Specific Configurations
from crawl4ai import CrawlerRunConfig, MatchMode
# Different configs for different URL patterns
pdf_config = CrawlerRunConfig(
url_matcher="*.pdf",
# PDF-specific settings
)
blog_config = CrawlerRunConfig(
url_matcher=["*/blog/*", "*/article/*"],
match_mode=MatchMode.OR
)
default_config = CrawlerRunConfig() # Fallback
results = await crawler.arun_many(
urls=urls,
config=[pdf_config, blog_config, default_config]
)For dispatchers and advanced: arun_many() Reference.
---
Session Management
Persistent Sessions
# First crawl - establish session
login_config = CrawlerRunConfig(
session_id="user_session",
js_code="""
document.querySelector('#username').value = 'myuser';
document.querySelector('#password').value = 'mypass';
document.querySelector('#submit').click();
""",
wait_for="css:.dashboard"
)
await crawler.arun("https://site.com/login", config=login_config)
# Subsequent crawls - reuse session
config = CrawlerRunConfig(session_id="user_session")
await crawler.arun("https://site.com/protected", config=config)
# Clean up
await crawler.crawler_strategy.kill_session("user_session")Dynamic Content Handling
config = CrawlerRunConfig(
wait_for="css:.ajax-content",
js_code="""
window.scrollTo(0, document.body.scrollHeight);
document.querySelector('.load-more')?.click();
""",
page_timeout=60000
)For session patterns: Advanced Features - Session Management (lines 5429-5940)
---
Best Practices
1. Use context managers - async with AsyncWebCrawler() ensures cleanup 2. Enable caching during development - cache_mode=CacheMode.ENABLED 3. Set appropriate timeouts - 30s normal, 60s+ for JS-heavy sites 4. Prefer CSS extraction over LLM - 10-100x more efficient 5. Use clone() for config variants - config.clone(screenshot=True) 6. Respect rate limits - Use delays between requests
---
See Also
- CLI Guide - Command-line interface alternative
- Complete SDK Reference - Full API documentation
- Escalation - Lookup order when a field name, return shape, or import path doesn't match this guide
Troubleshooting
Symptoms, likely causes, fixes. When a recipe in this list doesn't resolve the issue, follow Escalation.
JavaScript content not loading
Symptom: crwl <url> -o markdown returns boilerplate or skeleton, but the page renders fine in a real browser.
Cause: page content arrives via JS after the initial HTML load. Crawl4AI returned the page before JS finished.
Fix: tell crawl4ai what to wait for.
crwl https://example.com -c "wait_for=css:.dynamic-content,page_timeout=60000"If wait_for=css: selector doesn't exist in the raw HTML (shadow DOM, dynamic class names, etc.), use a JS predicate:
crwl https://example.com -c "wait_for=js:document.querySelector('.content') !== null,page_timeout=60000"For infinite scroll, add scan_full_page=true:
crwl https://example.com -c "wait_for=css:.list-item,scan_full_page=true,page_timeout=60000"Bot detection / Cloudflare challenge
Symptom: result.success is True but result.markdown is the challenge page boilerplate, or you get a 403 / empty response.
Cause: site detected automated traffic at one of four layers (headers, JS fingerprint, behaviour, IP).
Fix: layer the mitigations from Anti-Detection in order — random user agent first, then init_scripts fingerprint patches, then undetected mode, then proxies. Verify each step actually rendered the real page (not the challenge page) by inspecting result.cleaned_html directly.
crwl https://example.com -B templates/browser.yml -o all -vContent not extracted (extracted_content empty / [])
Symptom: extraction returns empty even though the target elements are visible in result.html.
Causes (in order of likelihood):
1. baseSelector in the schema doesn't match any element. Verify:
crwl <url> -o html | rg "<base-selector-text>" # zero hits = wrong selector2. Field selector is wrong (absolute when it should be relative to baseSelector, or vice versa). 3. Page hasn't finished rendering at extract time. Add wait_for=css:<base-selector>.
For LLM-based extraction returning malformed JSON, log result.extracted_content directly — the LLM may be wrapping JSON in markdown fences or prose, which json.loads() then fails on.
Session not persisting
Symptom: after crwl https://site.com/login -C login_crawler.yml, the second crawl with session_id=user_session doesn't show the logged-in state.
Causes:
1. The login flow didn't actually complete. Add a strict wait_for: "css:.dashboard" (or whatever the post-login UI selector is). Check result.cleaned_html from the login crawl to confirm the session UI rendered. 2. session_id is misspelled or different between the two calls. 3. Server rejected the login. Inspect result.cookies and result.status_code.
Quick verify:
crwl https://site.com/protected -c "session_id=user_session" -o all -v | rg -i "session\|cookie"Crawl is slow
Symptom: single-URL crawls take 10s+; batch crawls feel slower than they should.
Causes (with fixes):
1. Browser cold start (~2s): unavoidable per process. Use batch_crawl.py to amortise across many URLs. 2. Default cache mode: cache_mode: "bypass" re-fetches every time. Use "enabled" during development, "bypass" only for production refresh runs. 3. `scan_full_page: true` on a long page: each scroll waits scroll_delay (default 0.3s); pages with 100+ scroll steps take 30s+. Disable when content is above the fold. 4. `page_timeout` set too high: a 60s timeout means failed crawls wait the full 60s. Tighten to 30s and let transient failures fail fast. 5. `max_concurrent` too low for batch: defaults to 5. With a stable network and target, 10-20 is often fine; watch memory.
Schema generation produces nonsense
Symptom: ./scripts/generate_schema.py returns a schema that extracts zero items, or wildly wrong fields.
Causes:
1. The instruction was too vague. "Extract products" is worse than "Extract product cards with name, price, and link; the cards are visually arranged in a grid." 2. The page renders content client-side and the LLM only saw the skeleton HTML. Try wait_for=css:<the visible content> in generate_schema.py's CrawlerRunConfig and re-run. 3. The page has multiple repeating patterns (e.g. a "featured" carousel and a main grid). The LLM picked the wrong one. Inspect the schema, narrow the baseSelector, and retry.
crwl-doctor says everything is fine, but crawls fail
Symptom: crawl4ai-doctor passes, but real crawls return network errors / empty responses.
Causes:
1. DNS / network from your env: doctor probes crawl4ai.com; your target may be behind a firewall, a VPN, or rate-limited. 2. Proxy not picked up: crawl4ai uses proxy_config (per-config), not $HTTPS_PROXY. If your network requires a proxy, set it in templates/browser.yml. 3. TLS errors: some intermediate CAs in corporate envs trip the browser. Try ignore_https_errors: true in templates/browser.yml for diagnosis only — don't ship with it on.
Library upgrade broke my schema / config
Symptom: previously-working scripts fail after a crawl4ai version bump.
Fix: check the VERSION file at this skill's root — it records the version the skill was verified against. If you upgraded crawl4ai past that, the field names or default values may have changed. Bump VERSION only after re-running the bundled tests/ against the new version.
If a regression appears between two versions, check the upstream CHANGELOG.md (https://github.com/unclecode/crawl4ai/blob/main/CHANGELOG.md) for the specific delta. The 0.7 → 0.8 transition in particular changed several default behaviours (e.g. AsyncLogger now routes to stderr; LLMExtractionStrategy defaults extraction_type to "schema").
URL Discovery
Find URLs before crawling them. Two surfaces, picked by the shape of the question.
| Surface | Use when |
|---|---|
AsyncUrlSeeder + SeedingConfig | You have a query / topic and want BM25-scored URL relevance, with optional live HEAD validation |
DomainMapper | You want maximum URL coverage of one domain, optionally including subdomains |
The output is a list of URLs that you feed into arun_many(), batch_crawl.py, or batch_extract.py. Discovery itself never fetches page content.
AsyncUrlSeeder
Discovers URLs from sitemaps and the Common Crawl index, applies pattern filters, scores by BM25 against a query, and optionally validates each candidate with a live HEAD request before returning.
from crawl4ai import AsyncUrlSeeder, SeedingConfig
config = SeedingConfig(
source="sitemap+cc", # "sitemap" | "cc" | "sitemap+cc"
pattern="*/blog/*", # glob filter on URL path
query="machine learning", # BM25 relevance scoring
score_threshold=0.3, # drop seeds below this score
live_check=True, # HEAD-validate each URL before returning
max_concurrent=10,
)
seeder = AsyncUrlSeeder()
seeds = await seeder.urls("example.com", config)
urls = [s["url"] for s in seeds]Each entry in seeds is a dict with url, score (BM25 0-1), and metadata pulled from sitemap entries / JSON-LD / Open Graph tags (when available).
SeedingConfig fields
| Field | Type | Default | Notes |
|---|---|---|---|
source | str | "sitemap" | "sitemap", "cc", or "sitemap+cc" |
pattern | str / list | None | Glob pattern(s) on URL path; matching mode controlled by match_mode |
query | str | None | BM25 relevance score against page metadata; omit to skip scoring |
score_threshold | float | 0.0 | Drop seeds below this BM25 score (only applies when query is set) |
live_check | bool | False | HEAD-validate each URL; expensive but removes dead links |
max_concurrent | int | 10 | Concurrent HEAD requests when live_check=True |
cache_ttl_hours | int | 24 | TTL for cached discovery results |
validate_sitemap_lastmod | bool | False | Re-fetch sitemap when <lastmod> indicates updates |
Multi-domain parallel discovery
AsyncUrlSeeder.many_urls() runs discovery across multiple domains in parallel and returns the merged result. Use when the URL universe spans more than one domain (e.g. a news aggregator).
domains = ["site1.com", "site2.com", "site3.com"]
all_seeds = await seeder.many_urls(domains, config)DomainMapper
Comprehensive domain URL discovery. Walks sitemap + light crawl, optionally expands into subdomains, with a per-source timeout so a slow sitemap doesn't block the whole map. Use when coverage is the goal and relevance scoring is not.
from crawl4ai import DomainMapper
mapper = DomainMapper(
include_subdomains=False, # True to walk `*.example.com`
per_source_timeout=30, # seconds per discovery source
)
urls = await mapper.map_domain("example.com")Returns a flat list of URLs deduplicated across sources. Pair with arun_many() for the actual crawl.
When to pick which
- Topic-bound and high-precision:
AsyncUrlSeederwithquery+score_threshold. Best for "I want all blog posts
about X."
- Domain-bound and high-recall:
DomainMapper. Best for "give me every URL on this site." - Both: run
DomainMapperfirst, then filter the result withAsyncUrlSeeder.urls(domain, config)wherequery
encodes the topic.
Composition with the bundled scripts
# Discover, write to urls.txt, then batch-crawl
python -c "
import asyncio
from crawl4ai import AsyncUrlSeeder, SeedingConfig
async def main():
s = AsyncUrlSeeder()
seeds = await s.urls('example.com', SeedingConfig(source='sitemap+cc', query='ml', score_threshold=0.3))
for x in seeds: print(x['url'])
asyncio.run(main())
" > urls.txt
./scripts/batch_crawl.py urls.txt --out batch_markdown/
./scripts/batch_extract.py urls.txt my_schema.json --out batch_extracted.jsonThe seeder + mapper APIs are documented in detail in `complete-sdk-reference.md`. When the field names here diverge from the installed library, verify against the installed version (crwl --version or python -c "import importlib.metadata; print(importlib.metadata.version('crawl4ai'))") and the VERSION file at the skill root. When stuck, see escalation.md for the full lookup order.
{
"name": "items",
"baseSelector": "<css-selector-matching-each-item>",
"fields": [
{"name": "title", "selector": "<selector-relative-to-baseSelector>", "type": "text"},
{"name": "price", "selector": "<selector>", "type": "text"},
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"},
{"name": "image", "selector": "img", "type": "attribute", "attribute": "src"}
]
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# JsonCssExtractionStrategy template. Copy, fill, pass via
# `crwl <url> -e extract_css.yml -s css_schema.json -o json`.
# Field reference: `references/complete-sdk-reference.md#extraction-strategies`.
type: "json-css"
params:
verbose: false
0.8.9
Related skills
How it compares
Pick crawl4ai for CLI-based public-page-to-markdown pipelines; use browser MCP skills when live interactive page control is required.
FAQ
What output formats does the crawl4ai skill support?
crawl4ai documents Crawl4AI CLI workflows for clean markdown, structured JSON, and LLM-ready extracted data from public webpages. Advanced sections cover LLM Q&A and structured data extraction configuration.
What configuration areas does crawl4ai cover?
crawl4ai covers browser configuration, crawler configuration, and extraction configuration in the CLI guide. Developers can tune crawl behavior before piping results into RAG or agent pipelines.
Is Crawl4ai safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.