# Enrichment

> Look companies and people up with People Data Labs or your own provider, store each result, and fill the record's empty columns.

Source: https://bettersupabase.com/docs/blocks/enrichment

The `enrichment` block looks a [parties](/docs/blocks/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.

```bash
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 [#providers]

```ts
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:

```ts
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 [#enrich]

```ts
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 filled
```

A 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 [#backfill-on-a-schedule]

```ts
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 [#events]

Every stored result writes `party.enriched` to the
[outbox](/docs/blocks/outbox) with the subject, the provider, the status and
the filled columns. Handle it to re-run [segments](/docs/blocks/segments) or
[assignment](/docs/blocks/assignment) on the new values. See
[Events](/docs/extending/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)      |