Documentation
Migrate from Firecrawl
Translate routes, formats, actions, cache units and result envelopes.
Swarmtab runs browsers on your Mac and exposes its own MCP and REST tools. It is not a Firecrawl-compatible endpoint. This guide describes Firecrawl v2 and Swarmtab nineteenth checkpoint, checked on 17 October 2026. See the official scrape API, crawl, map, and batch contracts before translating an application.
Connection and cleanup
Run examples from a Swarmtab source checkout with Node 22+. Set SWARMTAB_URL to the actual http://127.0.0.1:<port> and SWARMTAB_AGENT_TOKEN privately from the running app’s agent connect information. These examples use the checkout SDK, not a published npm package. Never use the window token in agent code.
| Firecrawl | Swarmtab |
|---|---|
| Hosted API bearer key | Local agent bearer token |
| Independent account requests | First POST /v1/<tool> creates an agent session |
| Job/account identity | Preserve Swarmtab-Session-Id from the response on every later call |
| Finish a request | Keep the session while polling; DELETE /v1/session ends it and stops unfinished work |
| Provider session/retention rules | Defaults: 600-second disconnected grace, 21,600-second maximum life, configurable by the human; server restart invalidates REST sessions |
The SDK keeps the header automatically. A raw REST caller must check both HTTP status and isError. A tool refusal can be HTTP 200. Missing a session header creates a new agent; it cannot poll the old agent’s jobs. After a lost action response, inspect the page before retrying.
import { Swarmtab } from './sdk/node/index.mjs';
const browser = new Swarmtab({
baseUrl: process.env.SWARMTAB_URL,
token: process.env.SWARMTAB_AGENT_TOKEN,
});
try {
const page = await browser.site('scrape', {
url: 'https://example.com', format: 'markdown', max_age: 0,
});
console.log(page.content.filter(c => c.type === 'text').map(c => c.text).join('\n'));
} finally {
await browser.close();
}
Runnable equivalent: node examples/integrations/migrate.mjs firecrawl https://example.com.
Routes and jobs
| Firecrawl v2 route | Swarmtab route and JSON arguments | Difference |
|---|---|---|
POST /v2/scrape |
POST /v1/site, {action:"scrape",url,format:"markdown"} |
One format; no formats array |
POST /v2/map |
/v1/site, {action:"map",url} |
Exact origin; strips query strings; no titles or relevance ranking |
Map search |
Map query |
Case-insensitive URL substring filter, not semantic ranking |
POST /v2/crawl |
/v1/site, {action:"crawl",url,max_pages:25,max_depth:2,sitemap:"skip"} |
Explicit defaults; same-site traversal may leave the starting path |
POST /v2/batch/scrape |
/v1/site, {action:"batch",count:total,urls:firstChunk} then {action:"batch",job,urls:nextChunk} |
1–100 URLs per chunk, 5,000 total, all chunks within 60 seconds; SDK batch(urls) handles submission |
GET /v2/crawl/:id, batch status |
/v1/site, {action:"status",job:"j1"} |
Text status in MCP envelope; waitJob returns that envelope, not a document array |
DELETE /v2/crawl/:id, batch cancel |
/v1/site, {action:"cancel",job:"j1"} |
Same owning session required; already dispatched actions cannot be undone |
POST /v2/search |
/v1/site, {action:"search",query,count:5} |
Human configures Brave/Tavily key and budget; count 1–20, scrape at most 10; results use address, rank, snippet |
POST /v2/extract |
Per-page read/site(scrape) with top-level schema |
No wildcard or combined cross-page extraction job |
Map continuation is {action:"map",cursor:<returned cursor>} in the same session. Continue while the cursor is non-null, even if limited:false. Cursors last five minutes; a new map replaces that snapshot. Crawl maximums are 5,000 pages and depth 10; budgets count attempts, including duplicates. Jobs retain the last 20 job records. Original per-page results are retained separately from the 200-page scrape cache, bounded to 16 MiB per job and 50 MiB per agent. Retrieve them with site(status) and offset/limit, following next_offset. Saved-space batches publish owner-only batch.jsonl; private batches keep results in memory without an artifact.
Deterministic schema crawls run as background jobs up to 5,000 pages; schema batches accept 1–100 URLs in one submission without chunking. Poll status and page retained results; each row has JSON text containing the extraction envelope. Saved-space schema crawls keep resumable sealed checkpoints. Actual client-model sampling keeps its 16-page, 180-second collection limit.
Formats and extraction
| Firecrawl format | Swarmtab | Boundary |
|---|---|---|
markdown |
format:"markdown" |
read defaults to controls; site(scrape) defaults to Markdown |
html |
format:"clean_html" |
Cleaned rendered HTML |
rawHtml |
read({format:"html"}) after open |
Live DOM, not original response bytes |
rawBase64 |
Unsupported | No original-body Base64 mode |
links, images |
Same single format names |
Output shapes differ |
screenshot / option object |
Single format:"screenshot" or screenshot({full:true}) |
MCP image/artifact, not a hosted download URL |
{type:"json",schema} |
Top-level schema, format:"json" |
Deterministic evidence extraction without a model by default |
JSON prompt without schema |
Unsupported in ordinary REST | Use caller-owned model with bounded page evidence |
summary, question/answer |
Sampling-capable MCP client or caller model | REST has no sampling channel |
product, branding |
Same single format names | Different shapes and narrower coverage; product identity mixing was reported at this base |
changeTracking |
site(changes) / approved monitors |
No tagged scrape-history or arbitrary schema-diff format compatibility |
For combined output, first open(url), then read({formats:"markdown,links,screenshot"}). The plural value is a comma-separated string, not an array. Allowed combined read modes are markdown,links,json,screenshot; json uses CSS fields. HTML is not combinable. structuredContent.data holds the combined payload. A screenshot reference such as swarmtab://agent/self/downloads/... is an agent-scoped resource, not an HTTP URL.
For a schema read, the extraction envelope is nested:
{"content":[],"structuredContent":{"untrusted":true,"data":{"path":"deterministic","complete":true,"data":{"title":"Fixture book","price":19.95},"missing":[],"sources":{}}}}
This is an illustrative shape; inspect the actual content and source metadata. Read the extracted value at result.structuredContent.data.data. Tool success can still mean complete:false. Check missing, sources, evidence truncation and paging. The example result reader in examples/integrations/migration-results.mjs requires completeness and a caller-supplied throwing validator. Existing SDK extractSchema(schema,{model}) / extract_schema(schema,model=...) callbacks run in the caller; callback output is explicitly validated:false and must be independently validated.
Actions and duration units
| Firecrawl action | Swarmtab scrape actions entry |
Difference |
|---|---|---|
{type:"click",selector:"#more"} |
{click:"#more"} |
Add submit:true explicitly if it submits a form |
write to focused field |
{type:{selector:"#name",text:"Visitor"}} |
Explicit unique selector; replaces the field value |
{type:"press",key:"Enter"} |
{press:"Enter",submit:true} |
Submission permission is explicit |
{type:"wait",milliseconds:500} |
{wait:500} |
0–10,000 milliseconds per scrape wait |
| Wait for text | {wait:{text:"Ready"}} |
Text wait, with ten-second bound |
| Selector-visible wait | Unsupported | Count assertions are not visibility waits |
| Directional scroll | {scroll:600} / {scroll:-600} |
Signed pixels; no element/direction object translation |
executeJavascript, intermediate screenshot/scrape/PDF |
Separate read(format:"script"), screenshot, files(pdf) calls |
No equivalent action inside one scrape sequence |
Scrape sequences accept at most 20 actions, versus Firecrawl’s documented 50. For example:
await browser.site('scrape', {
url, format: 'markdown',
actions: [{ type: { selector: '#name', text: 'Visitor' } }, { click: '#more' }, { wait: 100 }],
});
| Duration | Units / translation |
|---|---|
Firecrawl maxAge |
Milliseconds; convert to Swarmtab max_age seconds, rounding down to avoid older-than-requested cache |
Swarmtab max_age omitted or 0 |
Fresh read; no two-day Firecrawl cache default |
Swarmtab scrape action {wait:500} |
Milliseconds |
Standalone wait({seconds:10,text:"Ready"}) |
Seconds |
Node request timeout and waitJob({timeout,interval}) |
Milliseconds |
Python request timeout and wait_job(timeout=...,interval=...) |
Seconds |
| Monitor interval | Seconds; minimum 900; calendar strings use UTC, e.g. weekdays@09:00 |
Unsupported options to remove or redesign
There is no generic passthrough. Swarmtab rejects unknown fields. Do not copy onlyMainContent, includeTags, excludeTags, Firecrawl waitFor, timeout, browser headers, cookies, proxy, location, mobile, blockAds, removeBase64Images, skipTlsVerification, storeInCache, zeroDataRetention, parsers, screenshot option objects, extraction prompt, PII redaction, or prompt-injection classification into a Swarmtab scrape call. Some concerns have separate human-owned settings; they are not per-request aliases.
Crawl external-link scope, per-job concurrency, map query-string preservation, search news/images/categories/domain/date-range controls, goal judging, email/Slack alerts and cron/timezone schedules have no direct equivalent. Webhooks use terminal counts and X-Swarmtab-Signature, not Firecrawl’s events/page documents/signature. Do not reuse an existing receiver unchanged. No hosted fleet or automatic CAPTCHA solving is supplied. Models, search and proxy providers remain caller/human-owned services with separate costs.