# Row Operations

<!-- 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. | 27-32 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 33-42 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 43-53 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 54-64 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 65-73 |
| [Feed Qualification With Real Source Context](#feed-qualification-with-real-source-context) | Row qualification is only as good as what each row exposes to the rubric. | 74-130 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 131-145 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 146-159 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 160-192 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 193-205 |
| [Examples](#examples) | Representative usage patterns and code. | 206-369 |
| [Verification](#verification) | Evidence required before reporting success. | 370-378 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 379-389 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 390-400 |
<!-- END GENERATED: guide-map -->

## Purpose

Row Operations owns deterministic work over rows that have already been acquired: `RowSet` transforms, local CSV joins, variable coverage checks, column mapping, custom row annotation, and row qualification.

This reference is the bridge between acquired data and downstream artifacts, reports, enrichment, or sequence staging. It does not acquire provider data, own durable artifact lifecycle mechanics, or activate sequences.

## Use This When

- You already have rows in memory, a `RowSet`, artifact page records, an artifact id/`Artifact`, or an absolute CSV path and need deterministic transforms, filters, grouping, joins, or previews.
- You need to check whether rows cover required variables before a report, personalization step, source import, or sequence staging.
- You need to infer a local CSV column mapping before handoff.
- You need reviewable row-level scoring, classification, prioritization, or custom typed annotation over already-loaded rows — use **`vf.rows.ai_analyze(rows, mode=..., tier=..., fields=...)`**. Its three modes (`"qualify"`, `"classify"`, `"enrich"`) return JSON-Schema-validated structured output and chunk any input size internally.
- You only need a low-cost fixed-label match and do not need a generated rationale/confidence — use **`vf.rows.classify(...)`**. It is the distinct ScaleDown path; cached rows are free and only successful uncached provider calls consume credits.
- You need to **extract a small set of named string fields** from row text/HTML against an explicit schema (e.g. pull city/state/country from a freeform location string) — use `vf.rows.extract(rows, schema={...})`. Simpler than `ai_enrich` when the output is a few named string columns.
- You need to **decide whether each row sits inside a target geography** (continent, country, state/admin1, named metro, or city) — use `vf.rows.geo_classify(rows, target=...)`. Local geo tables can avoid LLM fallback surcharges, but every processed row still has the resolver-plan baseline cost. See `geo.md` for the cost model, 500-row cap, output audit columns, and ambiguous-target traps.

## Do Not Use This For

| Task | Owning Reference |
| --- | --- |
| Acquiring CRM, source, LinkedIn, web, Clay, Crunchbase, or provider data | `audience_discovery.md`, `crm.md`, `linkedin.md`, `web.md`, `host_browser.md`, `clay.md`, `crunchbase.md` |
| Canonical email, domain, phone, profile, company, or identity enrichment | `enrichment.md` |
| Waiting on jobs, paging durable artifacts, exporting artifacts, or staging artifacts downstream | `artifacts_jobs.md` |
| Sequence source authoring, staging, publish, send, or audience repair | `sequence_authoring.md`, `sequences.md` |
| Workflow source windows, checkpoints, replay, repair, or workspace Play sync | `workflow_sources.md`, `workflow_operations.md`, `workflow_schedules.md` |
| Final Play UI/report construction | `workflow_reports.md` |

## Mental Model

| Concept | Meaning |
| --- | --- |
| `RowSet` | Local, in-memory, deterministic row container. Use it for transforms and previews before starting platform work. |
| CSV | Local interchange format. CSV helpers require absolute paths and should not become the canonical workflow store. |
| Artifact | Durable platform rowset. Row Operations may start row jobs that produce artifacts, but artifact lifecycle, paging, export, and staging belong to `artifacts_jobs.md`. |
| Row qualification | Credit-consuming artifact job that scores/classifies rows with an explicit rubric. |
| Custom row annotation | Credit-consuming artifact job that adds caller-defined typed columns from a prompt and JSON schema. It is not canonical identity enrichment. |
| Sequence source | Downstream activation target. This reference can prepare rows; sequence docs own staging and publish/send safety. |

## Preflight

- Inspect the row count, column names, and stable identifiers before transforming or starting row jobs.
- Use local `RowSet` operations for deterministic facts such as normalized domains, exact missing fields, grouping, and coverage.
- Use semantic row qualification or custom row annotation for judgment calls such as ICP fit, buying-committee role, prioritization, or copy variables.
- Confirm workspace auth before any `rows.ai_analyze`, `rows.classify`, or `rows.extract` call because those start Bridge artifact jobs and may consume credits. Pricing is server-authoritative. All three `ai_analyze` modes execute as `rows.ai_enrich`, so inspect `vf.capabilities.get("rows.ai_enrich")` for its tier variants. Inspect `vf.capabilities.get("rows.classify")` and `vf.capabilities.get("rows.extract")` for the two ScaleDown rates.
- Use absolute CSV paths. Relative paths are rejected by the SDK.
- Do not run a row job on unknown large input until you know the row count, chunk size, expected output columns, and verification path.

## Feed Qualification With Real Source Context

Row qualification is only as good as what each row exposes to the rubric. A row with `name`, `title`, `company` and nothing else gives the LLM almost nothing — it will hallucinate or shrug. Before running `vf.rows.ai_analyze(mode="qualify", ...)`, decide what the rubric actually needs to read, and **scrape it onto the row first**. Then pass those column names as `fields=[...]` — `ai_analyze` validates them against actual row keys upfront and raises if you typo (no more silent 396/397-row passes from a wrong field name).

When the row's source of truth is a LinkedIn profile (a `linkedin_profile_url`, `LinkedIn Profile`, or similar column):

- Use `vf.enrichment.person_profiles(urls=[...], requested_field_keys=[...])` to pull headline, about/summary, experiences, education, location, follower counts, etc.
- Left-join the returned fields back onto the row by `linkedin_profile_url`.
- Then pass the enriched rows to `vf.rows.ai_analyze(mode="qualify", fields=[...], rubric=...)` where `fields` lists the scraped columns and the rubric references them.

When the row's source of truth is a company website / domain (a `company_domain`, `website`, `company_url`, or job-posting URL):

- Use `vf.host.web.capture_pages([...], output_dir=..., formats=("markdown",))`
  to capture the homepage, an about page, a careers page, or a specific URL
  referenced in the row. For multiple pages, call `vf.web.search(...)` first
  to find the right URL.
- Stage the extracted text/fields onto the row (`scraped_about`, `scraped_pricing`, `scraped_services`, etc.) via `RowSet.derive(...)` or a deterministic join.
- Pass the enriched rows to `vf.rows.ai_analyze(mode="qualify", fields=[...], rubric=...)` with the scraped column names in `fields`.

When the row references both (typical Clay people row), do both — LinkedIn for the person, website for the company — then qualify.

Failure mode to avoid: writing a rich rubric ("only B2B SaaS founders with >$5M ARR raised in last 18 months") against rows that only carry `name`, `title`, `linkedin_profile_url`. The model has nothing to anchor on and either rejects everything or hallucinates. Always: scrape → join → qualify.

Bridge-side enrichment (the old `with_resolution` mode) is gone. Scrape what the rubric needs explicitly and pass those column names in `fields=[...]` — the LLM only sees columns you list.

### Make LLM judgments reviewable

Every material LLM-derived score, qualification, classification, recommendation,
or inferred claim must carry a short evidence-grounded rationale and an explicit
confidence field in the durable output row. Model reasoning effort is not a
review artifact, and agents must not request or expose hidden chain-of-thought.
The required output is a concise decision basis a human can audit.

- `mode="qualify"` and `mode="classify"` return `reason` plus `confidence`
  (`high`, `medium`, or `low`) automatically.
- For `mode="enrich"`, add required `<decision>_rationale` and
  `<decision>_confidence` properties to `output_schema`. Use
  `analysis_rationale` and `analysis_confidence` when the row has one overall
  judgment; use field-specific names such as `fit_rationale` and
  `fit_confidence` when several independent inferences need separate review.
- The rationale must name the row evidence that drove the output and say what
  is missing or ambiguous. Confidence is about evidence sufficiency and
  ambiguity, not the model's tone or reasoning tier.
- Keep exact source evidence in separate URL/quote/timestamp fields. A rationale
  explains how evidence supports the decision; it does not replace evidence.
- A URL field is a pointer, not page evidence. `ai_analyze` sees only the
  columns named in `fields=` and does not fetch URLs. Product cues, website
  claims, pricing, use cases, or technical positioning may be scored only after
  the owning web/enrichment capability has acquired the relevant text and the
  extracted text, source URL, and retrieval time are joined onto the row.
- Missing model output is a failed or held row, not a low-confidence negative.
  Never silently substitute score `0`, `DQ`, blank rationale, or `low`
  confidence for an absent/error result.
- Deterministic calculations should expose their formula and component values
  as ordinary row fields. Do not ask an LLM to narrate arithmetic that code can
  explain exactly.

## Risk And Side Effects

| Operation | Side Effect | Rule |
| --- | --- | --- |
| `vf.from_records(...)`, `RowSet` transforms, `vf.rows.join_csv(...)`, `vf.rows.check_variable_coverage(...)`, `vf.rows.infer_column_mapping(...)` | Local only | Safe after inspecting input shape. |
| `vf.rows.ai_analyze(mode="qualify", ...)` | `artifact_job` | Pass/fail rubric filter. Preview inputs, write the rubric explicitly, list the columns the rubric needs in `fields=`, preserve `job_id`/`artifact_id`, and verify the returned `reason` and `confidence` before handoff. |
| `vf.rows.ai_analyze(mode="classify", ...)` | `artifact_job` | Fixed-label classifier. Labels are `[{name, rubric}, ...]`; names must be unique; rubrics must be specific ("is this a SaaS company?" beats "SaaS"). The winning label, grounded `reason`, and `confidence` must remain on every output row. |
| `vf.rows.ai_analyze(mode="enrich", ...)` | `artifact_job` | Generates user-defined columns from a `prompt` and `output_schema` (JSON Schema). Material judgments require rationale and confidence properties in that schema. Do not overwrite canonical identifiers; inspect returned issue/error fields before handoff. |
| `vf.rows.classify(...)` | `artifact_job` | Uses ScaleDown for a low-cost fixed-label match. Successful uncached provider calls use the server-published credit rate; cache hits are free. Use `ai_analyze(mode="classify")` instead when the decision needs a durable generated rationale and confidence. |
| `vf.rows.extract(...)` | `artifact_job` | Uses the ScaleDown extraction capability. Successful uncached provider calls use the server-published credit rate; cache hits are free. Schema fields must be CSV-safe identifiers; `top_n` between 1 and 10. Verify extracted fields against an authoritative source before activation. |
| `vf.rows.geo_classify(...)` | `artifact_job` | Uses the live server-published baseline rate plus the published rate for each uncached LLM fallback. Reject ambiguous targets (`Georgia` is both country and US state); use an actually supported unambiguous target such as `GE`/`GEO` for the country. See `geo.md` for the full metering model and supported metros. |
| `vf.rows.geo_supported_targets()` | `readonly_api` | Free, no credits. Call before passing an obscure metro name to confirm it resolves. |
| Artifact paging/export/staging after a row job | Read/write depending on operation | Switch to `artifacts_jobs.md`. |
| Sequence staging/publish/send after row preparation | Staging or customer-visible mutation | Switch to `sequences.md`; publish/send requires explicit confirmation. |

## Canonical Operating Pattern

1. Acquire rows through the owning provider/workflow reference, or load rows from a known local CSV/report output.
2. Convert rows to `RowSet` with `vf.from_records(...)` when local shaping is needed.
3. Normalize crisp identifiers deterministically: email domains, company domains, lowercased URLs, exact IDs, and source keys.
4. Check variable coverage and inspect samples before any report, personalization, or staging handoff.
5. Use local filters for crisp facts. Use row qualification or custom annotation jobs only for semantic judgments or generated columns.
6. Require a grounded rationale and confidence for every material LLM judgment; preserve exact evidence separately and keep deterministic formulas explicit.
7. Preserve every `job_id` and `artifact_id` from row jobs, then use `artifacts_jobs.md` to wait, page, export, report, or stage.
8. Verify final counts, missing fields, rationale/confidence coverage, duplicate policy, and rejected/error rows before any downstream mutation.
9. Derive crisp bands and tiers deterministically from the validated score and
   qualification status. Do not ask the model to reproduce a threshold formula
   that code can enforce exactly.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
Client SDK local `RowSet` and expression helpers:

| Client SDK Surface | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.from_records(rows, label=None)` | Build a local `RowSet` from in-memory dictionaries before deterministic transforms or SDK handoff. | `rows` (`Iterable[Mapping[str, object]]`, required): in-memory row dictionaries. `label` (`str \| None`, optional): debug/source label. | Returns `RowSet`. Preserve the handle when chaining local transforms; rows are normalized to JSON-compatible dictionaries. | local | installed SDK: `verbiflow/rowset.py:298` |
| `RowSet.count()` / `len(rowset)` | Check local row volume before sampling, provider jobs, reports, or staging. | No inputs. | Returns integer row count. Preserve the count in status output when it gates downstream work. | local | installed SDK: `verbiflow/rowset.py:173` |
| `RowSet.to_list()` | Convert local rows to plain dictionaries for inspection, tests, or SDK calls expecting iterable mappings. | No inputs. | Returns copied `list[dict]`. There is no `.records` property on `RowSet`; preserve canonical IDs in the row dictionaries. | local | installed SDK: `verbiflow/rowset.py:176` |
| `RowSet.sample(n, seed=None)` | Preview a bounded deterministic subset without changing the original `RowSet`. | `n` (`int`, required): maximum sample size. `seed` (`int \| None`, optional): deterministic sampling seed. | Returns sampled `RowSet`; `n <= 0` returns an empty `RowSet`. Preserve sample counts and seed when debugging. | local | installed SDK: `verbiflow/rowset.py:179` |
| `RowSet.take(n)` | Take a leading preview slice for display or quick verification. | `n` (`int`, required): number of leading rows. | Returns first `n` rows as a new `RowSet`; negative values produce an empty set. | local | installed SDK: `verbiflow/rowset.py:185` |
| `RowSet.filter(predicate)` | Apply deterministic local filters for crisp facts only. | `predicate` (`Callable[[dict], bool]` or `Expr[bool]`, required): local deterministic filter. | Returns filtered `RowSet`; preserve rejected-count context outside the handle if it matters. | local | installed SDK: `verbiflow/rowset.py:188` |
| `RowSet.derive(name, value, **values)` | Add deterministic derived columns such as normalized domains or join keys. | Either `name` plus `value`, or keyword values. Values may be constants, callables, or `Expr` objects. | Returns a new `RowSet` with derived columns; derived values must be JSON-compatible. | local | installed SDK: `verbiflow/rowset.py:191` |
| `RowSet.group_by(*keys).agg(**aggregations)` | Aggregate local rows for counts, grouping, and coverage checks. | `keys` (`str`, one or more): group columns. `aggregations`: `Aggregation` objects such as `vf.count()`. | Returns grouped aggregate `RowSet`. `group_by()` with no keys raises. | local | installed SDK: `verbiflow/rowset.py:215` |
| `RowSet.to_csv_text()` | Produce CSV text for small local previews or compatibility handoff. | No inputs. | Returns CSV text with columns ordered by first appearance across rows. Preserve the original `RowSet` for further typed operations. | local | installed SDK: `verbiflow/rowset.py:220` |
| `RowSet.to_csv(path)` | Write local rows to an intentional absolute CSV path. | `path` (`str \| Path`, required): absolute local output path. | Creates parent directories, writes CSV, and returns `Path`. Preserve the returned path for reports or file handoff. | local_file_write | installed SDK: `verbiflow/rowset.py:223` |
| `RowSet.ai_analyze(...)` | Start row analysis (qualify/classify/enrich) directly from a `RowSet` without manually converting to lists. | Same kwargs as `vf.rows.ai_analyze(...)`: `mode`, `tier`, `fields`, plus mode-specific args (`rubric`, `labels`, `prompt`+`output_schema`). Requires `client=...`. | Returns `list[PendingJob]`. Preserve `job_id`s immediately and `artifact_id`s after wait. | artifact_job | installed SDK: `verbiflow/rowset.py:231` |
| `vf.col(name)` / `vf.email_domain(column)` / `vf.is_personal_domain(column_or_domain)` / `vf.count()` | Build local `RowSet` expressions and aggregations for deterministic transforms. | Expression helper inputs are column names or expressions; `vf.count()` takes no inputs. | Returns `Expr` or `Aggregation` handles used by `RowSet` methods. Preserve final row fields, not helper objects, across workflow boundaries. | local | installed SDK: `verbiflow/rowset.py:302` |

SDK row/artifact functions:

| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.artifacts.join_csv` | Use when local CSV files need a deterministic join before upload or staging. | <strong>Required</strong>: `left_csv_path` (str \| Path; required): Absolute path to the left local CSV file.; `right_csv_path` (str \| Path; required): Absolute path to the right local CSV file.<br><strong>Optional</strong>: `how` (inner \| left \| right \| full; optional; default `'inner'`): Join mode: inner keeps matches only; left/right preserve one side; full preserves both sides.; `left_on` (str; optional; default `'company_domain'`): Join key column in the left artifact or CSV.; `normalize_key` (domain \| lowercase \| none; optional; default `'domain'`): Key normalization strategy before joining: domain, lowercase, or none.; `right_on` (str; optional; default `'company_domain'`): Join key column in the right artifact or CSV. | `RowSet`: local in-memory rows. It is safe to transform locally or pass into SDK Bridge calls.<br><strong>Inspect</strong>: Returns a `RowSet` whose rows include prefixed `left_*`/`right_*` columns plus `join_key`.<br><strong>Common next calls</strong>: `vf.rows.ai_analyze`, `vf.sequences.upload_csv_to_source` | local | installed SDK: `verbiflow/artifacts.py:458-501` |
| `vf.rows.ai_analyze` | One LLM primitive for row-level filter/label/enrich. `mode='qualify'` runs a pass/fail rubric, `mode='classify'` assigns one of N named labels, `mode='enrich'` generates fields from a JSON schema. `tier` selects the quality and compute profile; customer pricing remains server-authoritative. All modes share `rows.ai_enrich`; inspect its live tier terms with `vf.capabilities.get('rows.ai_enrich')` before execution. Callers never pass a model id directly — tier picks the model: low→MiniMax M2.7, medium→GLM 5.1, high→GLM 5.2 (all Fireworks serverless). `fields=` is validated against actual row keys upfront. Any input size — the SDK chunks at 500 rows and fans out chunks concurrently. Returns `list[PendingJob]`. | <strong>Required</strong>: `mode` (qualify \| classify \| enrich; required): What kind of LLM job to run per row. 'qualify' = pass/fail rubric filter. 'classify' = assign one of N user-named labels. 'enrich' = generate user-defined output fields from a JSON schema.; `rows_or_artifact` (Iterable[Mapping[str, object]] \| str \| Path \| Artifact \| RowSet; required): Rows, RowSet, artifact id, Artifact, or absolute CSV path. Any size — the SDK chunks internally.<br><strong>Optional</strong>: `concurrency` (int; optional; default `6`): Max chunks running in parallel when input exceeds one chunk.; `entity_kind` (contact \| company \| row; optional; default `'contact'`): mode='qualify' only: what each row represents.; `fields` (Sequence[str] \| None; optional; default `None`): Columns the LLM should read from each row. Validated upfront against the input row keys — passing a name that does not exist raises ValidationError before any LLM is invoked (no more silent 'wrong field name passed 396/397 rows').; `labels` (Sequence[Mapping[str, str]] \| None; optional; default `None`): mode='classify' only: candidate labels as [{name, rubric}, ...].; `output_column` (str; optional; default `'label'`): mode='classify' only: column written with the winning label.; `output_schema` (Mapping[str, object] \| None; optional; default `None`): mode='enrich' only: JSON Schema whose properties become the new columns. For any material LLM judgment, score, or recommendation, include required evidence-grounded rationale and confidence fields; see row_operations.md.; `output_scores_column` (str \| None; optional; default `None`): mode='classify' only: optional column written with the full per-label score map.; `prompt` (str \| None; optional; default `None`): mode='enrich' only: row-level prompt that produces the schema fields.; `rubric` (str \| None; optional; default `None`): mode='qualify' only: natural-language pass/fail criterion.; `tier` (low \| medium \| high; optional; default `'medium'`): Quality and compute dial: low is fastest, medium is the default, and high uses the strongest model pool and maximum reasoning effort. Pricing is server-authoritative; before execution inspect vf.capabilities.get('rows.ai_enrich'). The three ai_analyze modes share that hosted execution and its tier variants.; `wait` (bool; optional; default `False`): When true, wait for all chunk jobs before returning. | `list[PendingJob]`: one handle per started/waited chunk. Use `vf.jobs.wait_many(...)`, then wrap each item with `vf.jobs.result(...)` for rows/artifacts.<br><strong>Common next calls</strong>: `vf.jobs.wait_many`, `vf.jobs.get`, `vf.artifacts.get`, `vf.jobs.export_csv` | artifact_job | installed SDK: `verbiflow/rows.py:105-331` |
| `vf.rows.check_variable_coverage` | Use before staging CSV rows into a sequence to prove required variables are mapped. | <strong>Required</strong>: none<br><strong>Optional</strong>: `column_mapping` (Mapping[str, str] \| None; optional; default `None`): Optional target variable -> source column mapping.; `csv_path` (str \| Path \| None; optional; default `None`): Absolute CSV path to inspect. Pass either csv_path or rows.; `required_variables` (Iterable[str] \| None; optional; default `None`): Alternative name for variables, useful in workflow/play examples.; `rows` (Iterable[Mapping[str, object]] \| RowSet \| None; optional; default `None`): In-memory row dictionaries to inspect. Pass either rows or csv_path.; `sample_size` (int; optional; default `20`): Rows to sample for non-empty example values.; `variables` (Iterable[str] \| None; optional; default `None`): Template variables that must be covered by columns.<br><strong>Note</strong>: Pass exactly one row source: `rows=...` or `csv_path=...`. | `dict` with missing variables, mapped columns, sample coverage, and row counts.<br><strong>Inspect</strong>: Inspect `covered`, `missing_variables`, `checks`, `columns`, and `row_count` before handoff. | local | installed SDK: `verbiflow/rows.py:925-983` |
| `vf.rows.classify` | Use for local row transforms, CSV helpers, joins, validation, and row qualification. | <strong>Required</strong>: `labels` (Sequence[Mapping[str, str]]; required): Candidate labels as {name, rubric}. ScaleDown matches each uncached row against these fixed labels; use rows.ai_analyze when you also need a generated reason and confidence.; `rows_or_artifact` (Iterable[Mapping[str, object]] \| str \| Path \| Artifact \| RowSet; required): Rows, RowSet, artifact id, Artifact, or absolute CSV path.<br><strong>Optional</strong>: `max_rows` (int; optional; default `500`): Maximum rows to upload, process, or return for this call.; `output_column` (str; optional; default `'label'`): Column name written with the winning label per row.; `output_scores_column` (str \| None; optional; default `None`): Optional column containing the full per-label score map.; `start_row` (int; optional; default `1`): One-based first row to upload/process from a CSV or artifact.; `text_columns` (list[str] \| None; optional; default `None`): Columns to concatenate as input text for each row. Omit to auto-detect the longest text-shaped column.; `total_rows` (int \| None; optional; default `None`): Total source rows. Required only for a pre-sliced chunk with start_row > 1; normal SDK inputs are counted automatically.; `wait` (bool \| int; optional; default `False`): 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.jobs.get`, `vf.jobs.stream_rows`, `vf.artifacts.get`, `vf.jobs.export_csv` | artifact_job | installed SDK: `verbiflow/rows.py:412-541` |
| `vf.rows.extract` | Use to pull structured fields out of each row's free-form text/HTML against an explicit schema (field name + description + top_n). Right for cases like 'pull city/state/country from a location string', 'pull street address from a contact-page snippet', or 'pull pricing tier from a feature list'. Prefer over `rows.ai_analyze(mode='enrich', ...)` when the desired output is a small set of named string fields, not a JSON-schema object — it is cheaper and the schema is simpler to author. | <strong>Required</strong>: `rows_or_artifact` (Iterable[Mapping[str, object]] \| str \| Path \| Artifact \| RowSet; required): Rows, RowSet, artifact id, Artifact, or absolute CSV path.; `schema` (Mapping[str, Mapping[str, object]]; required): Field schema. Keys are output column names; values are {description, top_n=1}. Use top_n > 1 when the field can naturally occur multiple times (e.g. multiple addresses).<br><strong>Optional</strong>: `max_rows` (int; optional; default `500`): Maximum rows to upload, process, or return for this call.; `start_row` (int; optional; default `1`): One-based first row to upload/process from a CSV or artifact.; `text_columns` (list[str] \| None; optional; default `None`): Columns to concatenate as input text for each row. Omit to auto-detect the longest text-shaped column.; `total_rows` (int \| None; optional; default `None`): Total rows in the source dataset. Required only when you pass a pre-sliced chunk with `start_row > 1`; the SDK auto-discovers this for Iterable, RowSet, Path, and Artifact inputs.; `wait` (bool \| int; optional; default `False`): If false, return immediately with a job/status handle; if true or a timeout integer, poll until completion or timeout. | `PendingJob` whose output artifact adds one column per `schema` field (best extracted value per row, empty string when not found) and `extraction_error` for rows that failed.<br><strong>Common next calls</strong>: `vf.jobs.get`, `vf.jobs.stream_rows`, `vf.artifacts.get`, `vf.jobs.export_csv` | artifact_job | installed SDK: `verbiflow/rows.py:543-660` |
| `vf.rows.infer_column_mapping` | Use when a CSV/source file needs a first-pass mapping into sequence variables. | <strong>Required</strong>: `csv_path` (str \| Path; required): Absolute local filesystem path.; `variables` (Iterable[str]; required): Variables parameter for `rows.infer_column_mapping`.<br><strong>Optional</strong>: none | `dict` mapping requested variables to inferred CSV column names and confidence metadata.<br><strong>Inspect</strong>: Inspect `mapping` and `missing_variables`; it is a deterministic guess, not approval to stage. | local | installed SDK: `verbiflow/rows.py:1050-1067` |
| `vf.rows.join_csv` | Use for fast deterministic local CSV joins without Bridge/provider side effects. | <strong>Required</strong>: `left_csv_path` (str \| Path; required): Absolute path to the left local CSV file.; `right_csv_path` (str \| Path; required): Absolute path to the right local CSV file.<br><strong>Optional</strong>: `how` (inner \| left \| right \| full; optional; default `'inner'`): Join mode: inner keeps matches only; left/right preserve one side; full preserves both sides.; `left_on` (str; optional; default `'company_domain'`): Join key column in the left CSV.; `normalize_key` (domain \| lowercase \| none; optional; default `'domain'`): Key normalization strategy before joining.; `right_on` (str; optional; default `'company_domain'`): Join key column in the right CSV. | `RowSet`: local in-memory rows. It is safe to transform locally or pass into SDK Bridge calls.<br><strong>Inspect</strong>: Returns a `RowSet` whose rows include prefixed `left_*`/`right_*` columns plus `join_key`. | local | installed SDK: `verbiflow/rows.py:985-1048` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

- Row inputs are dictionaries or `RowSet` records. Keep scalar values for identifiers and reportable columns.
- Preserve canonical identifiers such as `email`, CRM object ids, LinkedIn URLs, company domains, artifact ids, source run ids, and workflow run ids.
- `check_variable_coverage(...)` returns `covered`, `missing_variables`, `checks`, `columns`, and `row_count` plus either `source` or `csv_path`.
- `infer_column_mapping(...)` returns `csv_path`, `mapping`, and `missing_variables`.
- `join_csv(...)` requires absolute CSV paths and returns a `RowSet` with prefixed `left_*`/`right_*` fields plus `join_key`.
- Row qualification requires a non-empty rubric. Include required fit signals, disqualifiers, tie-breakers, `entity_kind`, and visible columns when relevant. One job chunk is capped at 500 rows.
- Custom row annotation requires a JSON object schema with non-empty properties, safe column names, and no reserved identity/source fields such as `email`, `company_domain`, `contact_linkedin_url`, or CRM ids. One job chunk is capped at 500 rows.
- `vf.rows.ai_analyze(...)` returns one `PendingJob` per internal chunk (500-row cap each). For artifact ids passed as plain `gda_...` strings, the SDK materializes up to the row job cap; pass an `Artifact` with `row_count` or page explicitly when completeness matters.
- **Long-job rule.** `ai_analyze` defaults to `wait=False` and returns chunk handles immediately — recommended for large jobs. Pass `wait=True` only for small jobs (<3 chunks); a 10k-row run would block the foreground. For anything bigger: capture the `list[PendingJob]` and either (a) await `vf.jobs.wait_many(jobs, concurrency=...)` from a `Bash run_in_background=true` task, or (b) poll with `vf.jobs.get(...)` while the conversation stays responsive. Do not block the foreground on a job longer than ~30s; see `parallelism.md` for the fan-out pattern.
- Choose `tier="low"` for straightforward one-of-N classification with explicit per-label rubrics, `tier="medium"` for open rubrics and structured enrichment, and `tier="high"` for nuanced judgments where weaker models have been observed to drift. Tier selects quality and compute, not a price stored in the SDK; inspect live capability pricing before execution.

## Examples

Local deterministic transform:

```python
import verbiflow as vf

rows = vf.from_records([
    {"email": "ada@example.com", "role": "VP Engineering"},
    {"email": "founder@gmail.com", "role": "Founder"},
])
companies = (
    rows
    .derive("company_domain", vf.email_domain("email"))
    .filter(~vf.is_personal_domain("company_domain"))
    .group_by("company_domain")
    .agg(user_count=vf.count())
)
print("company preview", companies.to_list())
```

Coverage check before handoff:

```python
import verbiflow as vf

coverage = vf.rows.check_variable_coverage(
    rows=[{"first_name": "Ada", "company_name": "Example", "email": "ada@example.com"}],
    required_variables=["first_name", "company_name", "email"],
)
print(coverage["covered"], coverage["missing_variables"])
```

Row qualification job (pass/fail filter):

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
jobs = vf.rows.ai_analyze(
    [{"name": "Ada Lovelace", "title": "VP Engineering", "company_domain": "example.com"}],
    mode="qualify",
    tier="medium",
    fields=["name", "title", "company_domain"],
    rubric="Pass only senior product or engineering decision makers at relevant B2B software companies.",
)
results = vf.jobs.wait_many(jobs)
for r in results:
    print(r.job_id, r.artifact_id)
```

Custom row annotation (enrich mode — prompt + JSON schema):

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
jobs = vf.rows.ai_analyze(
    [{"company_name": "Example", "launch_note": "Released a new analytics product."}],
    mode="enrich",
    tier="medium",
    fields=["company_name", "launch_note"],
    prompt="Write a short reason this company may care about an analytics infrastructure product.",
    output_schema={
        "type": "object",
        "properties": {"relevance_reason": {"type": "string"}},
        "required": ["relevance_reason"],
    },
)
results = vf.jobs.wait_many(jobs)
```

Fixed-label classify (ICP bucket per row, cheaper than open-rubric qualify):

```python
jobs = vf.rows.ai_analyze(
    rows,
    mode="classify",
    tier="low",
    fields=["company_name", "company_description"],
    labels=[
        {"name": "in_icp",     "rubric": "US SaaS, 10-200 employees, B2B."},
        {"name": "out_of_icp", "rubric": "Anything outside the above."},
    ],
    output_column="icp_bucket",
)
results = vf.jobs.wait_many(jobs)
```

Large input (any size — SDK auto-chunks at 500 rows with parallel fan-out):

```python
# 12k-row qualification across all chunks, awaited concurrently.
jobs = vf.rows.ai_analyze(
    huge_csv_path,
    mode="qualify",
    tier="medium",
    fields=["headline", "scraped_about", "company_industry"],
    rubric="Pass only founders/CEOs at seed-to-Series-A B2B SaaS companies.",
    concurrency=8,
)
print(f"chunks queued: {len(jobs)}")
results = vf.jobs.wait_many(jobs, concurrency=8)
```

Reviewable custom scoring:

```python
jobs = vf.rows.ai_analyze(
    rows,
    mode="enrich",
    tier="medium",
    fields=["company_description", "website_text"],
    prompt=(
        "Score document-workflow fit from the supplied evidence. Return a concise "
        "auditable rationale, not hidden chain-of-thought."
    ),
    output_schema={
        "type": "object",
        "properties": {
            "fit_score": {"type": "integer", "minimum": 0, "maximum": 100},
            "fit_rationale": {"type": "string"},
            "fit_confidence": {
                "type": "string",
                "enum": ["high", "medium", "low"],
            },
        },
        "required": ["fit_score", "fit_rationale", "fit_confidence"],
    },
)
```

Geo containment filter (before sequence staging):

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
job = vf.rows.geo_classify(
    rows,
    target="SF Bay Area",
    text_columns=["location"],
    output_column="in_sf_bay",
)
result = vf.jobs.result(job).wait(require_success=True)
all_rows = result.rows(limit=500)
bay_only = [r for r in all_rows if r["in_sf_bay"] == "true"]
print(f"{len(bay_only)} kept, {len(all_rows) - len(bay_only)} dropped")
```

Schema-based extract (pull city/state/country from a freeform string):

```python
job = vf.rows.extract(
    rows,
    schema={
        "city":    {"description": "City name; empty if not stated."},
        "state":   {"description": "US state or province; empty if not stated."},
        "country": {"description": "Country; empty if not stated."},
    },
    text_columns=["raw_location"],
)
```

## Verification

- For local transforms, print row counts, column names, and `to_list()` samples.
- For coverage/mapping, confirm `covered` is true or inspect `missing_variables` before handoff.
- For row jobs, print `job_id`, `artifact_id`, and a sample of output rows from `vf.jobs.result(job).rows(...)`.
- Verify that every material LLM judgment has a non-empty rationale and a valid confidence value; treat missing review metadata as incomplete output, not a cosmetic report issue.
- Inspect backend-reported issue/error columns, if present, before reports, personalization, or sequence staging.
- Switch to `artifacts_jobs.md` for paging/export/staging and to `sequences.md` before any sequence mutation.

## Troubleshooting

| Symptom | First Check |
| --- | --- |
| `RowSet` sample code fails with `.records` | `RowSet` exposes `to_list()`, `sample(...)`, and `take(...)`; use `sample(...).to_list()` for plain dictionaries. |
| CSV path rejected | Use an absolute filesystem path. |
| Coverage reports missing variables | Inspect `columns`, `checks`, and any `column_mapping`; do not stage until required variables are covered or explicitly waived. |
| Row job has no `artifact_id` yet | Use `vf.jobs.result(job).wait(require_success=True)`, then inspect `result.artifact_id`. |
| Custom annotation rejects schema | Output schema properties must be valid new column names and must not reuse reserved identity/source fields. |
| Large input feels slow or partial | `vf.rows.ai_analyze(...)` already chunks any input at 500 rows; pass `concurrency=` higher and inspect each returned `PendingJob` via `artifacts_jobs.md`. |

## Related References

| If You Are Doing This | Read Next |
| --- | --- |
| Waiting on row jobs, paging artifacts, exporting CSVs, or staging artifacts | `artifacts_jobs.md` |
| Resolving canonical emails, domains, profiles, phones, or company facts | `enrichment.md` |
| Geo containment (continent / country / state / metro / city) over row text | `geo.md` |
| Acquiring rows from CRM, source jobs, LinkedIn, web, Clay, or Crunchbase | `audience_discovery.md`, `crm.md`, `linkedin.md`, `web.md`, `host_browser.md`, `clay.md`, `crunchbase.md` |
| Operating a generated workflow root | `workflow_operations.md`, `workflow_sources.md` |
| Rendering a Play UI/report from rows or artifacts | `workflow_reports.md` |
| Staging or activating sequence rows | `sequence_authoring.md`, `sequences.md` |
