Most Salesforce enrichment integrations work around the platform. They run external schedulers, stand up background sync services, or push data through middleware that lives outside your org.
We went the other direction. The Enrich Layer integration runs its orchestration entirely inside Salesforce: no exports, no separate sync service, no middleware. Enrichment still calls the Enrich Layer API, but the workflow itself stays within the platform's own execution model. Here’s how we did it.
Working within Salesforce's world
Salesforce has specific ideas about how code should run. Execution contexts are short-lived and stateless. Each time your code runs, it starts fresh with no memory of what happened before. You can't maintain state implicitly across runs, and you can't run anything for more than a few seconds at a time.
For single-record enrichment, these constraints don't matter much. You click a button, we call the Enrich Layer API, we write the data back to your Salesforce fields, and you're done. The whole thing happens in a couple of seconds.
But bulk enrichment is different. When someone wants to enrich 5,000 contacts at once, you need a different approach.
The problem with bulk enrichment
We couldn't just loop through 5,000 records and call the API for each one. Salesforce would cease the execution before we got through the first hundred. And even if we could, we'd slam into the Enrich Layer API rate limits immediately.
Instead, we designed around chained Queueable jobs. Each job picks records off the queue, checks an in-org rate limiter before every API call, and when the API rate-limits us, schedules the next job using the Retry-After delay. The chain is bounded, at most 60 links deep. Batch size comes from the per-day rate limit you set in package settings during install, not from anything we read off the API.
This design works within Salesforce's execution time limits and stays under API rate limits. It’s not elegant, but it’s predictable.
The identifier question
The obvious identifier for re-enrichment is email or name. But emails rotate and names change. The better question: what identifier survives across enrichment runs? For us, that turned out to be LinkedIn profile URLs.
When the record already has a LinkedIn profile URL, we use it directly and enrich in one API call. When it doesn't, we resolve it first. We send the person's first and last name plus the company name to Enrich Layer's resolve endpoint, get back a LinkedIn URL, and then enrich. That's two API calls per record instead of one. The package needs both first and last name before it tries a lookup. Once resolved, we store the URL on the record so the next enrichment is one call again.
This gives us something reliable to work with. Names and emails change, while LinkedIn profile URLs are usually more stable across enrichment runs. Using them as the primary identifier gives us a lower-ambiguity way to re-enrich the same record later.
Authentication and field mapping
Authentication uses your Enrich Layer API key, configured during installation. No OAuth flows, no connected apps. Every outbound request uses the same key.
Field mapping is explicit. The package writes enrichment data to its own Enrich Layer fields on Contacts, Leads, and Accounts, never to standard Salesforce fields. We don't try to represent full historical datasets or multi-valued attributes inside Salesforce objects.
Salesforce's schema model is rigid, and we work within that rigidity rather than fighting it.
When things go wrong
Enrichment requests fail in three different ways, and each gets a different path:
- A 429 (rate limit) keeps the record in Queued status and re-tries it after the API's Retry-After delay clears. No retry ceiling.
- A 404 (profile not found) is terminal: the record is marked Not Found and left alone. Anything else — auth, network, server errors — retries up to three times, then lands in Failed - Exhausted.
- A sweep job runs every 15 minutes and re-queues anything still under three retries.
We log everything: enrichment attempts, success or failure status, retry behavior. The logs are associated with the specific record, so you can inspect what happened and when. This gives you operational visibility without needing external monitoring tools.
Distribution and installation
We deliver the integration as a managed Salesforce package, installed directly via package URL. You install it into your org with a one-click link, and updates flow through the same channel.
The managed package approach lets us version and update the integration while staying within Salesforce's standard packaging framework. Updates happen through the same channel, and your customizations stay intact.
What we ended up with
The integration orchestrates enrichment inside Salesforce. You trigger enrichment from your CRM, and it updates your records directly using LinkedIn profile URLs as identifiers and your API key for authentication. Single records enrich in seconds. Bulk operations work through queued batches that respect both Salesforce execution limits and API rate limits.
We built this to work within constraints, not around them. Salesforce's platform has specific rules about execution contexts, state management, schema flexibility, and time limits. We designed for those rules from the beginning.
The result is an integration that operates within Salesforce's execution model, not alongside it. No external schedulers or middleware, no separate sync processes, no state management outside the platform. Enrichment still reaches out to the Enrich Layer API for the data itself; the orchestration around that call doesn't leave Salesforce.
