# Enrichment

<!-- 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-39 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 40-51 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 52-62 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 63-76 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 77-94 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 95-104 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 105-140 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 141-170 |
| [Identity-Sensitive Relationship Evidence](#identity-sensitive-relationship-evidence) | `vf.enrichment.company_research(...)` intentionally omits unverified founder candidates. | 171-188 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 189-237 |
| [Examples](#examples) | Representative usage patterns and code. | 238-291 |
| [Verification](#verification) | Evidence required before reporting success. | 292-306 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 307-318 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 319-328 |
<!-- END GENERATED: guide-map -->

## Purpose

Enrichment resolves canonical facts for rows you already have. Use it after an
audience/source step has produced contacts, accounts, URLs, or partial identity
hints, and before qualification, personalization, reporting, or sequence
staging depends on canonical fields.

This reference owns `vf.enrichment.*`: work email discovery, email validation,
recipient-domain ESP metadata, company profile fields, company LinkedIn URLs,
company-name to domain resolution, person profiles and recent posts, phone
lookup, address discovery, company research, investor portfolio rows, partial
identity resolution, and person-attribute extraction.

## Use This When

- Existing contact rows need `contact_email`, validation fields, or ESP fields.
- Existing account rows need canonical `company_domain`, `company_linkedin_url`,
  profile facts, address fields, or company research.
- Existing LinkedIn profile URLs need person profile fields, recent posts, or
  person attributes for personalization.
- Partial signup/contact records need a likely LinkedIn profile before people
  expansion or email enrichment.
- A workflow needs provider-backed enrichment outputs as durable jobs/artifacts
  that can be inspected, reported, synced, or staged later.

## Do Not Use This For

| Need | Use Instead | Why |
| --- | --- | --- |
| Finding the initial audience universe | `audience_discovery.md`, `crm.md`, `web.md`, `linkedin.md`, `places.md` | Enrichment improves known rows; it should not hide audience acquisition. |
| Deterministic row cleanup, joins, coverage checks, local transforms, or CSV shaping | `row_operations.md`, `artifacts_jobs.md` | These are cheaper local or artifact operations and should not spend provider credits. |
| Extracting arbitrary facts from free-form row text or HTML | `row_operations.md`, `web.md`, `testing.md` | Canonical enrichment uses known providers/resolvers; extraction needs its own grounding and verification path. |
| Geography classification | `geo.md` | Geo verdicts have their own supported target semantics and audit fields. |
| Sequence source staging, promotion, publish, or send | `sequences.md`, `sequence_authoring.md`, `safety.md` | Activation is a separate platform mutation with stricter approval boundaries. |
| Workflow source windows, checkpointing, replay, reports, or Play sync | `workflow_sources.md`, `workflow_operations.md`, `workflow_reports.md`, `workflow_schedules.md` | Workflow freshness and canonical output sync are not provider enrichment semantics. |

## Mental Model

Enrichment has three operating classes:

| Class | Typical Surfaces | Result Shape | Preserve |
| --- | --- | --- | --- |
| Inline resolver | `emails`, `validate_emails`, `companies`, `company_linkedin_urls`, `resolve_rows` | `dict` with resolved rows/issues/provenance | Input row ids, requested fields, mapped fields, provider issues |
| Artifact-backed resolver/job | `resolve_artifact`, `addresses`, `company_research_start`, `investor_portfolio_start`, `person_profiles(..., with_recent_posts=True)`, `resolve_email_esps(..., async_artifact=True)` | `PendingJob` | `job_id`, `artifact_id`, row counts, preview rows, output artifact |
| Provider/platform status job | `company_domains`, `phone_lookup_start/status`, `deanonymize_contacts/results`, `extract_person_attributes/status/results` | `dict` job/status/results payloads | job/lookup ids, status, unresolved rows, errors, confidence |

Keep source acquisition, enrichment, qualification, and activation as visible
boundaries. A good run log says what rows entered enrichment, what provider job
or resolver ran, how many rows got usable fields, and what was not found.

## Preflight

1. Verify workspace focus with `vf.auth.status()` or `vf.session.init()` before
   provider-backed calls.
2. Print the input row count and a redacted sample. Confirm the rows came from
   the intended source/workflow run.
3. Confirm the exact fields needed downstream. Do not enrich fields just because
   they might be useful later.
4. Choose the narrow helper before `resolve_rows` or `resolve_artifact`.
5. Preserve stable row identifiers such as `row_identifier`, CRM ids,
   artifact row ids, company domains, and LinkedIn URLs.
6. For email enrichment, pass every available identity field. Do not pass only
   `contact_linkedin_url` when name and company/domain context exists.
7. Decide inline vs artifact before batching. Use artifacts for large outputs,
   downstream report/Play UI work, or later sequence staging.
8. For anything that can spend credits or call providers, run one or a small
   sample first and inspect returned fields/errors before N rows.

## Risk And Side Effects

| Operation | Risk | Operating Rule |
| --- | --- | --- |
| Inline email, profile, company, phone, address, identity, and research calls | Provider work and possible workspace credit usage | Bound row counts, print counts and job ids, and keep unresolved rows visible. |
| `email.esp.resolve` | Public DNS-backed metadata, currently no credit charge | Treat as enrichment output, not email discovery or deliverability approval. |
| Address discovery | Variable resolver-plan spend: baseline, paid Firecrawl fetches, LLM calls, and paid search fallback | Use only after domains are verified and cap `max_rows`; static HTTP fetches are free but still not proof of correctness. |
| Person attributes | Claim-gated provider work for copy-calibration | Use person LinkedIn URLs and sender context; do not use company URLs. |
| Sequence staging/publish/send | Out of scope here | Review `sequences.md` and `safety.md`; enrichment output is not approval to activate. |

## Canonical Operating Pattern

```python
import verbiflow as vf

vf.auth.ensure_logged_in()

rows = [
    {
        "row_identifier": "signup_001",
        "contact_full_name": "Ada Lovelace",
        "contact_linkedin_url": "https://www.linkedin.com/in/example",
        "company_name": "Example",
        "company_domain": "example.com",
    }
]

result = vf.enrichment.emails(rows=rows)
resolved_rows = result.get("rows", [])
print("email enrichment rows", len(resolved_rows), "issues", result.get("issues"))
```

For artifact-backed work:

```python
pending = vf.enrichment.resolve_artifact(
    rows=rows,
    capability_id="company.profile.fetch",
    requested_field_keys=["company_name", "company_linkedin_url", "company_description"],
    artifact_label="Company profiles",
)
result = vf.jobs.result(pending).wait(timeout=300)
preview = result.rows(limit=10)
print(pending.job_id, pending.artifact_id, preview[:2])
```

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.enrichment.addresses` | Use to discover the postal address (street, city, state, postal code, country) for a list of company domains by fetching their landing page, picking the legal / contact pages from the link list, and extracting the address fields from those pages. Exact credit rates come from the server's currently published meter book; paid provider work is accounted from actual usage. Static HTTP page fetches do not add provider usage. | <strong>Required</strong>: `rows_or_artifact` (RowsOrArtifact; required): Rows, RowSet, artifact id, Artifact, or absolute CSV path. Each row must include a domain column.<br><strong>Optional</strong>: `domain_column` (str; optional; default `'company_domain'`): Column carrying the company domain or URL to discover an address for.; `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.; `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 address columns per row: `address_street`, `address_city`, `address_state`, `address_postal`, `address_country`, `address_source` (the URL the fields came from), `address_confidence` (low / medium / high), `address_error` (set when the discovery failed), `address_credit_units`, and `address_audit_json`.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.jobs.stream_rows`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/enrichment.py:90-180` |
| `vf.enrichment.companies` | Use to resolve company profile fields for supplied company rows/domains. | <strong>Required</strong>: `rows` (array; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `requested_field_keys` (array; optional): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (string; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `dict` with resolved rows, canonical fields, provider provenance, issues, and raw Bridge metadata. | provider_call | installed SDK: `verbiflow/enrichment.py:411-435` |
| `vf.enrichment.company_domains` | Use to start or wait on Bridge-backed company-name to domain resolution for rows containing company names and optional website hints. | <strong>Required</strong>: `companies` (Sequence[Mapping[str, object]]; required): Company rows to resolve. Pass dictionaries with `company_name` plus optional `company_domain`, `company_website`, or source URL hints; do not pass bare strings.<br><strong>Optional</strong>: `wait` (bool; optional; default `False`): If false, return immediately with a job/status handle; if true or a timeout integer, poll until completion or timeout.<br><strong>Note</strong>: `companies` must be row dictionaries, not bare strings. Include `company_name` and any available `company_domain`, `company_website`, or source URL hints. `wait=True` polls until results are ready. | `dict` start/status/results payload for company-name to domain resolution. `wait=False` returns job/start metadata; `wait=True` returns completed result rows.<br><strong>Inspect</strong>: `wait=False` returns the start/job payload. `wait=True` polls Bridge and returns the completed domain-resolution results. | provider_call | installed SDK: `verbiflow/enrichment.py:524-553` |
| `vf.enrichment.company_linkedin_urls` | Use to resolve company LinkedIn URLs from company rows/domains. | <strong>Required</strong>: `domains` (Sequence[str]; required): Company domains to resolve, enrich, or search.<br><strong>Optional</strong>: `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `dict` with resolved rows, canonical fields, provider provenance, issues, and raw Bridge metadata. | provider_call | installed SDK: `verbiflow/enrichment.py:497-522` |
| `vf.enrichment.company_research` | Use to start a company research status workflow for one domain. | <strong>Required</strong>: `domain` (str; required): Company domain to research, such as example.com. URLs are accepted and normalized by Bridge.<br><strong>Optional</strong>: `force` (bool; optional; default `False`): When true, bypass a fresh cached company research result and start a new provider-backed run. | `company_research_status` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.enrichment.get_company_research` | provider_call | installed SDK: `verbiflow/enrichment.py:555-585` |
| `vf.enrichment.company_research_start` | Use to start durable artifact-backed company research for a domain when downstream workflow steps need streamed rows, job progress, artifact inspection, report rendering, or staging. | <strong>Required</strong>: `domain` (str; required): Company domain to research, such as example.com. URLs are accepted and normalized by Bridge.<br><strong>Optional</strong>: `force` (bool; optional; default `False`): When true, bypass a fresh cached company research result and start a new provider-backed run. | `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.result`, `vf.jobs.stream_rows`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/enrichment.py:587-617` |
| `vf.enrichment.deanonymize_contacts` | Use when you have partial signup/contact identity hints such as name plus company, company_domain, job title, or location and need the likely LinkedIn profile/title before enrichment or people expansion. | <strong>Required</strong>: `contacts` (Sequence[Mapping[str, object]]; required): Partial contact records to resolve into likely LinkedIn identities. Each row should include at least two useful identity hints, such as first_name plus company_domain, or first_name plus last_name plus company_name.<br><strong>Optional</strong>: `confidence_threshold` (int \| None; optional; default `None`): Optional minimum match confidence accepted by the backend deanonymizer. Omit to use the backend default.; `skip_cache` (bool; optional; default `False`): When true, bypass cached identity-resolution results and force fresh provider work. | `dict` with deanonymization job id, workflow id, status, partial/final results, match confidence, LinkedIn URL, profile payload, and input echo.<br><strong>Common next calls</strong>: `vf.enrichment.deanonymize_contacts_results` | provider_call | installed SDK: `verbiflow/enrichment.py:771-829` |
| `vf.enrichment.deanonymize_contacts_results` | Use to fetch partial or final identity-resolution results after `deanonymize_contacts` returns a job id. | <strong>Required</strong>: `job_id` (str; required): Backend job id returned by a start/search/enrichment/qualification call.<br><strong>Optional</strong>: none | `dict` with deanonymization job id, workflow id, status, partial/final results, match confidence, LinkedIn URL, profile payload, and input echo. | read | installed SDK: `verbiflow/enrichment.py:831-846` |
| `vf.enrichment.emails` | Use to resolve work emails for supplied contact rows with person and company identity fields. | <strong>Required</strong>: `rows` (array; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `requested_field_keys` (array; optional): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (string; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `dict` with resolved rows, canonical fields, provider provenance, issues, and raw Bridge metadata. | provider_call | installed SDK: `verbiflow/enrichment.py:308-334` |
| `vf.enrichment.extract_person_attributes` | Use to start person-attributes extraction from supplied LinkedIn profile URLs when sender context is available for copy-calibration. | <strong>Required</strong>: `linkedin_urls` (array; required): LinkedIn profile URLs to enrich.<br><strong>Optional</strong>: `sender_context` (object; optional): Sender details for calibration, such as sender_name, sender_company, sender_role, sender_school, company description, or affiliations.<br><strong>Note</strong>: `linkedin_urls` are person profile URLs, not company URLs. Provide `sender_context` for useful connection-story and tone calibration even though the SDK accepts an omitted context. | `dict` with extraction job id, workflow id, and input count. Poll `extract_person_attributes_status`, then read `extract_person_attributes_results`.<br><strong>Common next calls</strong>: `vf.enrichment.extract_person_attributes_status`, `vf.enrichment.extract_person_attributes_results` | provider_call | installed SDK: `verbiflow/enrichment.py:848-879` |
| `vf.enrichment.extract_person_attributes_results` | Use after `extract_person_attributes_status` is finished to read extracted person attributes. | <strong>Required</strong>: `job_id` (str; required): Backend job id returned by a start/search/enrichment/qualification call.<br><strong>Optional</strong>: none | `dict` with completed person-attribute rows, found flags, status, and error metadata. | read | installed SDK: `verbiflow/enrichment.py:897-910` |
| `vf.enrichment.extract_person_attributes_status` | Use to poll person-attributes extraction after the start call returns a job id. | <strong>Required</strong>: `job_id` (str; required): Backend job id returned by a start/search/enrichment/qualification call.<br><strong>Optional</strong>: none | `dict` with person-attributes job status, input/unique/result counts, found count, and failure metadata.<br><strong>Common next calls</strong>: `vf.enrichment.extract_person_attributes_results` | read | installed SDK: `verbiflow/enrichment.py:881-895` |
| `vf.enrichment.get_company_research` | Use to read completed company research by domain. | <strong>Required</strong>: `domain` (str; required): Domain name to search, order, enrich, or normalize.<br><strong>Optional</strong>: none | `dict` with company research fields, citations/provenance, confidence signals, and source/provider metadata. | read | installed SDK: `verbiflow/enrichment.py:619-633` |
| `vf.enrichment.get_funding_data` | Use to resolve canonical fields such as emails, domains, profiles, company research, phones, or identity data. | <strong>Required</strong>: `company_name` (str; required): Company name to look up funding data for.<br><strong>Optional</strong>: `company_website` (str \| None; optional; default `None`): Optional company website/domain. Passed to the model to disambiguate same-named companies — recommended whenever the user has the domain. | `funding_data` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data. | provider_call | installed SDK: `verbiflow/enrichment.py:912-960` |
| `vf.enrichment.investor_portfolio` | Use to look up portfolio companies for one investor/source account. | <strong>Required</strong>: `name` (str; required): Investor, fund, accelerator, or source account name to resolve into portfolio-company rows.<br><strong>Optional</strong>: `force` (bool; optional; default `False`): Force parameter for `enrichment.investor_portfolio`. | `dict` with investor portfolio company rows, source provenance, cache metadata, and lookup errors when present. | provider_call | installed SDK: `verbiflow/enrichment.py:635-650` |
| `vf.enrichment.investor_portfolio_start` | Use to start durable artifact-backed portfolio-company lookup for one or more investors when downstream workflow steps need streamed rows, artifact inspection, joins, or reports. | <strong>Required</strong>: `names` (Sequence[str]; required): Investor, fund, or accelerator names to resolve into portfolio-company rows.<br><strong>Optional</strong>: `force` (bool; optional; default `False`): When true, bypass fresh cached portfolio results and refresh each investor lookup. | `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.result`, `vf.jobs.stream_rows`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/enrichment.py:652-706` |
| `vf.enrichment.person_profiles` | Use to resolve LinkedIn/person profile fields, optionally including recent posts. | <strong>Required</strong>: `linkedin_urls` (Sequence[str]; required): LinkedIn profile URLs to enrich.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `async_artifact` (bool; optional; default `False`): When true, start an artifact job instead of returning inline resolver rows.; `max_rows` (int; optional; default `200`): Maximum rows to upload, process, or return for this call.; `posts_within_days` (int; optional; default `20`): Lookback window in days for recent LinkedIn posts.; `recent_posts_limit` (int; optional; default `10`): Maximum recent posts per person, capped by backend policy.; `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `skip_cache` (bool; optional; default `False`): Bypass resolver cache where the backend capability supports it.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows.; `with_recent_posts` (bool; optional; default `False`): Whether to include recent LinkedIn posts in person profile enrichment. | `dict` resolver rows for inline mode. Returns `PendingJob` when `async_artifact=True` or `with_recent_posts=True`; use `vf.jobs.result(job).wait().rows(...)` for artifact rows. | provider_call | installed SDK: `verbiflow/enrichment.py:437-495` |
| `vf.enrichment.phone_lookup_start` | Use to start phone lookup for contacts after you have enough identity fields. | <strong>Required</strong>: none<br><strong>Optional</strong>: `linkedin_url` (str \| None; optional; default `None`): Canonical LinkedIn profile URL for the person to look up.; `provider_contact_id` (str \| None; optional; default `None`): Provider contact identifier for the person to look up.<br><strong>Note</strong>: Pass at least one lookup identity: `provider_contact_id` or `linkedin_url`. A no-argument call is accepted by the signature but is not a meaningful lookup. | `dict` with phone lookup job/status, matched phone fields, provider provenance, and unresolved rows.<br><strong>Common next calls</strong>: `vf.enrichment.phone_lookup_status` | provider_call | installed SDK: `verbiflow/enrichment.py:708-752` |
| `vf.enrichment.phone_lookup_status` | Use to poll phone lookup results from `enrichment.phone_lookup_start`. | <strong>Required</strong>: `lookup_id` (str; required): Phone lookup job id returned by `enrichment.phone_lookup_start`.<br><strong>Optional</strong>: none | `dict` with phone lookup job/status, matched phone fields, provider provenance, and unresolved rows. | read | installed SDK: `verbiflow/enrichment.py:754-769` |
| `vf.enrichment.resolve_artifact` | Use to resolve canonical fields such as emails, domains, profiles, company research, phones, or identity data. | <strong>Required</strong>: `capability_id` (str; required): Backend capability id to execute, such as email.enrich or company.profile.fetch.; `rows` (RowsInput; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `mapped_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical fields used to map input rows into resolver inputs.; `max_rows` (int; optional; default `500`): Maximum rows to upload, process, or return for this call.; `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `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.result`, `vf.artifacts.inspect`, `vf.rows.ai_analyze` | artifact_job | installed SDK: `verbiflow/enrichment.py:237-306` |
| `vf.enrichment.resolve_email_esps` | Use to resolve public DNS-backed recipient-domain ESP metadata from contact_email, company_domain, or company_website rows; this is not email discovery. | <strong>Required</strong>: `rows` (RowsInput; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `async_artifact` (bool; optional; default `False`): When true, start an artifact job instead of returning inline resolver rows.; `max_rows` (int; optional; default `500`): Maximum rows to upload, process, or return for this call.; `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `dict` with public DNS ESP rows in inline mode, or `PendingJob` when `async_artifact=True`.<br><strong>Inspect</strong>: Inline mode returns resolver rows. `async_artifact=True` returns `PendingJob`; preserve `job_id` and `artifact_id` and page through `vf.jobs.result(...)`.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.artifacts.inspect`, `vf.rows.ai_analyze` | public_dns_lookup | installed SDK: `verbiflow/enrichment.py:363-409` |
| `vf.enrichment.resolve_rows` | Use for general inline canonical field resolution when a narrower enrichment helper does not fit. | <strong>Required</strong>: `capability_id` (str; required): Backend capability id to execute, such as email.enrich or company.profile.fetch.; `rows` (RowsInput; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `mapped_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical fields used to map input rows into resolver inputs.; `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows.; `target_id` (str \| None; optional; default `None`): Optional backend target/resource id for provenance. | `dict` with resolved rows, canonical fields, provider provenance, issues, and raw Bridge metadata.<br><strong>Common next calls</strong>: `vf.rows.ai_analyze` | provider_call | installed SDK: `verbiflow/enrichment.py:182-235` |
| `vf.enrichment.validate_emails` | Use to validate supplied contact_email values through Bridge and return email validation rows; do not use it for domains, profiles, company research, phones, or identity fields. | <strong>Required</strong>: `rows` (RowsInput; required): Rows, dictionaries, or RowSet records to process.<br><strong>Optional</strong>: `requested_field_keys` (Sequence[str] \| None; optional; default `None`): Canonical output fields to request; omit to use the capability default field set.; `source_kind` (str; optional; default `'sdk'`): Source label sent to Bridge for provenance; keep sdk for SDK-created rows. | `email_validation_rows` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data. | provider_call | installed SDK: `verbiflow/enrichment.py:336-361` |
<!-- END GENERATED: core-sdk-surfaces -->

## Identity-Sensitive Relationship Evidence

`vf.enrichment.company_research(...)` intentionally omits unverified founder
candidates. Do not infer founders, executives, or other high-value
relationships from a loose title/headline match or from the absence of a field.

When the deliverable depends on a relationship, define the evidence predicate
first. Start with a typed people-at-company source using narrow role criteria,
then verify each candidate against current profile/title evidence through
`linkedin.md` or the managed browser route in `host_browser.md`. Preserve the
candidate's canonical profile URL, observed title, source URL, and observation
time. For high-stakes copy or activation, require corroborating evidence before
treating the relationship as verified.

Local operators may use the typed Crunchbase host surface as an additional
structured source after validating its saved provider session; it is supporting
evidence, not permission to skip current-profile verification.

## Data Contracts

### Canonical Identity Fields

Email enrichment uses the Apollo identity field set below. The SDK also
promotes common aliases such as `email`, `linkedin`, `fullname`, `domain`, and
`company` into these canonical keys before calling Bridge.

```python
vf.enrichment.APOLLO_EMAIL_IDENTITY_FIELD_KEYS
# (
#   "apollo_contact_id",
#   "contact_linkedin_url",
#   "contact_email",
#   "contact_full_name",
#   "contact_first_name",
#   "contact_last_name",
#   "company_domain",
#   "company_name",
# )
```

Pass all known fields. Richer identity context improves matching and prevents
the common anti-pattern of asking Apollo/email enrichment to resolve from only a
LinkedIn URL.

### Resolver Inputs

| Field | Meaning |
| --- | --- |
| `capability_id` | Backend capability to execute, such as `email.enrich`, `email.validate`, `email.esp.resolve`, `company.profile.fetch`, `person.profile.fetch`, `person.posts.fetch_recent`, or `company.linkedin.resolve`. |
| `rows` | Iterable dictionaries or a `RowSet`. Keep row ids and source columns in the rows for traceability. |
| `requested_field_keys` | Canonical output fields requested from the capability. Omit only when the capability default is correct. |
| `mapped_field_keys` | Canonical input fields Bridge should read from each row. The SDK automatically adds the Apollo identity field set for `email.enrich`. |
| `source_kind` | Provenance label. Keep the default `sdk` for SDK-created rows unless a specific integration needs another label. |

### Common Capability Boundaries

| Capability | Input Fields | Output Intent |
| --- | --- | --- |
| `email.enrich` | Apollo identity fields | Work email discovery. |
| `email.validate` | `contact_email` | Bounce-protection status/confidence fields. |
| `email.esp.resolve` | `contact_email`, `company_domain`, or `company_website` | Public DNS-backed ESP metadata. |
| `company.profile.fetch` | `company_linkedin_url`, `company_domain`, or `company_name` | Company profile facts. |
| `person.profile.fetch` | `contact_linkedin_url` | Person profile facts. |
| `person.posts.fetch_recent` | `contact_linkedin_url` plus recent-post options | Recent authored LinkedIn post evidence. |
| `company.linkedin.resolve` | `company_domain` | Company LinkedIn URL resolution. |
| `company.domain.resolve` | `company_name` plus optional hints | Bridge job for domain resolution, not the generic resolver endpoint. |

## Examples

### Validate And ESP-Check Existing Emails

```python
rows = [{"row_identifier": "1", "contact_email": "ada@example.com"}]

validation = vf.enrichment.validate_emails(rows=rows)
print(validation.get("rows", [])[:3])

esp = vf.enrichment.resolve_email_esps(rows=rows)
print(esp.get("rows", [])[:3])
```

### Fetch Person Profiles With Recent Posts

```python
job = vf.enrichment.person_profiles(
    ["https://www.linkedin.com/in/example"],
    with_recent_posts=True,
    recent_posts_limit=3,
    posts_within_days=30,
    artifact_label="Recent LinkedIn evidence",
)
rows = vf.jobs.result(job).wait(timeout=300).rows(limit=10)
print(rows)
```

### Resolve Partial Signups Before Email Enrichment

```python
identity_job = vf.enrichment.deanonymize_contacts(
    [
        {
            "first_name": "Ada",
            "last_name": "Lovelace",
            "company_name": "Example",
            "company_domain": "example.com",
        }
    ],
    confidence_threshold=70,
)
identity_rows = vf.enrichment.deanonymize_contacts_results(str(identity_job["job_id"]))
print(identity_rows.get("results", [])[:3])
```

### Company Research As A Durable Artifact

```python
pending = vf.enrichment.company_research_start("example.com")
result = vf.jobs.result(pending).wait(timeout=300)
print(pending.job_id, pending.artifact_id, result.rows(limit=5))
```

## Verification

- Compare input rows to output rows by stable ids; do not rely only on list
  order.
- Print match-rate counts: input rows, enriched rows, missing values, provider
  errors, and validation statuses.
- Inspect provider issue/error fields before filtering or staging.
- For email enrichment, verify returned `contact_email` domains and confidence
  against the target company/domain policy before activation.
- For company/domain enrichment, verify redirects and canonical domains when
  CRM or sequence suppression depends on domain equality.
- For artifact jobs, use `vf.jobs.result(job).wait(...)`, then inspect
  `artifact_id`, preview rows, schema, and row count before using the artifact
  downstream.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Email enrichment returns too few rows | Confirm rows include as many Apollo identity fields as possible: LinkedIn URL, full name, first/last name, company domain, and company name. |
| `resolve_rows` works but staging fails | `resolve_rows` is inline and has no artifact to stage. Use `resolve_artifact` or stage an artifact created later. |
| `person_profiles` returns a `dict` in one run and a job in another | It returns a `PendingJob` when `with_recent_posts=True` or `async_artifact=True`; otherwise it returns inline resolver rows. |
| `company_domains` returns only a start payload | You passed `wait=False`. Use the returned job/status fields or call with `wait=True` for completed results. |
| Phone lookup starts but cannot match | Provide `provider_contact_id` or `linkedin_url`; no-identity calls are not useful. |
| Company/profile enrichment returns provider-specific field names | Run one row first and print the raw dict. Normalize fields explicitly before qualification, reports, or sequence variables. |
| You need arbitrary text extraction from a page | Use `web.md` and `row_operations.md`; enrichment is for canonical provider/resolver fields. |

## Related References

| If You Are Doing This | Read Next |
| --- | --- |
| Acquiring the initial audience/source rows | `audience_discovery.md`, `crm.md`, `web.md`, `linkedin.md`, `places.md` |
| Cleaning, joining, sampling, or qualifying rows before/after enrichment | `row_operations.md`, `artifacts_jobs.md` |
| Polling, paging, exporting, or reporting artifact-backed enrichment jobs | `artifacts_jobs.md`, `workflow_reports.md` |
| Building a multi-step generated workflow play | `workflow_composition.md`, `workflow_operations.md`, `workflow_sources.md` |
| Staging enriched rows to a sequence source | `sequences.md`, `sequence_authoring.md`, `safety.md` |
| Verifying response shapes and one-row-before-N behavior | `testing.md` |
