Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .agents/skills/add-enrichment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Because enrichments run on Sim's hosted keys by default, **every provider tool y

## Architecture (what you're plugging into)

- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard (see `enrichments/provider-failures/`). Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `firstNonEmpty(arr)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard. Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
- **`enrichments/run.ts`** — the server-only cascade runner. Calls `executeTool(provider.toolId, { ...params, _context: { workspaceId, userId } })`, accumulates hosted-key cost, returns the first non-empty mapped result. **You do not edit this** — it works for any registry entry.
- **`enrichments/registry.ts`** — `ENRICHMENT_REGISTRY` / `ALL_ENRICHMENTS` / `getEnrichment`. Register new entries here.

Expand Down Expand Up @@ -111,7 +111,7 @@ export { myEnrichment } from './my-enrichment'
```

Rules:
- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, `@/enrichments/provider-failures/*`, and the types. **Never import `@/tools`** here — the runner does the tool call.
- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call.
- `buildParams` returns `null` when inputs are insufficient (provider skipped). `mapOutput` returns `null`/empty for a miss (falls through). Use `filterUndefined` when assembling optional tool params; coerce numbers explicitly (don't pass `''` to number outputs).
- Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config).

Expand Down
34 changes: 34 additions & 0 deletions apps/docs/content/docs/integrations/firecrawl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,38 @@ Retrieve the remaining and allocated Firecrawl credits for the team.

</ReferenceDetails>

### Firecrawl Enrich

Find work emails, phone numbers, LinkedIn profiles, company domains, and company information through Firecrawl Alexandria. Uses FullEnrich for contact lookups and People Data Labs for company, name-based profile, and single-part-name contact lookups. Provider terms must be enabled in Firecrawl. People Data Labs has a 50,000-record annual team limit.

<ReferenceDetails>

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Firecrawl API key |
| `enrichmentType` | string | Yes | Lookup type: work_email, phone, linkedin, company, or company_domain |
| `fullName` | string | No | Full name of the person |
| `companyDomain` | string | No | Company website domain |
| `companyName` | string | No | Company name when the domain is unknown |
| `linkedinUrl` | string | No | LinkedIn profile URL for work email or mobile lookup |
| `email` | string | No | Email address for reverse LinkedIn profile lookup |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `email` | string | Work email, when found |
| `emailVerified` | boolean | True only when FullEnrich reports DELIVERABLE; catch-all and high-probability addresses remain unverified |
| `phone` | string | Phone number, preferring mobile when available |
| `linkedinUrl` | string | LinkedIn profile URL, when found |
| `domain` | string | Company website domain, when found |
| `employeeCount` | string | Company headcount or size range |
| `description` | string | Company description |
| `creditsUsed` | number | Firecrawl credits charged for this lookup |
| `statusCode` | number | Provider or HTTP response status code |

</ReferenceDetails>


111 changes: 111 additions & 0 deletions apps/sim/blocks/blocks/firecrawl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const FirecrawlBlock: BlockConfig = {
],
parse: [{ text: 'Parse', field: DOCUMENT_FIELD, core: true, after: 'into markdown' }],
credit_usage: ['Read remaining credit balance'],
enrich: [{ text: 'Look up', field: 'enrichmentType', core: true }],
},
},
},
Expand All @@ -75,9 +76,93 @@ export const FirecrawlBlock: BlockConfig = {
{ label: 'Agent', id: 'agent' },
{ label: 'Parse Document', id: 'parse' },
{ label: 'Credit Usage', id: 'credit_usage' },
{ label: 'Enrich Contact or Company', id: 'enrich' },
],
value: () => 'scrape',
},
{
id: 'enrichmentType',
title: 'Enrichment Type',
type: 'dropdown',
options: [
{ label: 'Work Email', id: 'work_email' },
{ label: 'Phone Number', id: 'phone' },
{ label: 'LinkedIn Profile', id: 'linkedin' },
{ label: 'Company Information', id: 'company' },
{ label: 'Company Domain', id: 'company_domain' },
],
value: () => 'work_email',
condition: { field: 'operation', value: 'enrich' },
required: true,
},
{
id: 'fullName',
title: 'Full Name',
type: 'short-input',
placeholder: 'Full name of the person',
condition: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: ['work_email', 'phone', 'linkedin'] },
},
},
{
id: 'companyDomain',
title: 'Company Domain',
type: 'short-input',
placeholder: 'example.com',
required: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: 'company' },
},
condition: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: ['work_email', 'phone', 'linkedin', 'company'] },
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
},
},
{
id: 'companyName',
title: 'Company Name',
type: 'short-input',
placeholder: 'Company name',
required: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: 'company_domain' },
},
condition: {
field: 'operation',
value: 'enrich',
and: {
field: 'enrichmentType',
value: ['work_email', 'phone', 'linkedin', 'company_domain'],
},
},
},
{
id: 'linkedinUrl',
title: 'LinkedIn Profile URL',
type: 'short-input',
placeholder: 'https://www.linkedin.com/in/...',
condition: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: ['work_email', 'phone'] },
},
},
{
id: 'email',
title: 'Email',
type: 'short-input',
placeholder: 'Email address for reverse lookup',
condition: {
field: 'operation',
value: 'enrich',
and: { field: 'enrichmentType', value: ['linkedin'] },
},
},
{
id: 'fileUpload',
title: 'Document',
Expand Down Expand Up @@ -470,6 +555,7 @@ Example 2 - Product Data:
'firecrawl_agent',
'firecrawl_parse',
'firecrawl_credit_usage',
'firecrawl_enrich',
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
],
config: {
tool: (params) => {
Expand Down Expand Up @@ -500,6 +586,8 @@ Example 2 - Product Data:
return 'firecrawl_parse'
case 'credit_usage':
return 'firecrawl_credit_usage'
case 'enrich':
return 'firecrawl_enrich'
default:
return 'firecrawl_scrape'
}
Expand Down Expand Up @@ -714,6 +802,15 @@ Example 2 - Product Data:
if (jobId) result.jobId = jobId
break

case 'enrich':
result.enrichmentType = params.enrichmentType
result.fullName = params.fullName
result.companyDomain = params.companyDomain
result.companyName = params.companyName
result.linkedinUrl = params.linkedinUrl
result.email = params.email
break

case 'credit_usage':
break
}
Expand All @@ -723,6 +820,12 @@ Example 2 - Product Data:
},
},
inputs: {
enrichmentType: { type: 'string', description: 'Contact or company lookup type' },
fullName: { type: 'string', description: 'Full name of the person' },
companyDomain: { type: 'string', description: 'Company website domain' },
companyName: { type: 'string', description: 'Company name' },
linkedinUrl: { type: 'string', description: 'LinkedIn profile URL' },
email: { type: 'string', description: 'Email address for reverse lookup' },
apiKey: { type: 'string', description: 'Firecrawl API key' },
operation: { type: 'string', description: 'Operation to perform' },
url: { type: 'string', description: 'Target website URL' },
Expand Down Expand Up @@ -775,6 +878,14 @@ Example 2 - Product Data:
zeroDataRetention: { type: 'boolean', description: 'Enable zero data retention' },
},
outputs: {
email: { type: 'string', description: 'Work email, when found' },
emailVerified: { type: 'boolean', description: 'Whether the work email is mailbox-verified' },
phone: { type: 'string', description: 'Phone number, preferring mobile when available' },
linkedinUrl: { type: 'string', description: 'LinkedIn profile URL, when found' },
domain: { type: 'string', description: 'Company website domain, when found' },
employeeCount: { type: 'string', description: 'Company headcount or size range' },
description: { type: 'string', description: 'Company description' },
statusCode: { type: 'number', description: 'Enrichment provider or HTTP response status code' },
// Scrape output
markdown: { type: 'string', description: 'Page content markdown' },
html: { type: 'string', description: 'Raw HTML content' },
Expand Down
35 changes: 6 additions & 29 deletions apps/sim/enrichments/company-domain/company-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { Globe } from '@sim/emcn/icons'
import { normalizeDomain, str, toolProvider } from '@/enrichments/providers'
import type { EnrichmentConfig } from '@/enrichments/types'

/**
* Company Domain enrichment. Resolves a company's website domain from its name
* via a People Data Labs company match, falling back to Datagma's company enrich.
*/
export const companyDomainEnrichment: EnrichmentConfig = {
id: 'company-domain',
name: 'Company Domain',
Expand All @@ -15,34 +11,15 @@ export const companyDomainEnrichment: EnrichmentConfig = {
outputs: [{ id: 'domain', name: 'domain', type: 'string' }],
providers: [
toolProvider({
id: 'pdl',
label: 'People Data Labs',
toolId: 'pdl_company_enrich',
id: 'firecrawl',
label: 'Firecrawl',
toolId: 'firecrawl_enrich',
buildParams: (inputs) => {
const name = str(inputs.companyName)
if (!name) return null
// `required` makes PDL 404 (free) when the match has no website,
// instead of charging a credit for a match we'd discard as a no-match.
return { name, required: 'website' }
const companyName = str(inputs.companyName)
return companyName ? { enrichmentType: 'company_domain', companyName } : null
},
mapOutput: (output) => {
const company = output.company as Record<string, unknown> | undefined
const domain = normalizeDomain(company?.website)
return domain ? { domain } : null
},
}),
toolProvider({
id: 'datagma',
label: 'Datagma',
toolId: 'datagma_enrich_company',
buildParams: (inputs) => {
// Datagma's `data` accepts a company name and returns its website.
const data = str(inputs.companyName)
if (!data) return null
return { data }
},
mapOutput: (output) => {
const domain = normalizeDomain(output.website)
const domain = normalizeDomain(output.domain)
return domain ? { domain } : null
},
}),
Expand Down
71 changes: 5 additions & 66 deletions apps/sim/enrichments/company-info/company-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import { filterUndefined } from '@sim/utils/object'
import { normalizeDomain, str, toolProvider } from '@/enrichments/providers'
import type { EnrichmentConfig } from '@/enrichments/types'

/**
* Company Info enrichment. Looks up a company by domain, trying Hunter first
* (free) then People Data Labs, then Datagma and LeadMagic as fallbacks. Outputs
* are limited to the fields the providers reliably return — employee count and
* description — so the result stays consistent regardless of which provider fills
* the cell. `employeeCount` is a string so Hunter's range bucket (e.g. `"11-50"`),
* PDL's exact count, and LeadMagic's range all map onto the same column.
*/
export const companyInfoEnrichment: EnrichmentConfig = {
id: 'company-info',
name: 'Company Info',
Expand All @@ -23,69 +15,16 @@ export const companyInfoEnrichment: EnrichmentConfig = {
],
providers: [
toolProvider({
id: 'hunter',
label: 'Hunter',
toolId: 'hunter_companies_find',
buildParams: (inputs) => {
const domain = normalizeDomain(inputs.domain)
if (!domain) return null
return { domain }
},
mapOutput: (output) => {
return filterUndefined({
employeeCount: str(output.size) || undefined,
description: str(output.description) || undefined,
})
},
}),
toolProvider({
id: 'pdl',
label: 'People Data Labs',
toolId: 'pdl_company_enrich',
buildParams: (inputs) => {
const website = normalizeDomain(inputs.domain)
if (!website) return null
// `required` makes PDL 404 (free) when neither field we extract is
// present, instead of charging a credit for a match we'd discard.
return { website, required: 'employee_count OR summary' }
},
mapOutput: (output) => {
const company = output.company as Record<string, unknown> | undefined
return filterUndefined({
employeeCount: str(company?.employee_count) || undefined,
description: str(company?.summary) || undefined,
})
},
}),
toolProvider({
id: 'datagma',
label: 'Datagma',
toolId: 'datagma_enrich_company',
buildParams: (inputs) => {
const data = normalizeDomain(inputs.domain)
if (!data) return null
return { data }
},
mapOutput: (output) => {
return filterUndefined({
employeeCount: str(output.companySize) || undefined,
description: str(output.shortDescription) || undefined,
})
},
}),
toolProvider({
id: 'leadmagic',
label: 'LeadMagic',
toolId: 'leadmagic_company_search',
id: 'firecrawl',
label: 'Firecrawl',
toolId: 'firecrawl_enrich',
buildParams: (inputs) => {
const companyDomain = normalizeDomain(inputs.domain)
if (!companyDomain) return null
return { company_domain: companyDomain }
return companyDomain ? { enrichmentType: 'company', companyDomain } : null
},
mapOutput: (output) => {
// Prefer the headcount range to match Hunter's bucket style; fall back to the exact count.
return filterUndefined({
employeeCount: str(output.employeeRange) || str(output.employeeCount) || undefined,
employeeCount: str(output.employeeCount) || undefined,
description: str(output.description) || undefined,
})
},
Expand Down
Loading
Loading