read_result
Read back a result that was too large to return inline. When a tool's JSON exceeds max_inline_chars, its response carries a preview, a result_handle and truncated: true; pass that handle here to slice the text, search it, page through it by line or read one JSON path — without fetching the page again or paying the tool's price a second time.
Use Cases
Find the Section You Need
Run a search for a heading or phrase, then slice from the match offset — two calls that read one section of a long page instead of the whole result.
Page Through Long Text
lines returns a window of lines with has_more, so a large markdown document is read in windows your context can hold.
Pull One Field From a Large JSON
json_path reads a single path from a stored crawl_deep, batch_scrape or deep_research result, or from the parsed body of a JSON fetch_url.
Never Re-Fetch
The stored result is the one you already paid for. Reading it costs 1 credit per call; re-running the tool costs its full price and hits the site again.
Endpoint
/api/v1/tools/read_resultParameters
handle comes from a response with truncated: true — this tool never fetches a page, it only reads a result you already have. A handle expires 1 hour after the result was stored.| Name | Type | Required | Default | Description |
|---|---|---|---|---|
handle | string | Required | - | The `result_handle` from a truncated response (`res_` followed by a UUID). Readable only by the account that created it, for 1 hour. Example: res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c |
operation | string | Required | - | `slice` returns a character range of the text view; `search` finds a case-insensitive literal substring (never a regex) and returns each match with context; `lines` returns a window of lines; `json_path` reads one path from the stored JSON. Example: search |
offset | number | Optional | 0 | `slice`: first character to return. `lines`: index of the first line to return. Example: 18240 |
length | number | Optional | 10000 | `slice`: number of characters to return (default 10,000). `lines`: number of lines to return (default 200, at most 5,000). Example: 4000 |
query | string | Optional | - | `search` only, and required there: the literal text to look for, matched case-insensitively. Example: rate limits |
max_matches | number | Optional | 20 | `search`: maximum matches to return, 1-100. `truncated: true` in the response means more matched than were returned. Example: 5 |
path | string | Optional | - | `json_path` only, and required there: dotted keys and array indexes, in dot or bracket form (`pages.0.url` or `pages[0].url`); no wildcards, filters or slices. Read from the stored result object — or from the parsed body when the stored text is JSON, such as a `fetch_url` body. Example: pages.0.url |
max_inline_chars | number | Optional | 40000 | Largest amount of text to return inline, 1,000-10,000,000 characters; every operation caps the text it returns here. For `json_path`, a larger `value` comes back as `value: null` with a `preview`, `truncated: true` and a warning to narrow the path. Example: 40000 |
Operations
Every response carries handle, tool, operation, view, view_path, total_chars and expires_at, then the fields of the operation you asked for.
offset, length, text and has_more. Defaults to the first 10,000 characters.query, matches (each with offset, length, context_offset and context — 200 characters either side), total_matches and truncated.first_line, line_count, total_lines, char_offset, lines and has_more. offset is the first line index, length the line count (default 200, at most 5,000).path, value and value_chars. Over max_inline_chars the value is replaced by value: null, a preview and truncated: true, with a warning to narrow the path.Where a stored result lives
~/.crawlforge/results/ (1-hour TTL, 200 MB LRU) and nothing is uploaded. Error responses are never stored, and batch_scrape jobs share the same store.Request Examples
cURL
# 1. A scrape whose JSON exceeded max_inline_chars (default 40,000) came back
# with a preview instead of the markdown:
# "truncated": true,
# "result_handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
# "total_chars": 182406
# 2. Search the stored result for the section you need (1 credit)
curl -X POST https://crawlforge.dev/api/v1/tools/read_result \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
"operation": "search",
"query": "rate limits",
"max_matches": 5
}'
# 3. Read the section at the first match offset (1 credit) — no second fetch
curl -X POST https://crawlforge.dev/api/v1/tools/read_result \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
"operation": "slice",
"offset": 18240,
"length": 4000
}'TypeScript
// npm install crawlforge-sdk
import { CrawlForge } from 'crawlforge-sdk';
const client = new CrawlForge({ apiKey: process.env.CRAWLFORGE_API_KEY });
// 1. Scrape a long page. Over max_inline_chars (default 40,000 characters of
// JSON) the markdown is replaced by a preview and a result_handle.
const page = await client.scrape({
url: 'https://example.com/docs/api',
formats: ['markdown'],
});
// page.data is untyped in crawlforge-sdk 0.1 — a truncated result carries the
// handle fields shown in the cURL tab instead of the markdown.
const scraped = page.data as
| { truncated: true; result_handle: string; total_chars: number }
| { truncated?: false; formats: { markdown: string } };
if (scraped.truncated) {
const { result_handle, total_chars } = scraped;
console.log('Stored ' + total_chars + ' characters as ' + result_handle);
// 2. Search the stored markdown for the section you need (1 credit).
const found = await client.readResult({
handle: result_handle,
operation: 'search',
query: 'rate limits',
max_matches: 5,
});
// result.data is untyped in crawlforge-sdk 0.1 — its shape is the Response Example below.
const { matches } = found.data as { matches: { offset: number }[] };
// 3. Slice from the first match offset (1 credit) — no second fetch.
const [first] = matches;
if (first) {
const section = await client.readResult({
handle: result_handle,
operation: 'slice',
offset: first.offset,
length: 4000,
});
const { text, has_more } = section.data as { text: string; has_more: boolean };
console.log(text);
console.log('More after this slice:', has_more);
}
} else {
console.log(scraped.formats.markdown); // small enough to arrive inline
}Python
# pip install crawlforge
from crawlforge import CrawlForge
client = CrawlForge() # reads CRAWLFORGE_API_KEY
# 1. Scrape a long page. Over max_inline_chars (default 40,000 characters of
# JSON) the markdown is replaced by a preview and a result_handle.
page = client.scrape(url='https://example.com/docs/api', formats=['markdown'])
# result.data is a plain dict — its shape is the Response Example below.
if page.data.get('truncated'):
handle = page.data['result_handle']
print(f"Stored {page.data['total_chars']} characters as {handle}")
# 2. Search the stored markdown for the section you need (1 credit).
found = client.read_result(
handle=handle,
operation='search',
query='rate limits',
max_matches=5,
)
# 3. Slice from the first match offset (1 credit) - no second fetch.
if found.data['matches']:
first = found.data['matches'][0]
section = client.read_result(
handle=handle,
operation='slice',
offset=first['offset'],
length=4000,
)
print(section.data['text'])
print('More after this slice:', section.data['has_more'])
else:
print(page.data['formats']['markdown']) # small enough to arrive inlineResponse Example
{ "success": true, "data": { "handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c", "tool": "scrape", "operation": "search", "view": "text", "view_path": "markdown", "total_chars": 182406, "expires_at": "2026-09-05T15:42:10.000Z", "query": "rate limits", "matches": [ { "offset": 18240, "length": 11, "context_offset": 18040, "context": "…request. Every plan is metered per API key.\n\n## Rate limits\n\nEach key may make one request per second on the Free plan…" }, { "offset": 61377, "length": 11, "context_offset": 61177, "context": "…returns 429 with a Retry-After header; see the rate limits table above for the per-plan ceilings…" } ], "total_matches": 2, "truncated": false }, "credits_used": 1, "credits_remaining": 998, "processing_time": 42}data.handleThe handle you passed, echoed backdata.toolThe tool that produced the stored resultdata.view`text` when the operation read the text view (the markdown for scrape, the body for fetch_url); `json` for `json_path`data.view_pathWhich field of the original result the text view isdata.total_charsSize of the stored text view, in charactersdata.expires_atWhen the stored result is deleted — 1 hour after it was storeddata.matchesOne entry per match: `offset` and `length` index the text view; `context` carries up to 200 characters either side, starting at `context_offset`data.total_matchesHow many matches exist in total; `truncated` is true when more matched than `max_matches` allowedcredits_usedCredits deducted for this request (1 per read)credits_remainingYour remaining credit balanceError Handling
Invalid Input (400 Bad Request)
handle is missing, operation is not one of slice, search, lines or json_path, search was sent without query, json_path without path, or max_matches falls outside 1-100 (VALIDATION_ERROR).
Path Not Found (400 Bad Request)
json_path could not resolve path (PATH_NOT_FOUND); the error names the keys available where it stopped. Credits are not deducted.
Result Not Found (404 Not Found)
Unknown or expired result handle (RESULT_NOT_FOUND) — results are kept 1 hour and are readable only by the account that created them. Credits are not deducted.
Storage Unavailable (503 Service Unavailable)
The result store could not be reached (STORAGE_UNAVAILABLE). Retry shortly; nothing is charged for a read that did not complete.
Insufficient Credits (402 Payment Required)
Your account doesn't have enough credits. Purchase more credits or upgrade your plan.
Rate Limit Exceeded (429 Too Many Requests)
You've exceeded your plan's rate limit. Wait a moment or upgrade your plan for higher limits.
search, then slice at the match offset, reads one section for 2 credits. Walking the whole result in 10,000-character slices costs 1 credit per slice, and re-running the tool costs its full price plus another fetch of the site.Credit Cost
Free Plan: 1,000 one-time credits = 1,000 requests
Hobby Plan: 5,000 credits/month = 5,000 requests ($19/mo)
Professional Plan: 50,000 credits/month = 50,000 requests ($99/mo)
Business Plan: 250,000 credits/month = 250,000 requests ($399/mo)
Related Tools
result_handle — max_inline_chars sets how much arrives inline (2 credits)json_path reads one page from the stored crawl (4 credits)json_path reads its parsed body (1 credit)batch_scrape job; the batch's stored results share the same 1-hour store (1 credit)