Enrichment
Look companies and people up with People Data Labs or your own provider, store each result, and fill the record's empty columns.
The enrichment block looks a parties company or
person up with a data provider and stores the profile it returns, one result
per record and provider. A match fills the record's empty columns (a
company's domain and website, a person's phone and title) and never
overwrites a value a member typed. Every result writes a party.enriched
event.
better-supabase add enrichment # adds tenant, access and parties as well| Permission | Lets a member | Default roles |
|---|---|---|
enrichment.read | read a record's stored results | member, admin |
enrichment.run | look a record up and store it | admin (enrichment.*) |
The service role can always enrich, so a cron job works through the backlog without a member session.
Providers
import {
createEnrichment,
peopleDataLabsEnrichment,
rpcTransport,
} from "better-supabase/blocks/enrichment";
const enrichment = createEnrichment({
transport: rpcTransport(supabase),
provider: peopleDataLabsEnrichment({ apiKey: env.PDL_API_KEY }),
});peopleDataLabsEnrichment calls the People Data Labs company and person
enrich endpoints over fetch, so the block adds no SDK. A person match below
minLikelihood (6 by default, out of 10) counts as no match. Keep the key
server-side: create the client in a route handler, a server action or a job,
never in the browser.
Any other source plugs in as an EnrichmentProvider with apiVersion: 1, a
lowercase name that results are stored under, and two lookups that return
a profile or undefined for no match:
import type { EnrichmentProvider } from "better-supabase/blocks/enrichment";
import { AsyncResult, ok } from "better-supabase";
const crm: EnrichmentProvider = {
apiVersion: 1,
name: "internal-crm",
company: ({ domain }) =>
AsyncResult.from(async () =>
ok(domain === undefined ? undefined : await lookUp(domain)),
),
person: () => AsyncResult.ok(undefined),
};A company lookup gets the record's name, domain and website; a person
lookup gets name, email, phone and the domain of their primary company.
Return a provider error (rate_limited, unauthorized or network) instead
of throwing; the block then stores nothing.
Enrich
const result = await enrichment
.enrich({ type: "company", id: company.id })
.orThrow();
result.status; // "matched" or "not_found"
result.data; // the provider's profile, such as industry and employees
result.filled; // ["domain"]: the columns this result filledA second lookup with the same provider replaces the stored result. Pass
{ fill: false }, or fill: false to createEnrichment, to store profiles
without touching the record. A domain another active company in the
organization already holds is not filled, so enrichment never creates a
duplicate. of({ type, id }) lists a record's results, one per provider,
the newest first.
Backfill on a schedule
const report = await createEnrichment({
transport: rpcTransport(admin),
provider,
logger,
})
.enrichDue({ limit: 50, staleAfter: "90 days" })
.orThrow();enrichDue takes the companies with a domain or website and the people with
an email that have no result from the provider, or one older than
staleAfter (30 days by default), records never enriched first. It returns
{ checked, matched, notFound, failed }, logs each failed lookup, and stops
early when the provider rate limits or refuses the key. Failed records stay
due for the next run.
Events
Every stored result writes party.enriched to the
outbox with the subject, the provider, the status and
the filled columns. Handle it to re-run segments or
assignment on the new values. See
Events.
| Error code | When |
|---|---|
ENRICHMENT_NOT_FOUND | no active record you may enrich has the id |
ENRICHMENT_SUBJECT_UNKNOWN | the subject type is not the company or person type |
ENRICHMENT_INVALID | a bad provider name, status or profile |
ENRICHMENT_KEY_REFUSED | the provider refused the API key (401 or 403) |
Last updated on