Skip to main content

Enrichfold

Provider-neutral, provenance-first entity enrichment for people and companies.

Website · PyPI · npm

enrichfold has an offline-first enrichment core and an optional web search package. Applications supply credentials. Search engines include Exa, Parallel, You.com, Tavily, Linkup, Seltz, TinyFish, Nimble, Browserbase, Serper, and DuckDuckGo. Optional Keenable and Scrapefold page adapters are also available. Every accepted attribute retains its source URL, observation time, and confidence, so downstream systems can decide whether a result is suitable for an automated action or requires review.

from enrichfold import Entity, EnrichmentPipeline, Evidence

class CompanyProvider:
    def discover(self, entity):
        return [Evidence(
            source_url="https://example.com/team",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.94,
            attributes={"industry": "software", "company_size": "51-200"},
        )]

company = Entity.company(domain="example.com")
result = EnrichmentPipeline([CompanyProvider()]).enrich(company)
print(result.attributes["industry"].value)  # software

Evidence, conflicts, and review gates

The core keeps provider I/O in adapters. It turns supplied evidence into deterministic decisions while retaining disagreements for a human approval flow. A conflicting value is never silently accepted:

from enrichfold import Claim, Evidence, reconcile_claims

result = reconcile_claims([
    Claim(
        field="industry",
        value="software",
        evidence=Evidence(
            source_url="https://acme.example/about",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.91,
        ),
    ),
    Claim(
        field="industry",
        value="retail",
        evidence=Evidence(
            source_url="https://directory.example/acme",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.88,
        ),
    ),
])

industry = result.fields["industry"]
assert industry.value == "software"          # stable suggested value
assert industry.status == "needs_review"     # do not automate an action
assert result.requires_review is True

Claim(kind="inferred", ...) also requires review even with no competing claim. This distinction makes it possible to keep model-produced hypotheses without presenting them as observed facts.

Multi-provider research runs

ResearchEngine is the boundary for applications that call several research providers. It runs caller-owned adapters concurrently, reserves generic units before starting work, retains each provider outcome, and returns an explicit coverage/review state. It does not make network calls itself.

from enrichfold import (
    Claim,
    Evidence,
    Entity,
    ProviderOutput,
    ProviderSpec,
    ResearchBudget,
    ResearchEngine,
)

def official_site(entity):
    # The host application owns this adapter, its HTTP client, and credentials.
    return ProviderOutput(
        claims=(Claim(
            field="industry",
            value="software",
            evidence=Evidence(
                source_url="https://example.com/about",
                observed_at="2026-08-20T12:00:00Z",
                confidence=0.9,
            ),
        ),),
        usage_units=2,
    )

result = ResearchEngine(
    [ProviderSpec("official-site", official_site, reserved_units=2)],
    budget=ResearchBudget(max_units=5),
).run(Entity.company(domain="example.com"), requested_fields=("industry",))

assert result.status in {"completed", "partial", "needs_review", "failed"}
assert result.budget.reserved_units == 2

An optional EvidenceValidator can return EvidenceVerdict("needs_review", reason) for a weak source or EvidenceVerdict("rejected", reason) to keep it out of resolution. In either case, the result preserves the original claim, source URL, and verdict in evidence_assessments.

Keenable search provider

KeenableProvider uses the same REST search contract as Scrapefold's Keenable engine. It reads KEENABLE_API_KEY by default and performs one search per entity. Because search results are sources rather than verified attributes, the caller maps them to claims explicitly:

from enrichfold import (
    Claim,
    Entity,
    Evidence,
    KeenableProvider,
    ProviderSpec,
    ResearchEngine,
)

def claims(entity, results):
    for result in results:
        yield Claim(
            field="summary",
            value=result["description"],
            kind="inferred",
            evidence=Evidence(
                source_url=result["url"],
                observed_at=result["acquired_at"],
                confidence=0.8,
                provider="keenable",
            ),
        )

provider = KeenableProvider(
    lambda entity: f'{entity.identifiers["domain"]} company',
    claims,
)
result = ResearchEngine([
    ProviderSpec("keenable", provider, reserved_units=1),
]).run(Entity.company(domain="example.com"), requested_fields=("summary",))

Web search and Scrapefold pages

Install enrichfold[search] for direct search. Scrapefold's search() delegates to this API; its URL engines fetch pages. The caller decides which results substantiate claims:

from enrichfold import ScrapefoldScrapeProvider, WebSearchProvider
from enrichfold.search import SearchOptions, search

# await search("example.com company", SearchOptions(engines=("parallel", "tavily")))
provider = WebSearchProvider(
    query=lambda entity: f'{entity.identifiers["domain"]} company',
    map_results=claims,  # same (entity, results) mapper as KeenableProvider
    engines=("parallel", "tavily"),
    usage_units=2,
)

page_provider = ScrapefoldScrapeProvider(
    url=lambda entity: f'https://{entity.identifiers["domain"]}/about',
    map_result=map_page,  # (entity, page) -> claims
    engines=("firecrawl",),
    usage_units=1,
)

Each result passed to map_results has url, title, description, engines, and acquired_at. Pass usage_units and reserve at least that many units in its ProviderSpec when enforcing a research budget. The page mapper receives url, text, markdown, html, json, engine, and acquired_at.

Grounding validator

GroundingValidator is an optional EvidenceValidator that checks a provider-asserted value against the actual text of its own source page. The default engine behaviour is to trust provider values (the verdict is literally accepted, "no validator configured"); this adapter closes that gap.

It is a standalone, opt-in adapter like KeenableProvider: enrichfold's core never imports it and never gains an HTTP client. The adapter does I/O only through a caller-supplied fetch(url) -> str callable, so "core never fetches" stays true. Wire in a Scrapefold-backed fetch (or any other):

from enrichfold import GroundingValidator, ProviderSpec, ResearchEngine, Entity
import scrapefold  # host dependency, not enrichfold's

validator = GroundingValidator(lambda url: scrapefold.scrape_sync(url).text)

result = ResearchEngine(
    [ProviderSpec("official-site", official_site, reserved_units=2)],
    evidence_validator=validator,
).run(Entity.company(domain="example.com"), requested_fields=("industry",))

For each claim the validator fetches claim.evidence.source_url and grounds claim.value (and any evidence.attributes values) in the returned text:

  • accepted when the value is found (coverage reaches min_accept_coverage, all values by default).
  • rejected when no value is found - it is kept out of resolution.
  • needs_review when only some values are found, when there is no groundable value, or when the fetch fails. A fetch failure never raises: it becomes a review verdict with a redacted reason.

The adapter adds no claims - it only returns a verdict, so it "must not perform hidden enrichment" holds. As with any validator, the original claim, source URL, and verdict are preserved in evidence_assessments.

The matching is done by find_citations(text, targets), a stdlib-only, two-pass exact-then-normalized substring matcher returning coverage. It is a port of Scrapefold's citation algorithm, kept inside this adapter rather than imported so enrichfold has no dependency on Scrapefold and stays offline-testable.

Company identity gate

Before a caller enriches or acts on a company, use the offline identity gate. It is deliberately conservative: free mailboxes, invalid sites, domain conflicts, and corporate domains that do not exactly match the name receive a review status. Applications can pass separately verified site metadata when they have it.

from enrichfold import derive_company_identity

identity = derive_company_identity(
    email="hello@acme.example",
    company_name="Acme",
    website="https://www.acme.example/about",
)

assert identity.status == "verified"
assert identity.canonical_domain == "acme.example"

Design boundaries

  • Core reconciliation and identity APIs make no network calls; explicit provider adapters such as KeenableProvider may do so.
  • No inferred facts: a field is returned only when a provider supplies evidence.
  • Conflicts have a deterministic suggested value but are marked needs_review.
  • Inferred claims are always marked needs_review.
  • Multi-provider runs reserve caller-defined generic units before execution and expose partial coverage rather than hiding failed or skipped providers.
  • Optional source-policy hooks can accept, reject, or route evidence to review. GroundingValidator uses only a caller-supplied fetch callable; the optional search package performs network calls only when explicitly invoked.
  • Company identity is verified only through an exact name/domain match or caller-supplied, independently verified same-domain site metadata.
  • Built-in search adapters and caller-owned providers can be combined with public data APIs, browser tools, or internal approved sources.

The package intentionally does not decide whether a review is approved or run an action after one; persistence, permissions, UI, and provider-specific claim extraction stay with the host application.

Installation

Python

pip install enrichfold
# For direct web search:
pip install 'enrichfold[search]'

TypeScript / Node.js

The TypeScript companion currently exposes the same offline company identity gate. It is intentionally a normal npm dependency, rather than a Python subprocess hidden inside a web application:

npm install @mihailorama/enrichfold

Its provider runtime will follow as a compatible TypeScript surface; Python and TypeScript package versions are released independently.

Development

uv run --with pytest pytest -q
python -m build

License

MIT.

Release files for enrichfold 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enrichfold 0.6.0
File Size Uploaded
enrichfold-0.6.0.tar.gz 2.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for enrichfold 0.6.0
File Interpreter ABI Platform
enrichfold-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.6 MB

Release files / enrichfold-0.6.0.tar.gz

Download URL enrichfold-0.6.0.tar.gz
Size 2.5 MB
Tags Source
SHA-256 checksum
How to use checksums
35037d2330130fa012e328d1fd754681f5dc94736207ffae334c3b2da8f10b53
BLAKE2b-256 checksum
How to use checksums
7ff23df4d513fb938ffd6ad058609693f71c435d6ebdf479ca46c261e7710fd2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / enrichfold-0.6.0-py3-none-any.whl

Download URL enrichfold-0.6.0-py3-none-any.whl
Size 41.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b9de9b5f1b95d4ff7564cde9a0d98ef8e73fc3b42796a72bd5689859bad2e0e2
BLAKE2b-256 checksum
How to use checksums
de5043c68c0c2250beb950e485fbae4e9dcf34029348dd09ad30728772ea2f58
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page