# Artifacts And Jobs

<!-- 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-41 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 42-53 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 54-68 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 69-77 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 78-90 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 91-100 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 101-138 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 139-151 |
| [Examples](#examples) | Representative usage patterns and code. | 152-215 |
| [Verification](#verification) | Evidence required before reporting success. | 216-225 |
| [`preview_rows` Is A Preview. `final_result` Is The Full CSV.](#previewrows-is-a-preview-finalresult-is-the-full-csv) | `PendingJob` has two row surfaces; the names tell you which is which: | 226-259 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 260-271 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 272-282 |
<!-- END GENERATED: guide-map -->

## Purpose

Artifacts And Jobs owns the lifecycle of backend job handles and durable data artifacts. Use it to preserve `job_id` and `artifact_id`, wait or refresh job state, cancel queued/running jobs, stream committed batches or rows, page artifacts, inspect artifact schema/provenance, export artifacts to local CSV, upload files, and join durable artifacts.

This is a primitive reference, not the workflow operating model. Workflows may call these primitives, but workflow checkpointing, local row transforms, final Play UI/report design, and sequence activation belong to narrower references.

## Use This When

- An SDK call returned a `PendingJob`, a dict with `job_id`, or a raw `job_id` string.
- You need to wait, refresh, stream, or inspect a backend job without rerunning provider work.
- You need to cancel a broad queued/running provider job instead of letting it continue.
- You have an `artifact_id` and need to page rows, inspect schema/provenance, list artifacts, export CSV, or join durable artifacts.
- You need to upload a local file through the Bridge file helper.
- You need a one-off inspection or debugging path for provider/output rows outside a durable workflow run.

## Do Not Use This For

| Task | Owning Reference |
| --- | --- |
| Generated workflow roots, replay, repair, local SQLite run state, report, sync, or activation boundaries | `workflow_operations.md`; `workflow_schedules.md` owns sync |
| Recurring source windows, source checkpoints, object memory, and restart-safe acquisition | `workflow_sources.md` |
| Deterministic row transforms, local CSV joins, variable coverage, row qualification, or custom row annotation | `row_operations.md` |
| Canonical email, domain, profile, phone, company, or identity enrichment | `enrichment.md` |
| Final Play UI/report design and local Svelte report output | `workflow_reports.md` |
| Sequence source authoring, staging, publish, send, audience repair, or activation approvals | `sequence_authoring.md`, `sequences.md` |
| Provider acquisition itself, such as CRM/source/web/LinkedIn/host calls | The provider-specific reference that started the job |

## Mental Model

| Concept | Meaning |
| --- | --- |
| `PendingJob` | Lightweight job snapshot returned by many SDK calls. It has `job_id`, status/progress fields, optional preview rows, and optional `artifact_id`. |
| `JobResult` | Convenience handle from `vf.jobs.result(job)`. It owns `.get()`, `.wait()`, `.rows()`, `.iter_rows()`, `.to_rowset()`, `.to_report()`, and `.stage_to_sequence()`. |
| `job_id` | The durable backend job identity. Preserve it immediately; do not rerun provider work because context was lost. |
| `artifact_id` | Durable rowset identity. It may appear only after a job starts committing rows or finishes. |
| `preview_rows` | Small rows embedded in a `PendingJob` for status display. Never the full result — use `final_result` (CSV path) or `rows()` instead. |
| `final_result` | Path to the exported CSV of the full result. Lazy-populated on `JobResult.final_result` after `wait()`; `artifact_id` remains the canonical durable data identity. |
Local execution writes `final_result` to a temporary CSV. Copy or export it to
an intentional durable project path before handoff when it must survive cleanup.
| Artifact rows | Durable rows fetched through `vf.artifacts.get(...)`, `JobResult.rows(...)`, or `JobResult.iter_rows(...)`. |
| Streaming batches/rows | Read committed artifact batches while the same already-started backend job is still running. Streaming does not create workflow checkpointing by itself. |

## Preflight

- Ensure the active SDK workspace is correct before reading or waiting on workspace-scoped jobs/artifacts.
- Print and preserve `job_id` immediately after any start call.
- If an SDK call returns a dict or `PendingJob`, normalize it with `vf.jobs.result(job)` before wait/page/report/stage follow-up.
- Know whether you need a quick preview, a blocking wait, a streaming one-off inspection, or a durable workflow collection path.
- Use bounded `timeout`, `page_size`, and `max_rows` when output size is unknown.
- Do not call provider/source/enrichment start functions again until you have checked whether the existing job or artifact already contains the needed rows.

## Risk And Side Effects

| Operation | Side Effect | Rule |
| --- | --- | --- |
| `vf.jobs.get(...)`, `vf.jobs.wait(...)`, `vf.jobs.result(...).get/wait/rows/iter_rows`, `vf.jobs.events(...)`, `vf.jobs.stream_*`, `vf.artifacts.list/get/inspect(...)` | Platform read | Safe after workspace verification; print IDs and counts. |
| `vf.jobs.cancel(...)` | Platform write / job cancellation | Use only for the exact `job_id`; print status, phase, progress, and error message after cancellation. |
| `vf.artifacts.export_csv(...)` | Local file write | Use an absolute or intentional output path and print returned `output_path` plus `row_count`. |
| `vf.jobs.export_csv(...)` with `output_path` | Local file write | Use an absolute or intentional output path. It still returns CSV text, not row count. |
| `vf.artifacts.join(...)` | Artifact job/write | Verify join keys and row counts; inspect the joined artifact before downstream use. |
| `vf.files.upload(...)` | Platform upload | Use only for intentional file handoff; print returned file id/path. |
| `JobResult.to_report(...)` | Local report write | Use for a quick artifact report, then route report design/customization to `workflow_reports.md`. |
| `JobResult.stage_to_sequence(...)` | Sequence staging | Route to `sequences.md` first; staging is not publish/send and still requires review. |

## Canonical Operating Pattern

1. Capture the returned job value from the start call and immediately print `vf.jobs.result(job).job_id`.
2. For quick status, call `result.get(...)`; for terminal output, call `result.wait(require_success=True, timeout=...)`.
3. If `result.artifact_id` is present, page rows with `result.rows(...)`, `result.iter_rows(...)`, or `vf.artifacts.get(...)`.
4. Inspect artifact schema/provenance with `vf.artifacts.inspect(...)` before joins, exports, reports, or sequence handoff. Use `columns=[...]` and the default `max_field_chars` when artifacts may contain long markdown/html/text fields.
5. For one-off live inspection, use `vf.jobs.stream_batches(...)` or `vf.jobs.stream_rows(...)` with a bounded timeout.
6. For recurring/restart-safe workflow acquisition, stop here and use `ctx.collect_artifact_job(...)` from `workflow_sources.md` instead of raw streams.
7. Export, report, join, or stage only after printing IDs, counts, and sample rows.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
SDK job, artifact, and file functions:

| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.jobs.cancel` | Cancel a queued or running Bridge-backed job and return its terminal status. | <strong>Required</strong>: `job_id` (str; required): Bridge job id returned by a source, enrichment, qualification, personalization, or web start call.<br><strong>Optional</strong>: `reason` (str \| None; optional; default `None`): Optional operator-visible cancellation reason. | `PendingJob`: `job_id`, optional `artifact_id`, `status`, `finished`, `failed`, `error_message`, progress, preview rows, and summary. Prefer `vf.jobs.result(pending)` for wait/page/report/stage follow-up.<br><strong>Common next calls</strong>: `vf.jobs.get`, `vf.jobs.events` | platform_write | installed SDK: `verbiflow/jobs.py:660-694` |
| `vf.jobs.events` | Use for live progress/log display when a long Bridge job is running and the agent needs status or artifact.batch_committed events without waiting for completion. | <strong>Required</strong>: `job_id` (str; required): Bridge job id returned by a source, enrichment, qualification, personalization, or web start call.<br><strong>Optional</strong>: `after_seq` (int; optional; default `0`): Workflow event sequence number to resume after.<br><strong>Note</strong>: `after_seq` resumes after a previously seen event sequence. | `Iterator[dict]` or async iterator of live job progress events with run status, emitted events, last sequence, and terminal flag.<br><strong>Inspect</strong>: Event pages include run status, emitted events, `last_seq`, and terminal state.<br><strong>Common next calls</strong>: `vf.jobs.stream_rows`, `vf.jobs.wait`, `vf.artifacts.get` | read | installed SDK: `verbiflow/jobs.py:696-730` |
| `vf.jobs.export_csv` | Download a completed composable job result CSV. | <strong>Required</strong>: `job_id` (str; required): Completed composable job id to export as CSV.<br><strong>Optional</strong>: `output_path` (str \| Path \| None; optional; default `None`): Optional absolute local path to write the CSV text.<br><strong>Note</strong>: `output_path`, when provided, must be absolute. | `str`: CSV text, or writes the same CSV to `output_path` when provided.<br><strong>Inspect</strong>: Returns CSV text. When `output_path` is provided it also writes that text locally; it does not return row count. | local_file_write | installed SDK: `verbiflow/jobs.py:1095-1128` |
| `vf.jobs.get` | Get status and preview rows for a composable Growth job. | <strong>Required</strong>: `job_id` (str; required): Bridge job id returned by a source, enrichment, qualification, personalization, or web start call.<br><strong>Optional</strong>: `limit` (int; optional; default `20`): Preview row limit to request from job status. This is not artifact pagination.; `offset` (int; optional; default `0`): Preview row offset to request from job status. This is not artifact pagination.<br><strong>Note</strong>: `offset` and `limit` request preview rows from job status; use artifact paging for complete output. | `PendingJob`: `job_id`, optional `artifact_id`, `status`, `finished`, `failed`, `error_message`, progress, preview rows, and summary. Prefer `vf.jobs.result(pending)` for wait/page/report/stage follow-up.<br><strong>Inspect</strong>: `offset`/`limit` apply to preview rows in job status, not durable artifact pagination.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.jobs.export_csv` | read | installed SDK: `verbiflow/jobs.py:612-658` |
| `vf.jobs.result` | Use immediately after any SDK start call that returns a job id or PendingJob so waiting, paging, reporting, and staging are one explicit handle instead of re-derived control flow. | <strong>Required</strong>: `job_or_id` (JobInput; required): Job id, PendingJob, or mapping with a job_id returned by an SDK start call.<br><strong>Optional</strong>: none | `JobResult` handle with `.wait()`, `.artifact()`, `.rows()`, `.iter_rows()`, `.to_rowset()`, `.to_report()`, and `.stage_to_sequence()`.<br><strong>Inspect</strong>: Preserve `job_id`; call `.wait(...)`, then inspect `.artifact_id`, `.job`, and `.rows(...)`.<br><strong>Common next calls</strong>: `vf.jobs.wait`, `vf.artifacts.get`, `vf.artifacts.inspect` | read | installed SDK: `verbiflow/jobs.py:939-975` |
| `vf.jobs.stream_batches` | Use for row-heavy artifact jobs when downstream workflow steps should react to committed batches instead of waiting for the same already-started artifact job to finish. | <strong>Required</strong>: `job` (str \| PendingJob; required): Job id or PendingJob returned by an SDK start call.<br><strong>Optional</strong>: `after_batch_id` (str \| None; optional; default `None`): Optional committed batch id to resume after. Use this with a durable workflow cursor when a prior stream already materialized earlier batches.; `allow_partial` (bool; optional; default `False`): When false, raise if the stream times out before the job finishes.; `poll_interval` (float; optional; default `1.0`): Seconds between batch polling requests when no new batch is available.; `timeout` (float; optional; default `300.0`): Maximum seconds to poll for committed artifact batches.<br><strong>Note</strong>: Streams committed batches from one already-started job. It is not restart-safe workflow collection by itself. | `Iterator[dict]` or async iterator of committed artifact batch metadata while the backend job is still running.<br><strong>Common next calls</strong>: `vf.jobs.stream_rows`, `vf.jobs.wait`, `vf.artifacts.inspect` | read | installed SDK: `verbiflow/jobs.py:732-823` |
| `vf.jobs.stream_rows` | Use for pipelined local workflows that can process committed artifact rows while the same already-started artifact job commits later batches. | <strong>Required</strong>: `job` (str \| PendingJob; required): Job id or PendingJob returned by an SDK start call.<br><strong>Optional</strong>: `after_batch_id` (str \| None; optional; default `None`): Optional committed batch id to resume after. Use this with a durable workflow cursor when earlier artifact batches have already been processed.; `allow_partial` (bool; optional; default `False`): When false, raise if the stream times out before the job finishes.; `page_size` (int; optional; default `200`): Maximum rows fetched per polling request.; `poll_interval` (float; optional; default `1.0`): Seconds between row polling requests when no new rows are available.; `timeout` (float; optional; default `300.0`): Maximum seconds to poll for committed artifact rows.<br><strong>Note</strong>: Streams rows from one already-started job. It is not a workflow checkpoint or provider freshness primitive. | `Iterator[dict]` or async iterator of committed artifact rows while the backend job is still running.<br><strong>Common next calls</strong>: `vf.jobs.wait`, `vf.artifacts.inspect`, `vf.reports.render` | read | installed SDK: `verbiflow/jobs.py:825-937` |
| `vf.jobs.wait` | Poll a Bridge job until it finishes or the timeout is reached. | <strong>Required</strong>: `job_id` (str; required): Bridge job id returned by a source, enrichment, qualification, personalization, or web start call.<br><strong>Optional</strong>: `limit` (int; optional; default `20`): Preview row limit to request while polling.; `offset` (int; optional; default `0`): Preview row offset to request while polling.; `poll_interval` (float; optional; default `2.0`): Seconds between status polls.; `timeout` (float; optional; default `300.0`): Maximum seconds to wait before returning the latest observed job state.<br><strong>Note</strong>: `offset` and `limit` request preview rows during polling; this namespace call returns latest state rather than raising on failed/unfinished jobs. | `PendingJob`: `job_id`, optional `artifact_id`, `status`, `finished`, `failed`, `error_message`, progress, preview rows, and summary. Prefer `vf.jobs.result(pending)` for wait/page/report/stage follow-up.<br><strong>Inspect</strong>: Namespace `wait` returns the latest job state and does not raise on failed/unfinished jobs; `JobResult.wait(require_success=True)` is the fail-fast path.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.jobs.export_csv` | read | installed SDK: `verbiflow/jobs.py:977-1038` |
| `vf.jobs.wait_many` | Poll multiple Bridge jobs concurrently until each finishes or times out. | <strong>Required</strong>: `jobs` (Sequence[str \| PendingJob]; required): Sequence of job ids or PendingJob handles returned by SDK start calls.<br><strong>Optional</strong>: `concurrency` (int; optional; default `8`): Maximum jobs polled concurrently.; `poll_interval` (float; optional; default `2.0`): Seconds between status polls for each job.; `timeout` (float; optional; default `300.0`): Maximum seconds to wait for each job.<br><strong>Note</strong>: `concurrency` is clamped to at least 1; inspect each returned job for success/failure. | `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>Inspect</strong>: Returns latest state for each job in input order; inspect every returned `PendingJob` before downstream use.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.jobs.export_csv` | read | installed SDK: `verbiflow/jobs.py:1040-1093` |
| `vf.artifacts.export_csv` | Use when a durable artifact must become a local CSV deliverable or local workflow input. | <strong>Required</strong>: `artifact_id` (str \| Artifact; required): Artifact id to page/export.; `output_path` (str \| Path; required): Absolute CSV output path.<br><strong>Optional</strong>: `max_rows` (int \| None; optional; default `None`): Optional hard row cap for preview exports.; `page_size` (int; optional; default `200`): Rows per artifact page while exporting.<br><strong>Note</strong>: `page_size` is capped internally at 200 rows per artifact page. | `dict` with artifact id, absolute output path, and exported row count.<br><strong>Inspect</strong>: Returns `{artifact_id, output_path, row_count}` after paging and writing the local CSV.<br><strong>Common next calls</strong>: `vf.reports.source.rows`, `vf.sequences.upload_csv_to_source` | local_file_write | installed SDK: `verbiflow/artifacts.py:396-456` |
| `vf.artifacts.get` | Use after a job returns `artifact_id` to page durable rows without rerunning the provider operation. | <strong>Required</strong>: `artifact_id` (str \| Artifact; required): Durable artifact id returned by a previous SDK job or artifact operation.<br><strong>Optional</strong>: `limit` (int; optional; default `20`): Rows in this page. Use next_offset to continue.; `offset` (int; optional; default `0`): Zero-based pagination offset for preview/list reads.; `view` (str \| None; optional; default `None`): Optional artifact view/projection to request from Bridge.<br><strong>Note</strong>: `limit` is page size, not total artifact size; iterate with `next_offset` for complete reads. | `ArtifactPage`: `artifact`, typed `rows`, `records` as plain dictionaries, pagination offsets, and raw payload.<br><strong>Inspect</strong>: Use `records` for plain dictionaries and `next_offset` for pagination.<br><strong>Common next calls</strong>: `vf.rows.ai_analyze`, `vf.artifacts.join` | read | installed SDK: `verbiflow/artifacts.py:247-291` |
| `vf.artifacts.inspect` | Use before joining, exporting, reporting, or staging an artifact to confirm row count, columns, sample records, provenance, warnings, and next safe operations. | <strong>Required</strong>: `artifact_id` (str \| Artifact; required): Durable artifact id to inspect.<br><strong>Optional</strong>: `columns` (Sequence[str] \| None; optional; default `None`): Optional columns to include in schema/sample output.; `max_field_chars` (int; optional; default `500`): Maximum characters returned per string field in inspection samples.; `sample_limit` (int; optional; default `25`): Maximum sample rows used for schema and preview.<br><strong>Note</strong>: `sample_limit` is clamped to `1..200` and samples the first artifact page. | `dict` with artifact metadata, sample rows, inferred schema, provenance/issue keys, warnings, and next actions.<br><strong>Inspect</strong>: Uses a bounded first-page sample; schema is sample-based, not a full-artifact proof.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.artifacts.export_csv`, `vf.jobs.result` | read | installed SDK: `verbiflow/artifacts.py:293-341` |
| `vf.artifacts.join` | Use when two durable artifacts need a backend-side join by domain, email, or another stable key. | <strong>Required</strong>: `left` (str \| Artifact; required): Left artifact id or Artifact object for a durable join.; `right` (str \| Artifact; required): Right artifact id or Artifact object for a durable join.<br><strong>Optional</strong>: `how` (inner \| left \| right \| full; optional; default `'left'`): Join mode: inner keeps matches only; left/right preserve one side; full preserves both sides.; `left_on` (str \| None; optional; default `None`): Join key column in the left artifact or CSV.; `max_rows` (int; optional; default `5000`): Maximum rows to upload, process, or return for this call.; `normalize_key` (str; optional; default `'lowercase'`): Key normalization strategy before joining: domain, lowercase, or none.; `on` (str \| None; optional; default `None`): Shared join key column to use for both left and right sides.; `output_label` (str \| None; optional; default `None`): Human-readable label for the output artifact.; `right_on` (str \| None; optional; default `None`): Join key column in the right artifact or CSV.<br><strong>Note</strong>: Pass either `on` or both `left_on` and `right_on`; otherwise the backend receives empty join keys. | `Artifact`: durable joined artifact. Page it with `vf.artifacts.get(artifact.artifact_id, ...)`.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.rows.ai_analyze` | artifact_job | installed SDK: `verbiflow/artifacts.py:343-394` |
| `vf.artifacts.list` | Use to discover previously created durable rowsets in the active workspace. | <strong>Required</strong>: none<br><strong>Optional</strong>: `capability_id` (str \| None; optional; default `None`): Optional backend capability id filter.; `limit` (int; optional; default `20`): Maximum artifact records to return.; `offset` (int; optional; default `0`): Zero-based pagination offset for artifact list results.; `status` (str \| None; optional; default `None`): Optional artifact status filter.; `tool_kind` (str \| None; optional; default `None`): Optional artifact-producing tool/source kind filter. | `dict` with artifact records, labels, counts, creation/update metadata, and paging fields.<br><strong>Common next calls</strong>: `vf.artifacts.get`, `vf.artifacts.join`, `vf.jobs.export_csv` | read | installed SDK: `verbiflow/artifacts.py:188-245` |
| `vf.files.upload` | Upload a local image/video media file to the Bridge transient upload surface. | <strong>Required</strong>: `path` (str \| Path; required): Absolute local filesystem path.<br><strong>Optional</strong>: `content_type` (str \| None; optional; default `None`): MIME type for the uploaded file; omit to infer/default.<br><strong>Note</strong>: `path` must be an absolute local file path. | `dict` with uploaded file id/path, content type, and backend attachment metadata. | platform_write | installed SDK: `verbiflow/files.py:16-44` |

Client SDK `JobResult` handle methods returned by `vf.jobs.result(...)`:

| Client SDK Surface | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.jobs.result(job_or_id)` | Normalize any SDK job return into an explicit client handle before waiting, paging, reporting, or staging. | `job_or_id` (`str \| PendingJob \| Mapping`): must contain a non-empty `job_id`; mapping can preserve `artifact_id`. | Returns a `JobResult` handle. Preserve `job_id` and any `artifact_id`; it has no `.status()` or `.to_json()`. | read | installed SDK: `verbiflow/jobs.py:939` |
| `JobResult.get(offset=0, limit=20)` | Refresh job status without blocking until completion. | `offset`/`limit` preview controls; these are job-status preview rows, not artifact pagination. | Refreshes and returns latest `PendingJob`; updates `result.job`. Preserve `result.job`, `job_id`, and any `artifact_id`. | read | installed SDK: `verbiflow/jobs.py:217` |
| `JobResult.wait(require_success=True, poll_interval=2.0, timeout=300.0, offset=0, limit=20)` | Poll until terminal state and fail fast by default for unsuccessful jobs. | `require_success` controls fail-fast behavior; `timeout` bounds polling; preview `offset`/`limit` are status rows. | Returns the same `JobResult`. Preserve `job_id`, `artifact_id`, and `result.job`; failed/canceled/error jobs and unfinished timeout raise by default. | read | installed SDK: `verbiflow/jobs.py:221` |
| `JobResult.rows(offset=0, limit=100, wait=False)` | Read bounded rows from a completed artifact-backed job or preview rows for non-artifact jobs. | `offset`/`limit` page artifact records when `artifact_id` exists; otherwise they read bounded preview rows. `wait=True` uses default wait settings. | Returns `list[dict]`. Call `result.wait(timeout=...)` first for long jobs; preserve canonical IDs in returned rows. | read | installed SDK: `verbiflow/jobs.py:250` |
| `JobResult.iter_rows(page_size=200, max_rows=None)` | Iterate complete artifact rows without loading the full artifact into memory. | `page_size` clamps to `1..200`; `max_rows` optionally caps total yielded rows. | Iterates artifact records only; requires `artifact_id` and does not fall back to preview rows. Preserve yielded row IDs and final count externally. | read | installed SDK: `verbiflow/jobs.py:269` |
| `JobResult.to_rowset(page_size=200, max_rows=None, label=None)` | Convert an artifact-backed job result into local `RowSet` for deterministic transforms. | Artifact paging controls plus optional label. | Returns local `RowSet` loaded from artifact rows. Preserve the returned `RowSet` and original `job_id`/`artifact_id`. | read | installed SDK: `verbiflow/jobs.py:293` |
| `JobResult.to_report(output_dir, title=None, max_rows=5000)` | Build a quick local report from artifact rows for inspection. | `output_dir` (`str \| Path`, required): local report directory. `max_rows` caps artifact rows. | Writes a quick local report and returns report render metadata. Preserve the report path; route polished Play UI work to `workflow_reports.md`. | local_file_write | installed SDK: `verbiflow/jobs.py:305` |
| `JobResult.stage_to_sequence(...)` | Stage artifact rows into a sequence source after review. | Requires `sequence_id`, `source_node_id`, optional `ingress_id`, `row_keys`, `column_mapping`, `mapping_source`, `idempotency_key`. | Stages artifact rows to a sequence source. Preserve source/import ids and staged counts; this is a sequence staging mutation and `sequences.md` owns safety. | sequence_staging | installed SDK: `verbiflow/jobs.py:326` |
<!-- END GENERATED: core-sdk-surfaces -->


## Data Contracts

- `PendingJob` exposes `job_id`, optional `artifact_id`, status/progress fields, preview rows, `error_message`, summary, and raw backend payload.
- `JobResult` has no `.status()` and no `.to_json()`. Use `result.get(...)` for a non-blocking refresh, `result.wait(...)` for polling, then read status/progress from `result.job`.
- Artifact pages expose `artifact`, typed `rows`, plain-dict `records`, `offset`, `limit`, and continuation metadata such as `next_offset`. Use `rows` when you need canonical/provenance/issues/source refs; `records` strips those wrappers.
- `vf.jobs.cancel(...)` posts to the generic job cancel surface and returns a `PendingJob`. Cancellation is terminal from the SDK perspective; inspect `status`, `phase`, `failed`, and `error_message`.
- Terminal completion, failure, and cancellation are first-writer-wins. If completion committed while cancellation was arriving, the returned completed job and artifact remain canonical; cancellation does not rewrite them as failed.
- `vf.artifacts.inspect(...)` returns artifact metadata, inferred schema, bounded sample rows, provenance keys, issue keys, warnings, and next actions. It defaults to truncating long string fields; pass `columns=[...]` for focused inspection and `max_field_chars=...` only when more preview text is intentionally needed.
- `vf.artifacts.export_csv(...)` returns `artifact_id`, absolute `output_path`, and exported `row_count`.
- `vf.jobs.export_csv(...)` returns CSV text and optionally writes the same text to `output_path`; it does not return row count.
- Artifact page size is bounded by the SDK. Large reads should iterate pages instead of assuming one preview page is complete.
- Streaming helpers return committed artifact batches/rows from one already-started job. They do not represent provider-level cursor semantics.

## Examples

Wait and inspect a job returned by another SDK call:

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
job = vf.crm.fetch_objects(provider="hubspot", object_type="contacts", limit=100)
result = vf.jobs.result(job)
print("job", result.job_id)
result.wait(require_success=True, timeout=300)
print("artifact", result.artifact_id)
print("preview", result.rows(limit=5))
```

Page and inspect an existing artifact:

```python
import verbiflow as vf

inspection = vf.artifacts.inspect(
    "gda_...",
    sample_limit=25,
    columns=["company_domain", "title", "description"],
)
page = vf.artifacts.get("gda_...", limit=50)
print(inspection["artifact"], inspection["schema"], page.records[:3])
```

Cancel a provider job you no longer want to run:

```python
import verbiflow as vf

job = vf.jobs.cancel("job_...", reason="operator stopped broad search")
print(job.raw.get("job_id"), job.status, job.phase, job.error_message)
```

One-off stream from an already-started job:

```python
import verbiflow as vf

job = vf.crm.fetch_objects(provider="hubspot", object_type="contacts", limit=5000)
for row in vf.jobs.stream_rows(job, page_size=50, timeout=900):
    print(row.get("email"), row.get("company"))
final = vf.jobs.result(job).wait(require_success=True, timeout=900)
print("artifact", final.artifact_id)
```

Export an artifact to a runtime CSV:

```python
import verbiflow as vf

export = vf.artifacts.export_csv(
    "gda_...",
    output_path="/absolute/path/artifact.csv",
    max_rows=5000,
)
print(export["artifact_id"], export["output_path"], export["row_count"])
```

## Verification

- Job path: print `job_id`, final status from `result.job`, `artifact_id`, and a small row sample.
- Cancel path: print `job_id`, `status`, `phase`, progress, and `error_message`; do not assume partial rows are complete.
- Streaming path: print batch/row counts and use a bounded timeout. If the stream times out, treat partial rows as preview only unless `allow_partial=True` was intentional.
- Artifact path: inspect schema/provenance and page enough rows to prove the preview is not mistaken for the full dataset.
- Artifact export path: confirm output path is absolute/intended and row count matches the expected cap or artifact count.
- Job CSV export path: confirm the CSV text length and written path; use artifact export when row count matters.
- Join path: inspect both input artifacts, verify join key coverage, inspect the joined artifact, then route row transforms to `row_operations.md` if needed.

## `preview_rows` Is A Preview. `final_result` Is The Full CSV.

`PendingJob` has two row surfaces; the names tell you which is which:

- `pending.preview_rows` — a small sample (what's committed so far). Use it for status/preview only. **Never count, filter, qualify, or partition over `preview_rows`** — it is a sample, not the data.
- `result.final_result` (on `JobResult`, populated after `wait()`) — path to the exported CSV of the full result. Read this when you need the actual data.
  Local execution writes the lazy export to a temporary CSV. Move or re-export
  it to an intentional project path when it must remain durable.

```python
pending = vf.linkedin.search_people_start(
    url="https://www.linkedin.com/sales/search/people?...",
    max_contacts=50,
    sender_account_id="sa_...",
)
pending.preview_rows           # sample while running — for status only

result = vf.jobs.result(pending).wait()
result.final_result            # temporary local CSV path
# Read with vf.rows.from_csv, pandas, csv module, etc.
```

`final_result` is lazy: it exports the artifact to CSV on first access and caches the path. Returns `None` if the job has no artifact yet (not finished). The backend `artifact_id` is the canonical identity; the CSV is its runtime projection.

For row-shaped reads without going through a CSV, the existing methods still work:

```python
rows = result.rows()              # full list of dicts
for row in result.iter_rows():    # stream
    ...
```

Symptom you fell into the preview trap: `preview_rows` shows 5 rows, downstream count says 0 / 5 / 20 — but the artifact has thousands. The fix is always `result.final_result` (CSV) or `result.rows()` / `result.iter_rows()` (in-memory).

## Troubleshooting

| Symptom | First Check |
| --- | --- |
| Agent tries `result.status()` or `result.to_json()` | Those methods do not exist on `JobResult`; use `result.get(...)`, `result.wait(...)`, and `result.job`. |
| No `artifact_id` on a job | Call `result.wait(...)` for artifact-backed jobs. If still absent after completion, use preview rows or the function-specific export/status path. |
| Preview shows fewer rows than expected | Preview rows are bounded; page the artifact with `vf.artifacts.get(...)` or `result.iter_rows(...)`. See the `.rows` preview rule above. |
| Streaming times out | By default streaming raises on timeout. Pass `allow_partial=True` only for intentional previews, not complete results. |
| A recurring workflow loses rows after interruption | Use `ctx.collect_artifact_job(...)` in `workflow_sources.md`; raw `stream_rows` is not restart-safe workflow checkpointing. |
| Need to stage rows to a sequence | Stop and read `sequences.md`; artifact staging needs mapping review and is not publish/send. |
| Need a polished Play UI | Use `workflow_reports.md`; `JobResult.to_report(...)` is a quick artifact report helper, not the report design guide. |

## Related References

| If You Are Doing This | Read Next |
| --- | --- |
| Recurring source windows, checkpointing, or object memory | `workflow_sources.md` |
| Operating generated workflow roots, replay, repair, or report | `workflow_operations.md` |
| Syncing a Play revision or eligible output | `workflow_schedules.md` |
| Row transforms, local CSV joins, coverage, custom row annotation, or qualification | `row_operations.md` |
| Canonical enrichment fields | `enrichment.md` |
| Final Play UI/report design | `workflow_reports.md` |
| Sequence source staging or activation | `sequences.md`; use `sequence_authoring.md` only when the draft/source node must be created or edited first |
