# Web

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 29-36 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 37-44 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 45-54 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 55-65 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 66-74 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 75-85 |
| [Decision Rules](#decision-rules) | tab, active-tab JavaScript, or fetch/XHR/resource timing evidence from a live tab. | 86-97 |
| [Browser Evidence Choice](#browser-evidence-choice) | Operating guidance and contracts for this section. | 98-106 |
| [Pattern: find a specific piece of info from a company domain](#pattern-find-a-specific-piece-of-info-from-a-company-domain) | Given a company domain and a piece of information you want (locations, leadership, pricing, recent news, etc.), there are two standard plays. | 107-127 |
| [Bridge-Backed Search](#bridge-backed-search) | `vf.web.search_start` accepts one complete query string or a sequence of query strings. | 128-168 |
| [Browser-Backed Page Capture](#browser-backed-page-capture) | `capture_pages` accepts one URL string or a sequence. | 169-303 |
| [Local Host Capture](#local-host-capture) | Use host capture inside generated workflows when local screenshots, rendered text, or page-load network evidence are part of the durable evidence trail. | 304-383 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 384-394 |
| [Verification](#verification) | Evidence required before reporting success. | 395-405 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 406-415 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 416-424 |
<!-- END GENERATED: guide-map -->

## Purpose

Use this guide when the task needs web search, public page extraction, screenshots, markdown capture, or browser evidence for a GTM workflow.

Web acquisition produces evidence, not truth by itself. Preserve the query or
seed URL, observed URL, capture time, source path, exact bytes/digest, per-item
failure, and the qualification rule that uses the evidence.

## Use This When

- Public search should discover pages, companies, people, events, news, or
  evidence that no narrower provider owns.
- Explicit URLs need rendered Markdown, HTML, screenshots, or request evidence.
- A Play needs repeatable public-web acquisition with bounded retries and
  durable output paths.

## Do Not Use This For

| Need | Owning reference |
| --- | --- |
| Logged-in provider identity, managed profile setup, or LinkedIn browser state | `host_browser.md` |
| Clay, Crunchbase, CRM, LinkedIn, or places operations with a typed provider contract | The provider-specific guide |
| Row extraction, classification, joining, or qualification after capture | `row_operations.md` |
| Artifact paging/export or final file delivery | `artifacts_jobs.md`, `csv_output.md` |
| Final Play UI/report design | `workflow_reports.md` |

## Preflight

- Confirm execution authority and provider readiness before Bridge-backed work.
- Define the search query or URL set, evidence formats, maximum results/pages,
  concurrency, timeout, and freshness window before starting.
- Decide what makes a page usable and which missing/blocked outcomes remain
  explicit failures. Do not let a successful HTTP/browser action substitute for
  the requested fact.
- Use a stable output directory inside the Play/workspace when the evidence must
  survive the turn.

## Risk And Side Effects

| Operation | Risk | Rule |
| --- | --- | --- |
| `vf.web.search_start(...)` | Provider/credit job | Bound queries and result count; preserve one job/artifact rather than restarting it. |
| `vf.host.web.capture_pages(...)` | Browser/provider work plus file writes | Bound URLs, formats, concurrency, and timeouts; preserve each successful and failed row. |
| `vf.host.chrome.*` | Authenticated Local browser action | Scope the exact tab/host and keep credentials or sensitive page bodies out of logs. |
| Using captured evidence for outreach or publish/send | Downstream customer-visible risk | Verify source relevance and route the effect through its owning guide. |

## Canonical Operating Pattern

1. Choose provider search, explicit-URL capture, or a typed provider guide.
2. Start one bounded job/capture and preserve its handle and output directory.
3. Wait or page the same job; do not relaunch because the initial preview is
   empty.
4. Keep successful sibling rows when one query or URL fails.
5. Inspect representative saved evidence, then extract or qualify through
   `row_operations.md` with explicit source lineage.
6. Save final rows/artifacts and verify requested semantics before reporting.

## Decision Rules

- Use `vf.web.*` for Bridge-backed provider work. Bridge owns provider keys, workspace policy, audit, usage tracking, retries, and future credit reservation.
- Use `vf.host.web.*` for explicit-URL browser capture. The call materializes requested evidence beneath the absolute `output_dir`; the active runtime owns browser provisioning.
- Use `vf.host.chrome.*` when you need an existing authenticated Local Chrome
  tab, active-tab JavaScript, or fetch/XHR/resource timing evidence from a live
  tab.
- Use provider-specific Local host adapters for logged-in browser workflows:
  `vf.host.linkedin_browser.*` and `vf.host.crunchbase.*`.
- Do not call Serper, Firecrawl, or another provider directly from SDK scripts. Public provider operations go through `vf.web.*`.
- Print `job_id`, row counts, output paths, and sample evidence before using web results for enrichment, qualification, personalization, or sequence staging.

## Browser Evidence Choice

| Need | Use | Why |
| --- | --- | --- |
| Public or internal page crawl where you control the URL list | `vf.host.web.capture_pages(..., network_log=True)` | Playwright attaches request/response listeners before navigation, so it captures page-load requests, textual response bodies, screenshots, markdown/html, and a local manifest. |
| Existing authenticated Chrome tab, logged-in app state, or active UI debugging | `vf.host.chrome.run(...)` and `vf.host.chrome.network_log(...)` | Operates against the operator's current Chrome tab/session. Use `network_log(action="start")`, trigger the UI action, then `network_log(action="drain")`. |
| Need full page-load response bodies from an authenticated site | Prefer `vf.host.web.capture_pages(..., network_log=True)` only when the page can be opened in Playwright with the needed state. Otherwise use Chrome network logging and accept that page-load bodies before recorder install may be missing. | Chrome injection happens after selecting/opening the tab; Playwright capture logs before navigation. |
| Search-result URL capture without logged-in state | `vf.web.search_start(...)`, then `vf.host.web.capture_pages(...)` | Search runs through Bridge. Page capture uses the public host-browser capture contract and returns rendered markdown/html/screenshots plus evidence. |

## Pattern: find a specific piece of info from a company domain

Given a company domain and a piece of information you want (locations, leadership, pricing, recent news, etc.), there are two standard plays. Pick by speed/cost; combine only when accuracy matters more than either.

1. **Landing page + link-follow** (default — fast, free, no credits).
   - Scrape the homepage with
     `vf.host.web.capture_pages([url], output_dir=..., formats=("markdown",))`.
   - Pull every link from the markdown.
   - Run `vf.rows.ai_analyze(mode="qualify", ...)` or `vf.rows.extract(...)` over the link list with a rubric like *"which of these links is most likely a /locations, /about, /leadership page?"* — let the LLM pick the relevant subpages.
   - Re-scrape the chosen subpages and pull the field out.

2. **SERP** (slower, costs credits, noisier — fallback).
   - `vf.web.search(["site:domain.com locations"])` or
     `vf.web.search(["<company name> news 2026"])`, then
     `vf.host.web.capture_pages(top_urls, output_dir=..., formats=("markdown",))`.
   - Use this when the info isn't on the homepage tree: recent posts, third-party listings, press, org-chart-as-published-in-news.

**Both** is most accurate, slowest, most expensive. Reserve for when accuracy matters more than cost.

**If the user names an approach, use that.** Otherwise default to landing-page + link-follow, fall to SERP only when that misses, run both only on explicit ask.

## Bridge-Backed Search

`vf.web.search_start` accepts one complete query string or a sequence of query
strings. A scalar is normalized to one query; it is never split into
characters.

```python
import verbiflow as vf

pending = vf.web.search_start(["YC W26 B2B AI companies"], num_results=10)
result = vf.jobs.result(pending).wait(timeout=300)
rows = result.rows(limit=10)
print(result.job_id, result.artifact_id, len(rows))
print([{key: row.get(key) for key in ("query", "rank", "title", "url", "error")} for row in rows[:3]])
```

`search_start` returns an initial `PendingJob`, not completed search rows. Preserve
its `job_id`, wrap that same handle with `vf.jobs.result(...)`, wait once, then
read a bounded row preview. Do not start a replacement search because the first
snapshot has no rows yet.

### Search Row Shape

Each row Bridge returns for `vf.web.search_start` is scoped to one query. Bridge
isolates query failures: a transient or persistent provider failure for one
query returns an `error` row for that query and does not fail successful sibling
queries in the same job.

| field | type | when present | meaning |
| --- | --- | --- | --- |
| `query` | str | always | the originating search query |
| `rank` | int | on success | 1-based result rank within that query |
| `title` | str | on success | provider result title |
| `url` | str | on success | provider result URL |
| `description` | str | on success | provider snippet/description |
| `error` | str | on failure | provider failure after Bridge retry/isolation |
| `attempts` | int | retried success or failure | Firecrawl attempts for this query |

If a row has `error`, skip or retry only that specific query later. Do not throw
away successful sibling query rows.

## Browser-Backed Page Capture

`capture_pages` accepts one URL string or a sequence. Use `timeout_ms=...` or
the seconds alias `timeout=...`, never both.

```python
from pathlib import Path

root = Path(__file__).resolve().parent
capture = vf.host.web.capture_pages(
    ["https://example.com"],
    output_dir=root / "outputs" / "web",
    formats=("markdown", "html", "screenshot"),
    timeout_ms=30000,
)
print({key: capture.get(key) for key in ("status", "manifest_path", "completed_count", "failed_count")})
for row in capture["rows"][:2]:
    print({
        key: row.get(key)
        for key in ("url", "status", "error", "markdown_path", "html_path", "screenshot_path")
    })
```

The top-level page collection is always `capture["rows"]`. There is no
`capture["pages"]` field. Requested content is written beneath `output_dir` and
its paths are returned on each row. In one-off control scripts, use an absolute
working path. In synced Play code, derive the absolute path
from `Path(__file__).resolve().parent` so the immutable Play stays portable.
The default formats are `("markdown", "html", "screenshot")`; override them
only when the task deliberately does not need one of those retained evidence
forms.

Managed runtimes can return metadata-only rows: page bodies, screenshots, and network
entries are never copied into the Python result or manifest. Each requested
artifact is a deterministic `capture-NNN.*` file with path, exact byte count,
and SHA-256 metadata on its row. This keeps tool output bounded regardless of
page size. The saved file is the exact **retained capture**, not necessarily the
complete upstream page. Check `html_truncated` with `html_source_bytes` and
`markdown_truncated` with `markdown_source_chars` before making completeness
claims. When a truncation flag is true, the saved artifact and its digest prove
the retained bytes only.

HTML captures also produce a `capture-NNN.structured.json` evidence sidecar.
It retains every raw Schema.org JSON-LD script found in the retained HTML, plus
a bounded normalized projection of typed entities such as events,
organizations, people, places, organizers, hosts, and sponsors. Each normalized
entity includes the source page, JSON-LD script number, and JSON pointer. This
is general evidence extraction, not a page-specific qualification rule. A zero
document count is not proof that the upstream page had no JSON-LD when
`structured_source_html_truncated` is true.

Do not print the full capture mapping or inline `html` / `markdown`. Browser
captures can be large. Print bounded status, counts, URLs, errors, and artifact
paths. Read only the specific saved file and range needed for the next step.

### Row Shape

Each page row returned by `vf.host.web.capture_pages(...)` has the common fields
below. The SDK records per-URL failures in the row payload. Do not throw away
successful sibling rows when one URL fails.

| field | type | when present | meaning |
| --- | --- | --- | --- |
| `url` | str | always | captured page URL |
| `status` | str | always | `completed` or `failed` for this row |
| `error` | str | on failure | per-URL failure detail |
| `markdown_path` | str | when markdown was requested and materialized | absolute saved Markdown path |
| `markdown_bytes` / `markdown_sha256` | int / str | with `markdown_path` | exact UTF-8 byte count and SHA-256 digest |
| `html_path` | str | when HTML was requested and materialized | absolute saved HTML path |
| `html_bytes` / `html_sha256` | int / str | with `html_path` | exact UTF-8 byte count and SHA-256 digest |
| `screenshot_path` | str | when screenshot was requested and materialized | absolute saved PNG path |
| `screenshot_bytes` / `screenshot_sha256` | int / str | with `screenshot_path` | exact PNG byte count and SHA-256 digest |
| `network_log_path` | str | with `network_log=True` | absolute saved JSON network evidence path |
| `network_log_bytes` / `network_log_sha256` | int / str | with `network_log_path` | exact JSON byte count and SHA-256 digest |
| `network_entry_count` | int | with `network_log_path` | number of captured network entries |
| `structured_evidence_path` | str | when HTML was materialized | absolute saved raw + normalized JSON-LD evidence path |
| `structured_evidence_bytes` / `structured_evidence_sha256` | int / str | with `structured_evidence_path` | exact sidecar byte count and SHA-256 digest |
| `structured_document_count` / `structured_parse_error_count` | int / int | with `structured_evidence_path` | JSON-LD scripts retained and scripts that could not be parsed |
| `structured_entity_count` / `structured_event_count` | int / int | with `structured_evidence_path` | bounded normalized typed entities and event subset |
| `structured_entities_truncated` | bool | with `structured_evidence_path` | whether the normalized projection reached its entity bound; raw retained documents remain in the sidecar |
| `structured_source_html_bytes` / `structured_source_html_truncated` | int / bool | with `structured_evidence_path` | upstream HTML byte count reported by the capture provider and whether the retained HTML was clipped |

Metadata-only rows never carry inline `markdown`, `html`, `screenshot_base64`, or
`network_entries`. Read the saved path only when its content is required.

### Provider extract row extensions

Local `mode="verbiflow_scrape"` uses Bridge-managed extraction and can add these
provider fields. A managed runtime may restrict the available modes and will
reject unsupported modes explicitly.

| field | type | when present | meaning |
| --- | --- | --- | --- |
| `html` | str | when `html` in formats | page HTML, capped at 250,000 chars |
| `html_chars` | int | when `html` present | total char count before the inline cap |
| `markdown` | str | when `markdown` in formats | main-content markdown, capped at 200,000 chars |
| `metadata` | dict | usually | title, ogImage, favicon, language, and provider metadata |
| `html_truncated` | bool | when html > 250,000 chars | inline `html` is clipped at 250KB — the full content is in `html_url` |
| `html_url` | str (presigned URL) | when `html_truncated` is True | one-shot S3 GET URL with the **full** HTML. TTL: 1 hour. Fetch with `httpx.get(html_url)`. |
| `html_full_bytes` | int | when `html_url` is set | byte length of the spillover object (use to verify GET completeness) |
| `html_url_expires_in_seconds` | int | when `html_url` is set | TTL of the presigned URL (3600s = 1 hour) |
| `source` | `"static_get"` | when Firecrawl returned <1KB and the static-GET fallback rescued | the html came from a plain `httpx` GET, not Firecrawl. Treat as raw response body (head + body intact, no main-content stripping). |
| `attempts` | int | when retries fired | number of Firecrawl attempts (1 = first try; >1 = retried after 429/408/5xx) |
| `final_html_chars` | int | when `error == 'no_usable_html'` | how many chars we did manage to fetch (for debugging) |

### Error Values

| `error` value | meaning | consumer action |
| --- | --- | --- |
| (absent) | success | use `html` / `markdown` / `html_url` as documented |
| `no_usable_html` | both Firecrawl and the static-GET fallback returned <1KB. Page is anti-bot blocked, JS-only with no static shell, or genuinely empty. | skip this row or fall back to `vf.host.web.capture_pages` for real-browser rendering |
| `Web provider request failed with status NNN (after K attempts)` | persistent upstream failure after Bridge's retry budget (429/408/5xx already retried with backoff up to 3 attempts) | skip this row; do not retry client-side — Bridge already did |
| `Web provider request failed with status 4xx` (no "after K attempts") | permanent client error (404, 410, 403 with no Retry-After). Not retryable. | skip this row |

### Handling Big Pages

For local `mode="verbiflow_scrape"`, when `html_truncated` is True, the inline
`html` is the first 250KB and `html_url` carries the full document. Fetch it
only when you need more than the head and opening body:

```python
import httpx

handle = vf.jobs.result(pending).wait(require_success=False, timeout=300)
for row in handle.iter_rows():
    if row.get("error"):
        continue  # skip rows Bridge already gave up on
    html = row.get("html") or ""
    if row.get("html_truncated") and row.get("html_url"):
        html = httpx.get(row["html_url"], timeout=60).text
    # ... your processing on full html ...
```

Most use cases (scheduler detection, og:tag extraction, brand language) fit comfortably in the inline 250KB and don't need the spillover fetch.

## Local Host Capture

```python
status = vf.host.web.status()
print(status)
capture = vf.host.web.capture_pages(
    ["https://example.com"],
    output_dir="/absolute/path/to/workflow/outputs/web",
    formats=("markdown", "screenshot"),
    concurrency=4,
    network_log=True,
)
print(capture["action_id"], capture["rows"][:1])
```

Use host capture inside generated workflows when local screenshots, rendered text, or page-load network evidence are part of the durable evidence trail. Keep output files under the workflow root. For "scrape homepage → pick relevant links → re-scrape" flows, use the C2 pattern above (scrape, then run `vf.rows.ai_analyze(mode="qualify", ...)` over the link list) instead of auto-crawling.

### Shared Chromium pool

Every `vf.host.web.capture_pages` call on a single machine shares one Chromium
browser instead of launching its own. The first call lazily starts a headless
Chromium, lets Chrome choose a free localhost DevTools port, records the
selected endpoint in local SDK state, and every subsequent call connects to
that browser over CDP. A sibling reaper subprocess polls recent use and kills
Chromium 30s after the last call finishes -- no orphan Chromes, no stale
processes.

**What this fixes:** running 8 capture_pages scripts in parallel used to spawn 8 separate Chromium processes (~4GB RAM, would hang the machine). Now it's 1 Chromium + 8 BrowserContexts (~1GB total, no hang). Scripts don't need to coordinate or call any setup.

**Knobs:**
- `VERBIFLOW_CHROME_CDP=http://localhost:PORT` — override to attach to a Chromium you started yourself. It must expose a real Chrome DevTools `/json/version` endpoint; otherwise `vf.host.web.status()` and `capture_pages` fail fast.
- `VERBIFLOW_CHROME_BINARY=/path/to/chrome` — override the binary the pool launches (default: Playwright's bundled Chromium → falls back to system Chrome).

**You don't need to call anything explicitly.** The pool is fully transparent. If you crash mid-capture or kill the script, the reaper still cleans up Chromium 30s later because it's its own detached subprocess.

**Debug artifacts.** If pool launch fails, inspect the blocked reason from
`vf.host.web.status()` first. The local Chrome launch log is
`~/.verbiflow/state/chrome.log`; it is useful for local machine problems such
as binary launch failures, profile lock issues, or an invalid
`VERBIFLOW_CHROME_CDP` override. Do not treat this log as durable workflow
evidence.

### Safe Parallelism

The pool browser comfortably handles ~16 simultaneous tabs on a typical laptop. Past that you start to see slow page loads + occasional context errors.

| Scenario | Recommended shape |
| --- | --- |
| One script, many URLs | `concurrency=4` (or up to 8). One browser, N tabs, no contention. |
| Multiple parallel scripts on the same machine | total tabs `(scripts × concurrency)` should stay **≤ 16**. So 4 scripts × `concurrency=4`, or 8 scripts × `concurrency=2`. |
| One script doing slow page interactions (clicks, waits, login) | `concurrency=2`. The pool isn't the bottleneck — your wait_for_selector is. |
| Mixed: some scripts scraping, some driving UI | leave headroom (`total tabs ≤ 12`). UI interactions are spikier than reads. |

**Tested:** 8 parallel scripts × `concurrency=2` × 20 URLs each (160 URLs total) → 97.5% success, 291s wall time, one shared Chromium throughout. The pre-pool version of this would hang the machine.

### Hard Limits and Defaults

| Knob | Default | Hard limit / cap | What happens past it |
| --- | --- | --- | --- |
| `concurrency` per call | 4 | 8 recommended, 16 absolute | `TargetClosedError` spikes, slow page loads |
| `(scripts × concurrency)` machine-wide | — | **≤ 16** | Same as above, plus context-creation timeouts |
| `timeout_ms` per page | 30000 | 5000 (floor) — 90000 (ceiling) | Page hangs forever, blocks the tab slot |
| `wait_until` | `"domcontentloaded"` | — | `"networkidle"` hangs on any site with analytics/SSE/polling — single biggest cause of slowness, do not use unless you specifically need it |
| `post_load_wait_ms` | 500 | Keep ≤ 2000 | Wasted wall time, multiplies by URL count |
| Pool idle timeout | 30s | — | Chromium auto-closes 30s after the last `capture_pages` call returns (reaper kills it) |
| Pool keepalive | 5s | — | While `capture_pages` is running it touches `last_used_ts` every 5s so long batches don't get killed mid-flight |

### Common Failure Modes

| You see | Likely cause | Fix |
| --- | --- | --- |
| `TargetClosedError` mid-batch | too many tabs (`scripts × concurrency > 16`), or the shared browser was stopped while work was in flight | reduce parallelism per the table above and rerun only failed URLs |
| `RuntimeError: Timed out waiting for peer process to launch shared Chromium` | another script is launching the pool and Chromium took >60s under load | retry; or run fewer scripts at once |
| `VERBIFLOW_CHROME_CDP is set but does not expose a reachable Chrome DevTools endpoint` | The override points at a non-CDP listener, wrong port, or stopped browser. | Unset the override or start Chrome with remote debugging and verify `<endpoint>/json/version` returns JSON. |
| `Chromium launched but did not write DevToolsActivePort` | Chrome failed before exposing CDP, usually binary/profile/local machine launch failure. | Check `~/.verbiflow/state/chrome.log`, clear stale local Chrome pool state if needed, and rerun `vf.host.web.status()`. |
| Per-URL `status: "failed"` with `error: "..."` | upstream site issue (slow, blocked, 4xx, anti-bot), not the SDK | accept as a partial result; rows already include the seed_url so you can retry just the failures |
| Workers hang forever | almost always `wait_until="networkidle"` on sites with analytics | switch to `"domcontentloaded"` |
| Machine RAM thrashes / fan spins | you're launching N separate Chromiums (pre-pool behavior) | make sure you're on a recent SDK — the pool ships with capture_pages, no opt-in needed |


## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.host.web.capture_pages` | Use for browser-backed crawling of explicit public/internal URLs when you need rendered markdown/html/screenshots and page-load network evidence materialized under an absolute output directory. | <strong>Required</strong>: `output_dir` (str \| Path; required): Absolute local directory for markdown/html/screenshots/network logs/manifest.; `urls` (str \| Sequence[str]; required): One seed URL/bare domain or a sequence of them. Bare domains are normalized to https://.<br><strong>Optional</strong>: `concurrency` (int; optional; default `4`): Maximum seed URLs to crawl concurrently.; `formats` (Sequence[Literal['markdown', 'html', 'screenshot']]; optional): Local artifacts to write for each captured page: markdown, html, screenshot.; `mode` (Literal['simple', 'medium', 'browser', 'verbiflow_scrape'] \| Sequence[Literal['simple', 'medium', 'browser', 'verbiflow_scrape']]; optional; default `'browser'`): Scrape cascade. Pass a single tier or an ordered array of tiers tried left-to-right per URL, e.g. ["simple","medium","browser","verbiflow_scrape"]. Tiers: simple = plain httpx GET (fastest, fails on anti-bot/SPA); medium = cloudscraper (beats simple Cloudflare challenges); browser = local Playwright with JS, scroll, screenshots, network logs; verbiflow_scrape = Bridge-backed page_extract (provider-managed, spends credits, returns markdown/html). Screenshot/network_log require "browser" in the cascade. Each URL stops at the first tier that returns usable content; "source" on each row records the tier that won.; `network_log` (bool; optional; default `False`): Capture Playwright request/response evidence during each page visit.; `network_log_max_body_chars` (int; optional; default `4000`): Maximum textual request/response body characters to store per network entry.; `network_log_record_headers` (bool; optional; default `False`): Whether to include request and response headers in network log files.; `post_load_wait_ms` (int; optional; default `500`): Additional wait after navigation before scroll/capture.; `scroll` (bool; optional; default `True`): Scroll through the page after navigation to trigger lazy-loaded content.; `timeout` (float \| None; optional; default `None`): Seconds alias for timeout_ms. Do not pass both timeout and timeout_ms.; `timeout_ms` (int \| None; optional; default `30000`): Maximum navigation/body extraction timeout per page in milliseconds. Do not combine with timeout.; `wait_until` (load \| domcontentloaded \| networkidle; optional; default `'domcontentloaded'`): Playwright navigation readiness condition for each page. | `dict` with `status`, `manifest_path`, `completed_count`, `failed_count`, and the page collection under `rows` (never `pages`). Requested content is materialized as row-level `markdown_path`, `html_path`, or `screenshot_path`; keep stdout bounded to status, URLs, errors, and paths rather than printing inline page bodies. | local_file_write | installed SDK: `verbiflow/host.py:1488-2012` |
| `vf.host.web.status` | Use before browser capture to confirm browser runtime readiness. | No inputs. | `dict` with browser runtime readiness plus a blocked reason when capture cannot run.<br><strong>Common next calls</strong>: `vf.host.web.capture_pages` | host_local | installed SDK: `verbiflow/host.py:1427-1486` |
| `vf.web.search` | Run Bridge-backed web search and return the completed or started job handle. | <strong>Required</strong>: `queries` (str \| Sequence[str]; required): Queries parameter for `web.search`.<br><strong>Optional</strong>: `extra` (Mapping[str, object] \| None; optional; default `None`): Advanced provider/backend options passed through deliberately; prefer typed args when available.; `num_results` (int; optional; default `10`): Num results parameter for `web.search`.; `search_type` (str; optional; default `'web'`): Search type parameter for `web.search`.; `wait` (bool \| int; optional; default `True`): If false, return immediately with a job/status handle; if true or a timeout integer, poll until completion or timeout. | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.host.web.capture_pages`, `vf.rows.ai_analyze` | artifact_job | installed SDK: `verbiflow/web.py:87-133` |
| `vf.web.search_start` | Start Bridge-backed web search under workspace policy and credit tracking. | <strong>Required</strong>: `queries` (str \| Sequence[str]; required): One web search query or a sequence of queries.<br><strong>Optional</strong>: `extra` (Mapping[str, object] \| None; optional; default `None`): Advanced provider/backend options passed through deliberately; prefer typed args when available.; `num_results` (int; optional; default `10`): Maximum results per query.; `search_type` (str; optional; default `'web'`): Provider-neutral search type. V1 supports web.; `wait` (bool \| int; optional; default `False`): When true, poll the returned job before returning. | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.wait`, `vf.artifacts.get`, `vf.host.web.capture_pages` | artifact_job | installed SDK: `verbiflow/web.py:16-85` |
<!-- END GENERATED: core-sdk-surfaces -->

## Verification

- Search work preserves `job_id`, `artifact_id`, query, rank, result URL, and
  per-query error rows; the inspected page is the one actually used downstream.
- Capture work preserves requested URL, final row status, output paths, exact
  byte counts/digests, and per-URL errors without printing full page bodies.
- Saved evidence exists at the returned path and a bounded sample supports the
  claimed fact or qualification.
- A partial or blocked source is disclosed as partial/blocked, not converted to
  a zero-result success.

## Troubleshooting

| Symptom | First check | Next action |
| --- | --- | --- |
| Initial search snapshot has no rows | Whether the returned `PendingJob` is still running | Wait on that job and page its artifact; do not start a replacement. |
| One query or URL failed | The row-level `error`, attempts, and successful sibling rows | Retry only the failed item when appropriate. |
| Capture output overwhelms tool context | Whether full HTML/Markdown was printed | Keep bytes in files and inspect one bounded range. |
| Rendered page is empty or blocked | Static versus managed-browser evidence path and page status | Use managed capture when rendering/auth state is required; preserve the blocked result. |
| Downstream list quality is weak | Source relevance, freshness, and qualification predicate | Revisit acquisition/qualification; more pages alone are not a quality fix. |

## Related References

| If you are doing this | Read next |
| --- | --- |
| Managed provider profile or logged-in browser state | `host_browser.md` |
| Repeatable source windows and checkpoints | `workflow_sources.md` |
| Row extraction, qualification, joins, or coverage | `row_operations.md` |
| Job/artifact paging and export | `artifacts_jobs.md` |
| Final Play UI/report | `workflow_reports.md` |
