Enrichfold
Provider-neutral, provenance-first entity enrichment for people and companies.
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:
acceptedwhen the value is found (coverage reachesmin_accept_coverage, all values by default).rejectedwhen no value is found - it is kept out of resolution.needs_reviewwhen 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
KeenableProvidermay 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.
GroundingValidatoruses 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)
| File | Size | Uploaded | |
|---|---|---|---|
| enrichfold-0.6.0.tar.gz | 2.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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