Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Execute Firecrawl primary workflow: scrape and crawl websites into LLM-ready markdown. Use when scraping single pages, crawling entire sites, or building content ingestion pipelines with Firecrawl's scrapeUrl and crawlUrl methods. Trigger with phrases like "firecrawl scrape", "firecrawl crawl site", "scrape page to markdown", "crawl documentation".
.claude/skills/jeremylongshore-firecrawl-core-workflow-a/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 30% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-13 | ✓→✗ | ▼ Worse | 89% | 0% |
Primary workflow for Firecrawl: convert websites into clean LLM-ready markdown. Covers single-page scraping with scrapeUrl, multi-page crawling with crawlUrl, async crawl jobs with polling, and content processing pipelines.
@mendable/firecrawl-js installedFIRECRAWL_API_KEY environment variable settypescriptimport FirecrawlApp from "@mendable/firecrawl-js"; const firecrawl = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY!, }); // Scrape a single page to clean markdown const result = await firecrawl.scrapeUrl("https://docs.example.com/api", { formats: ["markdown"], onlyMainContent: true, // strips nav, footer, sidebars waitFor: 2000, // wait 2s for JS to render }); if (result.success) { console.log("Title:", result.metadata?.title); console.log("Source:", result.metadata?.sourceURL); console.log("Markdown:", result.markdown?.substring(0, 200)); }
typescript// Crawl a site — Firecrawl follows links, renders JS, returns all pages const crawlResult = await firecrawl.crawlUrl("https://docs.example.com", { limit: 50, // max pages to crawl maxDepth: 3, // link depth from start URL includePaths: ["/docs/*", "/api/*"], // only these paths excludePaths: ["/blog/*", "/changelog/*"], allowBackwardLinks: false, // only crawl child paths scrapeOptions: { formats: ["markdown"], onlyMainContent: true, }, }); console.log(`Crawled ${crawlResult.data?.length} pages`); for (const page of crawlResult.data || []) { console.log(` ${page.metadata?.sourceURL}: ${page.markdown?.length} chars`); }
typescript// Start an async crawl job — returns immediately with job ID const job = await firecrawl.asyncCrawlUrl("https://docs.example.com", { limit: 500, scrapeOptions: { formats: ["markdown"] }, }); console.log(`Crawl started: ${job.id}`); // Poll for completion with backoff let pollInterval = 2000; let status = await firecrawl.checkCrawlStatus(job.id); while (status.status === "scraping") { console.log(`Progress: ${status.completed}/${status.total} pages`); await new Promise(r => setTimeout(r, pollInterval)); pollInterval = Math.min(pollInterval * 1.5, 30000); status = await firecrawl.checkCrawlStatus(job.id); } if (status.status === "completed") { console.log(`Done: ${status.data?.length} pages scraped`); } else { console.error("Crawl failed:", status.error); }
typescriptimport { writeFileSync, mkdirSync } from "fs"; function processResults(pages: any[], outputDir: string) { mkdirSync(outputDir, { recursive: true }); const manifest = pages.map((page, i) => { const url = page.metadata?.sourceURL || `page-${i}`; const slug = new URL(url).pathname .replace(/\//g, "_") .replace(/^_|_$/g, "") || "index"; const filename = `${slug}.md`; // Clean markdown: collapse whitespace, remove JS links const content = (page.markdown || "") .replace(/\n{3,}/g, "\n\n") .replace(/\[.*?\]\(javascript:.*?\)/g, "") .trim(); writeFileSync(`${outputDir}/${filename}`, content); return { url, filename, chars: content.length }; }); writeFileSync(`${outputDir}/manifest.json`, JSON.stringify(manifest, null, 2)); return manifest; }
manifest.json with URL-to-file mapping| Error | Cause | Solution | |-------|-------|----------| | Empty markdown | JS content not rendered | Increase waitFor to 5000ms | | 429 Too Many Requests | Rate limit hit | Back off, reduce concurrency | | Crawl returns few pages | URL filters too strict | Widen includePaths patterns | | 402 Payment Required | Credits exhausted | Check balance, reduce limit | | Partial crawl results | Site blocks bot on some pages | Use scrapeUrl for failed URLs individually |
typescriptconst result = await firecrawl.scrapeUrl("https://example.com", { formats: ["markdown", "html", "links"], onlyMainContent: true, }); console.log("Markdown:", result.markdown?.length); console.log("HTML:", result.html?.length); console.log("Links:", result.links?.length);
typescriptconst job = await firecrawl.asyncCrawlUrl("https://docs.example.com", { limit: 100, scrapeOptions: { formats: ["markdown"] }, webhook: { url: "https://api.yourapp.com/webhooks/firecrawl", events: ["completed", "page"], }, }); console.log(`Crawl ${job.id} started — webhook will fire on completion`);
For structured data extraction, see firecrawl-core-workflow-b.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 10,016 | 7,740 | -23% | 1 | 1 | 0% | 2,072 | 2,692 | +30% | 0 | 0 | — |
case-02 | fail→fail | 15,129 | 10,442 | -31% | 1 | 1 | 0% | 3,074 | 3,657 | +19% | 0 | 0 | — |
case-03 | fail→fail | 19,683 | 14,290 | -27% | 1 | 1 | 0% | 4,031 | 4,817 | +19% | 0 | 0 | — |
case-04 | pass→pass | 12,570 | 4,417 | -65% | 1 | 1 | 0% | 2,387 | 2,356 | -1% | 0 | 0 | — |
case-05 | fail→fail | 125,201 | 8,613 | -93% | 1 | 1 | 0% | 1,803 | 2,715 | +51% | 0 | 0 | — |
case-06 | fail→pass | 14,006 | 5,983 | -57% | 1 | 1 | 0% | 1,992 | 2,364 | +19% | 0 | 0 | — |
case-07 | fail→pass | 9,434 | 5,509 | -42% | 1 | 1 | 0% | 1,652 | 2,431 | +47% | 0 | 0 | — |
case-08 | pass→pass | 10,909 | 11,799 | +8% | 1 | 1 | 0% | 1,829 | 3,735 | +104% | 0 | 0 | — |
case-09 | pass→pass | 9,322 | 7,633 | -18% | 1 | 1 | 0% | 1,548 | 2,873 | +86% | 0 | 0 | — |
case-10 | pass→pass | 6,738 | 3,487 | -48% | 1 | 1 | 0% | 1,061 | 2,025 | +91% | 0 | 0 | — |
case-11 | pass→pass | 13,293 | 12,193 | -8% | 1 | 1 | 0% | 2,361 | 3,367 | +43% | 0 | 0 | — |
case-12 | fail→pass | 7,324 | 7,421 | +1% | 1 | 1 | 0% | 1,446 | 3,026 | +109% | 0 | 0 | — |
case-13 | pass→fail | 11,589 | 12,646 | +9% | 1 | 1 | 0% | 2,176 | 4,109 | +89% | 0 | 0 | — |
case-14 | pass→pass | 14,347 | 4,019 | -72% | 1 | 1 | 0% | 2,530 | 2,342 | -7% | 0 | 0 | — |
case-15 | pass→pass | 7,616 | 4,150 | -46% | 1 | 1 | 0% | 1,420 | 2,237 | +58% | 0 | 0 | — |
case-16 | pass→pass | 7,146 | 3,509 | -51% | 1 | 1 | 0% | 1,203 | 2,060 | +71% | 0 | 0 | — |
case-17 | pass→pass | 5,783 | 2,535 | -56% | 1 | 1 | 0% | 1,004 | 1,957 | +95% | 0 | 0 | — |
case-18 | pass→pass | 6,241 | 3,453 | -45% | 1 | 1 | 0% | 1,106 | 2,165 | +96% | 0 | 0 | — |
case-19 | pass→pass | 8,095 | 3,444 | -57% | 1 | 1 | 0% | 1,553 | 2,196 | +41% | 0 | 0 | — |
case-20 | fail→fail | 14,526 | 11,360 | -22% | 1 | 1 | 0% | 2,645 | 3,640 | +38% | 0 | 0 | — |
case-21 | pass→pass | 23,627 | 23,168 | -2% | 1 | 1 | 0% | 4,521 | 5,140 | +14% | 0 | 0 | — |
case-22 | pass→pass | 18,838 | 16,900 | -10% | 1 | 1 | 0% | 3,866 | 4,858 | +26% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.