Read a loaded page as JSON rows and columns, with document and row provenance. This is a passive read: extraction never clicks, scrolls, changes focus, fetches another page, or follows pagination. It requires a matching CLI, daemon and extension build containing the tool.extract method.
bsk extract discover --session <id>
bsk extract table --session <id> --selector '#orders'
bsk extract table --session <id> --target <target_id> --format csv --out orders.csv
bsk extract list --session <id> --selector '#results' --item-selector '.result' --fields fields.jsonChoose exactly one of --selector, --target or --ref. A selector must match one container in the main document. Without these options, table/list extraction requires exactly one visible semantic container in that document; layout containers marked role="presentation" or role="none" are ignored, as in discovery. Discovery searches open shadow roots and available frames, including OOPIFs. Use its target_id to read those containers; ordinary observation refs often describe controls rather than the containing table. A fresh DOM ref is also accepted; visual-region refs are not.
Discovery handles expire after five minutes and are scoped to the session, tab, attachment and frame document. Navigation, node removal or a new attachment invalidates them. Rediscover after such changes. Discovery is capped at 16 frames, 32 returned targets and 8 preview column headers per target (row headers are not previewed); partial results are marked. A discovery selector limits the search to a subtree of the main document. Tables and grids take priority over lists within the visited DOM/frames. Traversal or time limits return the complete targets found so far. A target handle supplies its original tab when --tab-id is omitted; an explicit different tab is rejected. Discovery and extraction check the embedding iframe and its ancestors, including outer frames. Hiding a frame after discovery makes its retained targets return extract_target_hidden until the frame becomes visible again.
Fields files (or --fields-json with the same object) contain:
{
"fields": [
{ "key": "title", "name": "Title", "selector": "h3", "read": "text" },
{ "key": "url", "selector": "h3 a", "read": "href" },
{ "key": "sku", "selector": ":scope", "read": "attribute", "attribute": "data-sku" }
]
}Field selectors are relative to each item; :scope reads the item itself. Missing/hidden matches or absent attributes become null. More than one visible match is an error, rather than an arbitrary choice. Links are resolved against the document's base URL. Default semantic-list fields are text and url; an item with multiple visible links retains its text and returns a null url with ambiguous_default_url. Use explicit field selectors to choose a link. Arbitrary repeated cards need both a container and an item selector. Fields files are limited to 256 KiB.
The canonical result has schema_version 1, kind, tab_id, and:
| Field | Meaning |
|---|---|
| columns | Stable keys (c1, c2, … for tables), display names, hierarchical header_path, and name_source: header, generated or field. |
| rows | Records keyed by column key. Values are strings or null; order IDs, dates and amounts are not coerced. |
| source | Page URL, frame URL/ID, title, UTC capture time, and explicit selector/target when used. |
| row_sources | Zero-based output row, one-based DOM/ARIA row position including headers, row_kind (data or footer) and diagnostic locator. |
| spans | Merged-cell anchors: output row, column key, row_span and column_span. Covered positions are null, not copied values. |
| coverage | Loaded-DOM scope, returned count, truncation/stop reason, declared ARIA counts and completeness. |
| warnings | Generated names, partial DOM data, varying header associations or unavailable frames. |
| targets | Discovery only: handles, container kinds/names, frame provenance and column previews. |
CLI and plugin JSON list each row's keys in column order (c1, c2, ..., c10). columns remains the authoritative order and naming; read row[column.key]. CSV uses the same key mapping.
Native HTML tables and ARIA tables/grids/treegrids support row/column spans, row headers, multiple header rows, explicit header IDs, footer rows and ARIA row/column indices. Nested tables are independent targets; their cells do not leak into the parent. Duplicate names retain different column keys. Missing/empty headers become Column N; a headerless table keeps its first data row. Native spans use the browser's HTML parsing rules, including rowspan=0. Invalid ARIA spans fall back to one with invalid_aria-rowspan/invalid_aria-colspan warnings. Rows with aria-rowindex (ARIA grids and native tables) and list items with aria-posinset are sorted by logical position, since virtualized views may recycle DOM order. Invalid aria-rowindex/aria-colindex/aria-posinset/aria-setsize values fall back to DOM order with invalid_ warnings instead of failing. Split rows sharing an explicit index are joined only when all fragments supply explicit, non-overlapping column indices and agree on row kind; ambiguous/conflicting fragments fail. Reordering and merges are reported as aria_rows_reordered and split_aria_rows_merged; list items sharing a position are kept in DOM order with duplicate_item_positions. Merged row locators join the contributing locations with " | ". A row cap still collects later fragments (for example pinned-column containers) of rows already accepted. If a budget stops collection before every fragment of an accepted row is read, that row is dropped and split_aria_rows_incomplete is reported, rather than returning its unread columns as null. Treegrid extraction reads loaded rows; it does not expand groups or infer an unrendered hierarchy.
Hidden rows are omitted. Hidden cells, missing fields and span-covered cells are null; an existing empty cell is an empty string. Offscreen rendered DOM content can be read. Script/style/template content, password inputs and BrowserSkill overlays are excluded. Hidden rows produce hidden_rows_omitted; their known indices alone do not imply missing virtual rows. Locators are diagnostic, not durable selectors; ::shadow indicates a shadow boundary. Default row positions count only rows belonging to the selected table/grid, including its hidden rows; nested grids do not consume outer row positions. Explicit aria-rowindex values remain authoritative.
An ancestor's visibility:hidden does not discard descendants that restore visibility:visible. Their visible text and controls are retained, while the ancestor's hidden text is omitted. display:none, zero opacity and collapsed table rows still suppress their subtrees.
Text follows open shadow roots and assigned slots, collapses ordinary template whitespace, and keeps BR/block boundaries and preformatted whitespace. Content that is not rendered as text is skipped: SVG title/desc/defs (only SVG text elements contribute) and everything but the summary of a closed details element. Text-like inputs and textareas return live values; selects return selected option labels (one per line for multiple selections). Password/hidden inputs and checkbox, radio, file and image input values are not exported as text. This is DOM-based text extraction, not OCR or a complete reproduction of CSS generated content.
Span counts retain the page-declared extent, including omitted rows. HTML rowspan=0 is resolved within the collected row group; if collection was truncated, the rowspan_zero_extent_truncated warning marks that extent as provisional.
coverage.dataset_complete is never asserted true. It is unknown when the loaded DOM provides no proof of completeness, or incomplete on truncation, known missing ARIA rows/columns, or unavailable frames. A grid declaring 101 rows but loading only rows 51–52 is not a 100-record export. Compare returned counts, declared counts and warnings before using the result for totals.
| Option | Default | Allowed range |
|---|---|---|
| --max-rows | 500 | 1–5000 data/footer rows |
| --max-columns | 100 | 1–200 logical columns |
| --max-bytes | 1048576 | 1024–4194304 bytes of compact canonical JSON, including metadata |
| --timeout-ms | 5000 | 100–15000 milliseconds |
These are upper bounds, not promises that every page will reach them. The collector also caps traversal work in proportion to the requested shape (100,000 steps plus max_rows × (6 × max_columns + 48), at most 5,000,000) and header fragments at 32. Wider or more deeply nested content can exhaust the time or byte budget first. The byte budget counts emitted text, so collapsed template whitespace is free. Row locator sibling positions are cached so ordinary long tables do not incur quadratic scans. Time checking is cooperative inside the collector; cancellation prevents publishing a cancelled result and releases remote object groups. An unresponsive renderer remains subject to the existing CDP/transport timeouts.
Only complete rows are returned. If metadata or the first complete row cannot fit, extraction fails explicitly. Pretty-printed JSON and CSV files may exceed the compact JSON budget. Ambiguous selectors/fields, malformed ARIA layouts, unsupported targets and stale handles return structured errors. No automatic pagination, infinite-scroll traversal, canvas/OCR, closed shadow-root discovery or spreadsheet type inference is performed.
JSON is the default. Without --out, JSON or CSV goes to stdout; CSV summaries/warnings go to stderr. With --out, the CLI writes on its own host and prints a receipt; the global --json flag makes that receipt JSON. CSV plus global --json requires --out.
CSV uses UTF-8 and CRLF record separators, quoting commas, quotes and newlines. Duplicate column names are disambiguated. Every row includes _source_url, _source_frame_url and _source_row. A file export also writes .meta.json with canonical columns, CSV headers, source, row provenance, spans, coverage, warnings, null positions and the CSV SHA-256.
Raw CSV preserves strings, including formula-like values. Use --csv-safe when opening untrusted page data in a spreadsheet: it prefixes formula-like values with an apostrophe and records originals in metadata's escaped_cells. Spreadsheet applications may still infer numbers/dates from raw CSV; use JSON or explicit import types when preserving leading zeroes matters.
Existing output or metadata files are refused unless --overwrite is supplied. Each file is staged beside its destination and committed atomically. The CSV and metadata are two separate commits, not a filesystem transaction; the sidecar is committed first. A failure between commits reports an error, and the SHA-256 allows a consumer to detect a mismatched pair.
The existing bsk_inspect tool gains action: "extract":
bsk_inspect({ action: "extract", session: "<id>", extractKind: "discover" })
bsk_inspect({ action: "extract", session: "<id>", extractKind: "table", extractTarget: "<target_id>" })
bsk_inspect({ action: "extract", session: "<id>", extractKind: "table", selector: "#orders",
extractFormat: "csv", output: "/absolute/path/orders.csv", csvSafe: true })
List extraction uses itemSelector and the fields-object JSON string in extractFields. Budgets are maxRows, maxColumns, maxBytes and extractTimeoutMs. Rows returned inline enter the model context, so maxBytes defaults to 262144 there (1048576 when writing output), and the plugin renders compact JSON. CSV requires an output path so the plugin always receives structured JSON. The plugin uses the normal session registry, runner and queue; it does not introduce arbitrary page-script evaluation. Output paths are on the CLI host; relative paths resolve against that process's working directory. Prefer absolute paths in harness calls. Extraction shares bsk_inspect's output parameter with debug export and video save. Its model guidance lives in the bsk_inspect description and parameter schema rather than a separate DSH skill reference, keeping the DSH skill entry within its 4,500-byte budget in both LF and CRLF checkouts.
The extension collector reads DOM facts in an isolated world; a pure normalizer builds the logical table. CDP document checks surround collection, and discovery retains bounded node identities rather than remote objects or page contents. The daemon classifies tool.extract as a passive read and forwards the protocol. The CLI owns CSV encoding and local files.
Focused normalization/lifecycle and CSV tests accompany the implementation.