Skip to content

Commit b3c0c77

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat/org-home-redesign
2 parents 049e591 + 8e3df8a commit b3c0c77

36 files changed

Lines changed: 1849 additions & 965 deletions

‎.agents/skills/add-enrichment/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Because enrichments run on Sim's hosted keys by default, **every provider tool y
2222

2323
## Architecture (what you're plugging into)
2424

25-
- **`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.
26-
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `firstNonEmpty(arr)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
25+
- **`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.
26+
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
2727
- **`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.
2828
- **`enrichments/registry.ts`** — `ENRICHMENT_REGISTRY` / `ALL_ENRICHMENTS` / `getEnrichment`. Register new entries here.
2929

@@ -111,7 +111,7 @@ export { myEnrichment } from './my-enrichment'
111111
```
112112

113113
Rules:
114-
- 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.
114+
- 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.
115115
- `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).
116116
- Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config).
117117

‎apps/docs/content/docs/integrations/firecrawl.mdx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,38 @@ Retrieve the remaining and allocated Firecrawl credits for the team.
454454

455455
</ReferenceDetails>
456456

457+
### Firecrawl Enrich
458+
459+
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.
460+
461+
<ReferenceDetails>
462+
463+
#### Input
464+
465+
| Parameter | Type | Required | Description |
466+
| --------- | ---- | -------- | ----------- |
467+
| `apiKey` | string | Yes | Firecrawl API key |
468+
| `enrichmentType` | string | Yes | Lookup type: work_email, phone, linkedin, company, or company_domain |
469+
| `fullName` | string | No | Full name of the person |
470+
| `companyDomain` | string | No | Company website domain |
471+
| `companyName` | string | No | Company name when the domain is unknown |
472+
| `linkedinUrl` | string | No | LinkedIn profile URL for work email or mobile lookup |
473+
| `email` | string | No | Email address for reverse LinkedIn profile lookup |
474+
475+
#### Output
476+
477+
| Parameter | Type | Description |
478+
| --------- | ---- | ----------- |
479+
| `email` | string | Work email, when found |
480+
| `emailVerified` | boolean | True only when FullEnrich reports DELIVERABLE; catch-all and high-probability addresses remain unverified |
481+
| `phone` | string | Phone number, preferring mobile when available |
482+
| `linkedinUrl` | string | LinkedIn profile URL, when found |
483+
| `domain` | string | Company website domain, when found |
484+
| `employeeCount` | string | Company headcount or size range |
485+
| `description` | string | Company description |
486+
| `creditsUsed` | number | Firecrawl credits charged for this lookup |
487+
| `statusCode` | number | Provider or HTTP response status code |
488+
489+
</ReferenceDetails>
490+
457491

‎apps/sim/blocks/blocks/firecrawl.ts‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const FirecrawlBlock: BlockConfig = {
5353
],
5454
parse: [{ text: 'Parse', field: DOCUMENT_FIELD, core: true, after: 'into markdown' }],
5555
credit_usage: ['Read remaining credit balance'],
56+
enrich: [{ text: 'Look up', field: 'enrichmentType', core: true }],
5657
},
5758
},
5859
},
@@ -75,9 +76,93 @@ export const FirecrawlBlock: BlockConfig = {
7576
{ label: 'Agent', id: 'agent' },
7677
{ label: 'Parse Document', id: 'parse' },
7778
{ label: 'Credit Usage', id: 'credit_usage' },
79+
{ label: 'Enrich Contact or Company', id: 'enrich' },
7880
],
7981
value: () => 'scrape',
8082
},
83+
{
84+
id: 'enrichmentType',
85+
title: 'Enrichment Type',
86+
type: 'dropdown',
87+
options: [
88+
{ label: 'Work Email', id: 'work_email' },
89+
{ label: 'Phone Number', id: 'phone' },
90+
{ label: 'LinkedIn Profile', id: 'linkedin' },
91+
{ label: 'Company Information', id: 'company' },
92+
{ label: 'Company Domain', id: 'company_domain' },
93+
],
94+
value: () => 'work_email',
95+
condition: { field: 'operation', value: 'enrich' },
96+
required: true,
97+
},
98+
{
99+
id: 'fullName',
100+
title: 'Full Name',
101+
type: 'short-input',
102+
placeholder: 'Full name of the person',
103+
condition: {
104+
field: 'operation',
105+
value: 'enrich',
106+
and: { field: 'enrichmentType', value: ['work_email', 'phone', 'linkedin'] },
107+
},
108+
},
109+
{
110+
id: 'companyDomain',
111+
title: 'Company Domain',
112+
type: 'short-input',
113+
placeholder: 'example.com',
114+
required: {
115+
field: 'operation',
116+
value: 'enrich',
117+
and: { field: 'enrichmentType', value: 'company' },
118+
},
119+
condition: {
120+
field: 'operation',
121+
value: 'enrich',
122+
and: { field: 'enrichmentType', value: ['work_email', 'phone', 'linkedin', 'company'] },
123+
},
124+
},
125+
{
126+
id: 'companyName',
127+
title: 'Company Name',
128+
type: 'short-input',
129+
placeholder: 'Company name',
130+
required: {
131+
field: 'operation',
132+
value: 'enrich',
133+
and: { field: 'enrichmentType', value: 'company_domain' },
134+
},
135+
condition: {
136+
field: 'operation',
137+
value: 'enrich',
138+
and: {
139+
field: 'enrichmentType',
140+
value: ['work_email', 'phone', 'linkedin', 'company_domain'],
141+
},
142+
},
143+
},
144+
{
145+
id: 'linkedinUrl',
146+
title: 'LinkedIn Profile URL',
147+
type: 'short-input',
148+
placeholder: 'https://www.linkedin.com/in/...',
149+
condition: {
150+
field: 'operation',
151+
value: 'enrich',
152+
and: { field: 'enrichmentType', value: ['work_email', 'phone'] },
153+
},
154+
},
155+
{
156+
id: 'email',
157+
title: 'Email',
158+
type: 'short-input',
159+
placeholder: 'Email address for reverse lookup',
160+
condition: {
161+
field: 'operation',
162+
value: 'enrich',
163+
and: { field: 'enrichmentType', value: ['linkedin'] },
164+
},
165+
},
81166
{
82167
id: 'fileUpload',
83168
title: 'Document',
@@ -470,6 +555,7 @@ Example 2 - Product Data:
470555
'firecrawl_agent',
471556
'firecrawl_parse',
472557
'firecrawl_credit_usage',
558+
'firecrawl_enrich',
473559
],
474560
config: {
475561
tool: (params) => {
@@ -500,6 +586,8 @@ Example 2 - Product Data:
500586
return 'firecrawl_parse'
501587
case 'credit_usage':
502588
return 'firecrawl_credit_usage'
589+
case 'enrich':
590+
return 'firecrawl_enrich'
503591
default:
504592
return 'firecrawl_scrape'
505593
}
@@ -714,6 +802,15 @@ Example 2 - Product Data:
714802
if (jobId) result.jobId = jobId
715803
break
716804

805+
case 'enrich':
806+
result.enrichmentType = params.enrichmentType
807+
result.fullName = params.fullName
808+
result.companyDomain = params.companyDomain
809+
result.companyName = params.companyName
810+
result.linkedinUrl = params.linkedinUrl
811+
result.email = params.email
812+
break
813+
717814
case 'credit_usage':
718815
break
719816
}
@@ -723,6 +820,12 @@ Example 2 - Product Data:
723820
},
724821
},
725822
inputs: {
823+
enrichmentType: { type: 'string', description: 'Contact or company lookup type' },
824+
fullName: { type: 'string', description: 'Full name of the person' },
825+
companyDomain: { type: 'string', description: 'Company website domain' },
826+
companyName: { type: 'string', description: 'Company name' },
827+
linkedinUrl: { type: 'string', description: 'LinkedIn profile URL' },
828+
email: { type: 'string', description: 'Email address for reverse lookup' },
726829
apiKey: { type: 'string', description: 'Firecrawl API key' },
727830
operation: { type: 'string', description: 'Operation to perform' },
728831
url: { type: 'string', description: 'Target website URL' },
@@ -775,6 +878,14 @@ Example 2 - Product Data:
775878
zeroDataRetention: { type: 'boolean', description: 'Enable zero data retention' },
776879
},
777880
outputs: {
881+
email: { type: 'string', description: 'Work email, when found' },
882+
emailVerified: { type: 'boolean', description: 'Whether the work email is mailbox-verified' },
883+
phone: { type: 'string', description: 'Phone number, preferring mobile when available' },
884+
linkedinUrl: { type: 'string', description: 'LinkedIn profile URL, when found' },
885+
domain: { type: 'string', description: 'Company website domain, when found' },
886+
employeeCount: { type: 'string', description: 'Company headcount or size range' },
887+
description: { type: 'string', description: 'Company description' },
888+
statusCode: { type: 'number', description: 'Enrichment provider or HTTP response status code' },
778889
// Scrape output
779890
markdown: { type: 'string', description: 'Page content markdown' },
780891
html: { type: 'string', description: 'Raw HTML content' },

‎apps/sim/enrichments/company-domain/company-domain.ts‎

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import { Globe } from '@sim/emcn/icons'
22
import { normalizeDomain, str, toolProvider } from '@/enrichments/providers'
33
import type { EnrichmentConfig } from '@/enrichments/types'
44

5-
/**
6-
* Company Domain enrichment. Resolves a company's website domain from its name
7-
* via a People Data Labs company match, falling back to Datagma's company enrich.
8-
*/
95
export const companyDomainEnrichment: EnrichmentConfig = {
106
id: 'company-domain',
117
name: 'Company Domain',
@@ -15,34 +11,15 @@ export const companyDomainEnrichment: EnrichmentConfig = {
1511
outputs: [{ id: 'domain', name: 'domain', type: 'string' }],
1612
providers: [
1713
toolProvider({
18-
id: 'pdl',
19-
label: 'People Data Labs',
20-
toolId: 'pdl_company_enrich',
14+
id: 'firecrawl',
15+
label: 'Firecrawl',
16+
toolId: 'firecrawl_enrich',
2117
buildParams: (inputs) => {
22-
const name = str(inputs.companyName)
23-
if (!name) return null
24-
// `required` makes PDL 404 (free) when the match has no website,
25-
// instead of charging a credit for a match we'd discard as a no-match.
26-
return { name, required: 'website' }
18+
const companyName = str(inputs.companyName)
19+
return companyName ? { enrichmentType: 'company_domain', companyName } : null
2720
},
2821
mapOutput: (output) => {
29-
const company = output.company as Record<string, unknown> | undefined
30-
const domain = normalizeDomain(company?.website)
31-
return domain ? { domain } : null
32-
},
33-
}),
34-
toolProvider({
35-
id: 'datagma',
36-
label: 'Datagma',
37-
toolId: 'datagma_enrich_company',
38-
buildParams: (inputs) => {
39-
// Datagma's `data` accepts a company name and returns its website.
40-
const data = str(inputs.companyName)
41-
if (!data) return null
42-
return { data }
43-
},
44-
mapOutput: (output) => {
45-
const domain = normalizeDomain(output.website)
22+
const domain = normalizeDomain(output.domain)
4623
return domain ? { domain } : null
4724
},
4825
}),

‎apps/sim/enrichments/company-info/company-info.ts‎

Lines changed: 5 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ import { filterUndefined } from '@sim/utils/object'
33
import { normalizeDomain, str, toolProvider } from '@/enrichments/providers'
44
import type { EnrichmentConfig } from '@/enrichments/types'
55

6-
/**
7-
* Company Info enrichment. Looks up a company by domain, trying Hunter first
8-
* (free) then People Data Labs, then Datagma and LeadMagic as fallbacks. Outputs
9-
* are limited to the fields the providers reliably return — employee count and
10-
* description — so the result stays consistent regardless of which provider fills
11-
* the cell. `employeeCount` is a string so Hunter's range bucket (e.g. `"11-50"`),
12-
* PDL's exact count, and LeadMagic's range all map onto the same column.
13-
*/
146
export const companyInfoEnrichment: EnrichmentConfig = {
157
id: 'company-info',
168
name: 'Company Info',
@@ -23,69 +15,16 @@ export const companyInfoEnrichment: EnrichmentConfig = {
2315
],
2416
providers: [
2517
toolProvider({
26-
id: 'hunter',
27-
label: 'Hunter',
28-
toolId: 'hunter_companies_find',
29-
buildParams: (inputs) => {
30-
const domain = normalizeDomain(inputs.domain)
31-
if (!domain) return null
32-
return { domain }
33-
},
34-
mapOutput: (output) => {
35-
return filterUndefined({
36-
employeeCount: str(output.size) || undefined,
37-
description: str(output.description) || undefined,
38-
})
39-
},
40-
}),
41-
toolProvider({
42-
id: 'pdl',
43-
label: 'People Data Labs',
44-
toolId: 'pdl_company_enrich',
45-
buildParams: (inputs) => {
46-
const website = normalizeDomain(inputs.domain)
47-
if (!website) return null
48-
// `required` makes PDL 404 (free) when neither field we extract is
49-
// present, instead of charging a credit for a match we'd discard.
50-
return { website, required: 'employee_count OR summary' }
51-
},
52-
mapOutput: (output) => {
53-
const company = output.company as Record<string, unknown> | undefined
54-
return filterUndefined({
55-
employeeCount: str(company?.employee_count) || undefined,
56-
description: str(company?.summary) || undefined,
57-
})
58-
},
59-
}),
60-
toolProvider({
61-
id: 'datagma',
62-
label: 'Datagma',
63-
toolId: 'datagma_enrich_company',
64-
buildParams: (inputs) => {
65-
const data = normalizeDomain(inputs.domain)
66-
if (!data) return null
67-
return { data }
68-
},
69-
mapOutput: (output) => {
70-
return filterUndefined({
71-
employeeCount: str(output.companySize) || undefined,
72-
description: str(output.shortDescription) || undefined,
73-
})
74-
},
75-
}),
76-
toolProvider({
77-
id: 'leadmagic',
78-
label: 'LeadMagic',
79-
toolId: 'leadmagic_company_search',
18+
id: 'firecrawl',
19+
label: 'Firecrawl',
20+
toolId: 'firecrawl_enrich',
8021
buildParams: (inputs) => {
8122
const companyDomain = normalizeDomain(inputs.domain)
82-
if (!companyDomain) return null
83-
return { company_domain: companyDomain }
23+
return companyDomain ? { enrichmentType: 'company', companyDomain } : null
8424
},
8525
mapOutput: (output) => {
86-
// Prefer the headcount range to match Hunter's bucket style; fall back to the exact count.
8726
return filterUndefined({
88-
employeeCount: str(output.employeeRange) || str(output.employeeCount) || undefined,
27+
employeeCount: str(output.employeeCount) || undefined,
8928
description: str(output.description) || undefined,
9029
})
9130
},

0 commit comments

Comments
 (0)