Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Apply production-ready Firecrawl SDK patterns for TypeScript and Python. Use when implementing Firecrawl integrations, building reusable scraping services, or establishing team coding standards for Firecrawl. Trigger with phrases like "firecrawl SDK patterns", "firecrawl best practices", "firecrawl code patterns", "idiomatic firecrawl", "firecrawl wrapper".
.claude/skills/jeremylongshore-firecrawl-sdk-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-12 | ✓→✗ | ▼ Worse | 44% | 0% |
Production-ready patterns for Firecrawl SDK (@mendable/firecrawl-js / firecrawl-py). Covers singleton client, typed wrappers, retry with backoff, response validation, and reusable scraping service patterns.
@mendable/firecrawl-js installedtypescript// src/firecrawl/client.ts import FirecrawlApp from "@mendable/firecrawl-js"; let instance: FirecrawlApp | null = null; export function getFirecrawl(): FirecrawlApp { if (!instance) { if (!process.env.FIRECRAWL_API_KEY) { throw new Error("FIRECRAWL_API_KEY environment variable is required"); } instance = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY, ...(process.env.FIRECRAWL_API_URL ? { apiUrl: process.env.FIRECRAWL_API_URL } : {}), }); } return instance; }
typescript// src/firecrawl/scrape.ts import { getFirecrawl } from "./client"; interface ScrapeResult { url: string; title: string; markdown: string; links: string[]; scrapedAt: string; } export async function scrapePage( url: string, options?: { waitFor?: number; includeLinks?: boolean } ): Promise<ScrapeResult> { const firecrawl = getFirecrawl(); const formats: string[] = ["markdown"]; if (options?.includeLinks) formats.push("links"); const result = await firecrawl.scrapeUrl(url, { formats, onlyMainContent: true, ...(options?.waitFor ? { waitFor: options.waitFor } : {}), }); if (!result.success) { throw new Error(`Scrape failed for ${url}: ${result.error}`); } return { url: result.metadata?.sourceURL || url, title: result.metadata?.title || "", markdown: result.markdown || "", links: result.links || [], scrapedAt: new Date().toISOString(), }; }
typescript// src/firecrawl/retry.ts export async function withRetry<T>( operation: () => Promise<T>, config = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000 } ): Promise<T> { for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { return await operation(); } catch (error: any) { if (attempt === config.maxRetries) throw error; const status = error.statusCode || error.status; // Only retry on rate limits (429) and server errors (5xx) if (status && status !== 429 && status < 500) throw error; const delay = Math.min( config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500, config.maxDelayMs ); console.warn(`Firecrawl retry ${attempt + 1}/${config.maxRetries} in ${delay.toFixed(0)}ms`); await new Promise(r => setTimeout(r, delay)); } } throw new Error("Unreachable"); } // Usage: await withRetry(() => scrapePage("https://example.com"))
typescript// src/firecrawl/service.ts import PQueue from "p-queue"; import { scrapePage, type ScrapeResult } from "./scrape"; import { withRetry } from "./retry"; export class FirecrawlService { private queue: PQueue; constructor(concurrency = 3) { this.queue = new PQueue({ concurrency, interval: 1000, intervalCap: 5, // max 5 requests per second }); } async scrape(url: string): Promise<ScrapeResult> { return this.queue.add(() => withRetry(() => scrapePage(url))); } async scrapeMany(urls: string[]): Promise<ScrapeResult[]> { return Promise.all(urls.map(url => this.scrape(url))); } get pending(): number { return this.queue.pending; } }
typescriptimport { z } from "zod"; const FirecrawlScrapeResponse = z.object({ success: z.literal(true), markdown: z.string().min(1), metadata: z.object({ title: z.string().optional(), sourceURL: z.string().url(), statusCode: z.number().optional(), }), }); export function validateScrapeResponse(result: unknown) { const parsed = FirecrawlScrapeResponse.safeParse(result); if (!parsed.success) { console.error("Invalid Firecrawl response:", parsed.error.issues); return null; } return parsed.data; }
python# firecrawl_service.py import os from firecrawl import FirecrawlApp from functools import lru_cache import time @lru_cache(maxsize=1) def get_firecrawl() -> FirecrawlApp: """Singleton Firecrawl client.""" return FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"]) def scrape_with_retry(url: str, max_retries: int = 3) -> dict: """Scrape with exponential backoff.""" for attempt in range(max_retries): try: return get_firecrawl().scrape_url(url, params={ "formats": ["markdown"], "onlyMainContent": True, }) except Exception as e: if attempt == max_retries - 1: raise delay = (2 ** attempt) + (time.time() % 1) print(f"Retry {attempt + 1}/{max_retries} in {delay:.1f}s: {e}") time.sleep(delay)
| Pattern | Use Case | Benefit | |---------|----------|---------| | Singleton client | All SDK usage | One instance, consistent config | | Typed wrapper | Business logic | Compile-time safety | | Retry + backoff | 429 / 5xx errors | Automatic recovery | | Queue | Multiple URLs | Respect rate limits | | Zod validation | Any API response | Catch API changes early |
typescriptconst clients = new Map<string, FirecrawlApp>(); export function getClientForTenant(tenantId: string): FirecrawlApp { if (!clients.has(tenantId)) { const apiKey = getTenantApiKey(tenantId); clients.set(tenantId, new FirecrawlApp({ apiKey })); } return clients.get(tenantId)!; }
Apply patterns in firecrawl-core-workflow-a for real-world usage.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 27,296 | 19,691 | -28% | 1 | 1 | 0% | 4,860 | 5,291 | +9% | 0 | 0 | — |
case-02 | fail→fail | 28,899 | 22,259 | -23% | 1 | 1 | 0% | 5,002 | 5,824 | +16% | 0 | 0 | — |
case-03 | fail→fail | 19,387 | 19,869 | +2% | 1 | 1 | 0% | 2,786 | 5,031 | +81% | 0 | 0 | — |
case-04 | pass→pass | 36,619 | 6,943 | -81% | 1 | 1 | 0% | 2,732 | 3,327 | +22% | 0 | 0 | — |
case-05 | pass→pass | 13,100 | 16,373 | +25% | 1 | 1 | 0% | 2,593 | 4,210 | +62% | 0 | 0 | — |
case-06 | pass→pass | 22,226 | 15,897 | -28% | 1 | 1 | 0% | 3,402 | 5,242 | +54% | 0 | 0 | — |
case-07 | fail→fail | 23,107 | 17,873 | -23% | 1 | 1 | 0% | 3,349 | 4,603 | +37% | 0 | 0 | — |
case-08 | fail→fail | 20,108 | 21,597 | +7% | 1 | 1 | 0% | 2,715 | 5,154 | +90% | 0 | 0 | — |
case-09 | fail→fail | 25,560 | 12,110 | -53% | 1 | 1 | 0% | 4,197 | 4,558 | +9% | 0 | 0 | — |
case-10 | pass→pass | 21,582 | 15,023 | -30% | 1 | 1 | 0% | 3,311 | 3,993 | +21% | 0 | 0 | — |
case-11 | fail→fail | 14,482 | 12,091 | -17% | 1 | 1 | 0% | 2,659 | 4,270 | +61% | 0 | 0 | — |
case-12 | pass→fail | 20,885 | 12,269 | -41% | 1 | 1 | 0% | 3,055 | 4,390 | +44% | 0 | 0 | — |
case-13 | fail→fail | 16,782 | 15,799 | -6% | 1 | 1 | 0% | 2,184 | 3,830 | +75% | 0 | 0 | — |
case-14 | fail→pass | 20,330 | 13,933 | -31% | 1 | 1 | 0% | 3,059 | 4,797 | +57% | 0 | 0 | — |
case-15 | pass→pass | 16,500 | 17,778 | +8% | 1 | 1 | 0% | 2,291 | 4,565 | +99% | 0 | 0 | — |
case-16 | fail→pass | 19,643 | 16,436 | -16% | 1 | 1 | 0% | 2,198 | 4,077 | +85% | 0 | 0 | — |
case-17 | fail→pass | 14,636 | 12,152 | -17% | 1 | 1 | 0% | 1,863 | 3,318 | +78% | 0 | 0 | — |
case-18 | pass→pass | 7,664 | 6,526 | -15% | 1 | 1 | 0% | 1,639 | 3,243 | +98% | 0 | 0 | — |
case-19 | fail→pass | 14,691 | 9,862 | -33% | 1 | 1 | 0% | 1,939 | 2,847 | +47% | 0 | 0 | — |
case-20 | pass→pass | 24,408 | 22,496 | -8% | 1 | 1 | 0% | 4,418 | 5,840 | +32% | 0 | 0 | — |
case-21 | pass→pass | 18,808 | 12,131 | -36% | 1 | 1 | 0% | 2,969 | 3,482 | +17% | 0 | 0 | — |
case-22 | pass→pass | 10,671 | 5,298 | -50% | 1 | 1 | 0% | 1,383 | 3,128 | +126% | 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.