From 2fbe1fcf9e7a003fba61570753c2602fb880e445 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 19:46:08 -0700 Subject: [PATCH 01/13] feat(peopledatalabs): add People Data Labs integration Add 11 PDL operations: person enrich/identify/search/bulk, company enrich/search/bulk/clean, location/school cleaners, and autocomplete. All endpoints, params, and response shapes verified against official PDL docs (scroll_token pagination, top-level likelihood on company enrich, per-item likelihood on bulk company, full autocomplete field enum). Co-Authored-By: Claude Opus 4.7 --- apps/docs/components/icons.tsx | 18 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + .../content/docs/en/tools/peopledatalabs.mdx | 70 +++ .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 63 ++ apps/sim/blocks/blocks/peopledatalabs.ts | 549 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 18 + apps/sim/tools/peopledatalabs/autocomplete.ts | 95 +++ .../peopledatalabs/bulk_company_enrich.ts | 112 ++++ .../peopledatalabs/bulk_person_enrich.ts | 112 ++++ .../sim/tools/peopledatalabs/company_clean.ts | 89 +++ .../tools/peopledatalabs/company_enrich.ts | 150 +++++ .../tools/peopledatalabs/company_search.ts | 110 ++++ apps/sim/tools/peopledatalabs/index.ts | 23 + .../tools/peopledatalabs/location_clean.ts | 71 +++ .../sim/tools/peopledatalabs/person_enrich.ts | 153 +++++ .../tools/peopledatalabs/person_identify.ts | 211 +++++++ .../sim/tools/peopledatalabs/person_search.ts | 111 ++++ apps/sim/tools/peopledatalabs/school_clean.ts | 89 +++ apps/sim/tools/peopledatalabs/types.ts | 477 +++++++++++++++ apps/sim/tools/peopledatalabs/utils.ts | 145 +++++ apps/sim/tools/registry.ts | 24 + 24 files changed, 2697 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/peopledatalabs.mdx create mode 100644 apps/sim/blocks/blocks/peopledatalabs.ts create mode 100644 apps/sim/tools/peopledatalabs/autocomplete.ts create mode 100644 apps/sim/tools/peopledatalabs/bulk_company_enrich.ts create mode 100644 apps/sim/tools/peopledatalabs/bulk_person_enrich.ts create mode 100644 apps/sim/tools/peopledatalabs/company_clean.ts create mode 100644 apps/sim/tools/peopledatalabs/company_enrich.ts create mode 100644 apps/sim/tools/peopledatalabs/company_search.ts create mode 100644 apps/sim/tools/peopledatalabs/index.ts create mode 100644 apps/sim/tools/peopledatalabs/location_clean.ts create mode 100644 apps/sim/tools/peopledatalabs/person_enrich.ts create mode 100644 apps/sim/tools/peopledatalabs/person_identify.ts create mode 100644 apps/sim/tools/peopledatalabs/person_search.ts create mode 100644 apps/sim/tools/peopledatalabs/school_clean.ts create mode 100644 apps/sim/tools/peopledatalabs/types.ts create mode 100644 apps/sim/tools/peopledatalabs/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4092f8c10a..dbc8b6638b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -895,6 +895,24 @@ export function YouTubeIcon(props: React.SVGProps) { ) } +export function PeopleDataLabsIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index b515c6ccdd..609f37382d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -135,6 +135,7 @@ import { PackageSearchIcon, PagerDutyIcon, ParallelIcon, + PeopleDataLabsIcon, PerplexityIcon, PineconeIcon, PipedriveIcon, @@ -351,6 +352,7 @@ export const blockTypeToIconMap: Record = { outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, + peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index af967f765a..ad0f6b437a 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -131,6 +131,7 @@ "outlook", "pagerduty", "parallel_ai", + "peopledatalabs", "perplexity", "pinecone", "pipedrive", diff --git a/apps/docs/content/docs/en/tools/peopledatalabs.mdx b/apps/docs/content/docs/en/tools/peopledatalabs.mdx new file mode 100644 index 0000000000..ace546acde --- /dev/null +++ b/apps/docs/content/docs/en/tools/peopledatalabs.mdx @@ -0,0 +1,70 @@ +--- +title: People Data Labs +description: Enrich and search people and companies +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +## Description + +[People Data Labs](https://www.peopledatalabs.com/) is a person and company data provider with a global dataset of 3B+ person profiles and 25M+ company records. The Sim block exposes the core REST endpoints so agents can enrich a single contact, look up a company, or run dataset-wide search queries. + +Use this block to: +- **Person Enrich** — resolve a single person by email, phone, LinkedIn URL, or name + company/location, and pull back their work history, contact info, and skills. +- **Person Search** — run SQL or Elasticsearch DSL queries against the person dataset to build prospect lists or audience segments. +- **Company Enrich** — resolve a single company by name, website, ticker, LinkedIn URL, or PDL ID, and pull back firmographics. +- **Company Search** — query the company dataset with SQL or Elasticsearch DSL. +- **Autocomplete** — get suggested values for fields like `title`, `skill`, `industry`, or `location` to build well-formed search queries. + +Authentication uses an API key passed as the `X-Api-Key` header. Get a key from the [PDL dashboard](https://dashboard.peopledatalabs.com/). +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene. + + + +## Tools + +### `pdl_person_enrich` + + +### `pdl_person_identify` + + +### `pdl_person_search` + + +### `pdl_bulk_person_enrich` + + +### `pdl_company_enrich` + + +### `pdl_company_search` + + +### `pdl_bulk_company_enrich` + + +### `pdl_clean_company` + + +### `pdl_clean_location` + + +### `pdl_clean_school` + + +### `pdl_autocomplete` + + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index e19d3d267b..f80e850ceb 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -135,6 +135,7 @@ import { PackageSearchIcon, PagerDutyIcon, ParallelIcon, + PeopleDataLabsIcon, PerplexityIcon, PineconeIcon, PipedriveIcon, @@ -335,6 +336,7 @@ export const blockTypeToIconMap: Record = { outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, + peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 9de36c2890..b443d02090 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -9715,6 +9715,69 @@ "integrationTypes": ["search", "ai"], "tags": ["web-scraping", "llm", "agentic"] }, + { + "type": "peopledatalabs", + "slug": "people-data-labs", + "name": "People Data Labs", + "description": "Enrich and search people and companies", + "longDescription": "Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene.", + "bgColor": "#4831C3", + "iconName": "PeopleDataLabsIcon", + "docsUrl": "https://docs.sim.ai/tools/peopledatalabs", + "operations": [ + { + "name": "Person Enrich", + "description": "Enrich a single person profile using People Data Labs. Match by email, phone, LinkedIn URL, or name + company/location. Returns work history, contact details, location, and skills." + }, + { + "name": "Person Identify", + "description": "Return up to 20 candidate person matches with confidence scores. Useful when you want to see all plausible matches rather than the single best one. Reference: https://docs.peopledatalabs.com/docs/identify-api-quickstart" + }, + { + "name": "Person Search", + "description": "Search the People Data Labs person dataset using SQL or Elasticsearch DSL. Returns up to 100 matching records per call." + }, + { + "name": "Bulk Person Enrich", + "description": "Enrich up to 100 person records in a single call. Provide a JSON array of request objects, each with a `params` object (and optional `metadata` echoed back). Reference: https://docs.peopledatalabs.com/docs/bulk-person-enrichment-api" + }, + { + "name": "Company Enrich", + "description": "Enrich a single company using People Data Labs. Match by name, website, LinkedIn URL, ticker, or PDL ID." + }, + { + "name": "Company Search", + "description": "Search the People Data Labs company dataset using SQL or Elasticsearch DSL. Returns up to 100 matching companies per call." + }, + { + "name": "Bulk Company Enrich", + "description": "Enrich up to 100 companies in a single call. Provide a JSON array of request objects, each with a `params` object. Reference: https://docs.peopledatalabs.com/docs/bulk-company-enrichment-api" + }, + { + "name": "Company Cleaner", + "description": "Normalize a company string into a canonical company record. Provide at least one of name, website, or profile (LinkedIn URL)." + }, + { + "name": "Location Cleaner", + "description": "Normalize a freeform location string into a structured locality/region/country record with coordinates." + }, + { + "name": "School Cleaner", + "description": "Normalize a school string into a canonical school record. Provide at least one of name, website, or profile (LinkedIn URL)." + }, + { + "name": "Autocomplete", + "description": "Get autocomplete suggestions for a PDL field (title, skill, company, industry, location, school, major, role, sub_role)." + } + ], + "operationCount": 11, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment"] + }, { "type": "perplexity", "slug": "perplexity", diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts new file mode 100644 index 0000000000..7b5f192aff --- /dev/null +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -0,0 +1,549 @@ +import { PeopleDataLabsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { PdlPersonEnrichResponse } from '@/tools/peopledatalabs/types' + +export const PeopleDataLabsBlock: BlockConfig = { + type: 'peopledatalabs', + name: 'People Data Labs', + description: 'Enrich and search people and companies', + authMode: AuthMode.ApiKey, + longDescription: + 'Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene.', + docsLink: 'https://docs.sim.ai/tools/peopledatalabs', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment'], + bgColor: '#4831C3', + icon: PeopleDataLabsIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Person Enrich', id: 'pdl_person_enrich' }, + { label: 'Person Identify', id: 'pdl_person_identify' }, + { label: 'Person Search', id: 'pdl_person_search' }, + { label: 'Bulk Person Enrich', id: 'pdl_bulk_person_enrich' }, + { label: 'Company Enrich', id: 'pdl_company_enrich' }, + { label: 'Company Search', id: 'pdl_company_search' }, + { label: 'Bulk Company Enrich', id: 'pdl_bulk_company_enrich' }, + { label: 'Company Cleaner', id: 'pdl_clean_company' }, + { label: 'Location Cleaner', id: 'pdl_clean_location' }, + { label: 'School Cleaner', id: 'pdl_clean_school' }, + { label: 'Autocomplete', id: 'pdl_autocomplete' }, + ], + value: () => 'pdl_person_enrich', + }, + + // Person Enrich fields + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'jane@example.com', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + }, + { + id: 'profile', + title: 'LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/in/janedoe', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+15551234567', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'first_name', + title: 'First Name', + type: 'short-input', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'last_name', + title: 'Last Name', + type: 'short-input', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'company', + title: 'Company', + type: 'short-input', + placeholder: 'Acme Inc or acme.com', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'location', + title: 'Location', + type: 'short-input', + placeholder: 'San Francisco, CA', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'min_likelihood', + title: 'Min Likelihood', + type: 'short-input', + placeholder: '6', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + + // Person Search fields + { + id: 'sql', + title: 'SQL Query', + type: 'long-input', + placeholder: + "SELECT * FROM person WHERE job_title='engineer' AND location_country='united states'", + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + }, + { + id: 'query', + title: 'Elasticsearch Query (JSON)', + type: 'long-input', + placeholder: '{"bool": {"must": [{"term": {"job_title": "engineer"}}]}}', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'size', + title: 'Result Size', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'scroll_token', + title: 'Scroll Token', + type: 'short-input', + placeholder: 'Token from a prior response', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'dataset', + title: 'Dataset', + type: 'dropdown', + options: [ + { label: 'all', id: 'all' }, + { label: 'resume', id: 'resume' }, + { label: 'email', id: 'email' }, + { label: 'phone', id: 'phone' }, + { label: 'mobile_phone', id: 'mobile_phone' }, + { label: 'street_address', id: 'street_address' }, + { label: 'consumer_social', id: 'consumer_social' }, + { label: 'developer', id: 'developer' }, + ], + condition: { field: 'operation', value: 'pdl_person_search' }, + mode: 'advanced', + }, + + // Company Enrich fields + { + id: 'name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Acme Inc', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + }, + { + id: 'website', + title: 'Website', + type: 'short-input', + placeholder: 'acme.com', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + }, + { + id: 'company_profile', + title: 'LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/company/acme', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + mode: 'advanced', + }, + { + id: 'ticker', + title: 'Ticker', + type: 'short-input', + placeholder: 'AAPL', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + mode: 'advanced', + }, + { + id: 'pdl_id', + title: 'PDL Company ID', + type: 'short-input', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + mode: 'advanced', + }, + { + id: 'company_location', + title: 'Location', + type: 'short-input', + placeholder: 'San Francisco, CA', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + mode: 'advanced', + }, + + // Autocomplete fields + { + id: 'field', + title: 'Field', + type: 'dropdown', + options: [ + { label: 'title', id: 'title' }, + { label: 'skill', id: 'skill' }, + { label: 'company', id: 'company' }, + { label: 'industry', id: 'industry' }, + { label: 'location_name', id: 'location_name' }, + { label: 'all_location', id: 'all_location' }, + { label: 'country', id: 'country' }, + { label: 'region', id: 'region' }, + { label: 'school', id: 'school' }, + { label: 'major', id: 'major' }, + { label: 'class', id: 'class' }, + { label: 'role', id: 'role' }, + { label: 'sub_role', id: 'sub_role' }, + { label: 'website', id: 'website' }, + ], + value: () => 'title', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + required: { field: 'operation', value: 'pdl_autocomplete' }, + }, + { + id: 'text', + title: 'Search Text', + type: 'short-input', + placeholder: 'engin', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + }, + { + id: 'autocomplete_size', + title: 'Number of Suggestions', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + mode: 'advanced', + }, + + // Person Identify-only fields + { + id: 'identify_locality', + title: 'Locality (City)', + type: 'short-input', + placeholder: 'San Francisco', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_region', + title: 'Region (State)', + type: 'short-input', + placeholder: 'CA', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_country', + title: 'Country', + type: 'short-input', + placeholder: 'United States', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_postal_code', + title: 'Postal Code', + type: 'short-input', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_birth_date', + title: 'Birth Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'data_include', + title: 'Data Include', + type: 'short-input', + placeholder: 'work_email,personal_emails,phone_numbers', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'include_if_matched', + title: 'Include `matched_on`', + type: 'switch', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + + // Bulk Person Enrich + { + id: 'bulk_person_requests', + title: 'Requests (JSON Array)', + type: 'long-input', + placeholder: + '[{ "params": { "profile": "https://linkedin.com/in/janedoe" } }, { "params": { "email": "john@example.com" } }]', + condition: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + required: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + }, + { + id: 'bulk_person_required', + title: 'Required Fields', + type: 'short-input', + placeholder: 'emails AND job_title', + condition: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + mode: 'advanced', + }, + + // Bulk Company Enrich + { + id: 'bulk_company_requests', + title: 'Requests (JSON Array)', + type: 'long-input', + placeholder: '[{ "params": { "website": "acme.com" } }, { "params": { "name": "Globex" } }]', + condition: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + required: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + }, + { + id: 'bulk_company_required', + title: 'Required Fields', + type: 'short-input', + placeholder: 'name AND website', + condition: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + mode: 'advanced', + }, + + // Location Cleaner + { + id: 'clean_location_input', + title: 'Location', + type: 'short-input', + placeholder: 'SF, CA', + condition: { field: 'operation', value: 'pdl_clean_location' }, + required: { field: 'operation', value: 'pdl_clean_location' }, + }, + + // School Cleaner + { + id: 'school_name', + title: 'School Name', + type: 'short-input', + placeholder: 'Stanford University', + condition: { field: 'operation', value: 'pdl_clean_school' }, + }, + { + id: 'school_website', + title: 'School Website', + type: 'short-input', + placeholder: 'stanford.edu', + condition: { field: 'operation', value: 'pdl_clean_school' }, + }, + { + id: 'school_profile', + title: 'School LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/school/stanford-university', + condition: { field: 'operation', value: 'pdl_clean_school' }, + mode: 'advanced', + }, + + // API Key + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your People Data Labs API key', + password: true, + required: true, + }, + ], + + tools: { + access: [ + 'pdl_person_enrich', + 'pdl_person_identify', + 'pdl_person_search', + 'pdl_bulk_person_enrich', + 'pdl_company_enrich', + 'pdl_company_search', + 'pdl_bulk_company_enrich', + 'pdl_clean_company', + 'pdl_clean_location', + 'pdl_clean_school', + 'pdl_autocomplete', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'pdl_person_enrich': + case 'pdl_person_identify': + case 'pdl_person_search': + case 'pdl_bulk_person_enrich': + case 'pdl_company_enrich': + case 'pdl_company_search': + case 'pdl_bulk_company_enrich': + case 'pdl_clean_company': + case 'pdl_clean_location': + case 'pdl_clean_school': + case 'pdl_autocomplete': + return params.operation + default: + return 'pdl_person_enrich' + } + }, + params: (params) => { + const result: Record = { ...params } + + if (params.company_profile !== undefined) { + result.profile = params.company_profile + result.company_profile = undefined + } + if (params.company_location !== undefined) { + result.location = params.company_location + result.company_location = undefined + } + if (params.autocomplete_size !== undefined) { + result.size = Number(params.autocomplete_size) + result.autocomplete_size = undefined + } + if (params.size !== undefined) { + result.size = Number(params.size) + } + if (params.min_likelihood !== undefined) { + result.min_likelihood = Number(params.min_likelihood) + } + + // Person Identify field renames + if (params.identify_locality !== undefined) { + result.locality = params.identify_locality + result.identify_locality = undefined + } + if (params.identify_region !== undefined) { + result.region = params.identify_region + result.identify_region = undefined + } + if (params.identify_country !== undefined) { + result.country = params.identify_country + result.identify_country = undefined + } + if (params.identify_postal_code !== undefined) { + result.postal_code = params.identify_postal_code + result.identify_postal_code = undefined + } + if (params.identify_birth_date !== undefined) { + result.birth_date = params.identify_birth_date + result.identify_birth_date = undefined + } + + // Bulk renames + if (params.bulk_person_requests !== undefined) { + result.requests = params.bulk_person_requests + result.bulk_person_requests = undefined + } + if (params.bulk_person_required !== undefined) { + result.required = params.bulk_person_required + result.bulk_person_required = undefined + } + if (params.bulk_company_requests !== undefined) { + result.requests = params.bulk_company_requests + result.bulk_company_requests = undefined + } + if (params.bulk_company_required !== undefined) { + result.required = params.bulk_company_required + result.bulk_company_required = undefined + } + + // Cleaner renames + if (params.clean_location_input !== undefined) { + result.location = params.clean_location_input + result.clean_location_input = undefined + } + if (params.school_name !== undefined) { + result.name = params.school_name + result.school_name = undefined + } + if (params.school_website !== undefined) { + result.website = params.school_website + result.school_website = undefined + } + if (params.school_profile !== undefined) { + result.profile = params.school_profile + result.school_profile = undefined + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'People Data Labs API key' }, + // Person enrich + email: { type: 'string', description: 'Email address' }, + profile: { type: 'string', description: 'LinkedIn URL' }, + phone: { type: 'string', description: 'Phone number' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + company: { type: 'string', description: 'Company name or domain' }, + location: { type: 'string', description: 'Location' }, + min_likelihood: { type: 'number', description: 'Minimum match likelihood (1-10)' }, + // Search + sql: { type: 'string', description: 'PDL SQL query' }, + query: { type: 'string', description: 'Elasticsearch DSL query as JSON string' }, + size: { type: 'number', description: 'Result size' }, + scroll_token: { type: 'string', description: 'Pagination token from a prior response' }, + dataset: { type: 'string', description: 'Person dataset filter' }, + // Company enrich + name: { type: 'string', description: 'Company name' }, + website: { type: 'string', description: 'Company website' }, + ticker: { type: 'string', description: 'Stock ticker' }, + pdl_id: { type: 'string', description: 'PDL company ID' }, + // Autocomplete + field: { type: 'string', description: 'Autocomplete field' }, + text: { type: 'string', description: 'Search text' }, + // Identify + locality: { type: 'string', description: 'City (identify)' }, + region: { type: 'string', description: 'State/region (identify)' }, + country: { type: 'string', description: 'Country (identify)' }, + postal_code: { type: 'string', description: 'Postal code (identify)' }, + birth_date: { type: 'string', description: 'Birth date YYYY-MM-DD (identify)' }, + data_include: { type: 'string', description: 'Fields to include in identify match' }, + include_if_matched: { type: 'boolean', description: 'Include `matched_on` array per match' }, + // Bulk + requests: { type: 'string', description: 'JSON array of bulk request objects' }, + required: { type: 'string', description: 'Required-fields expression for bulk' }, + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a record was matched (enrich/clean)' }, + likelihood: { type: 'number', description: 'Match likelihood (person enrich)' }, + person: { type: 'json', description: 'Matched person record' }, + company: { type: 'json', description: 'Matched company record' }, + location: { type: 'json', description: 'Cleaned location record' }, + school: { type: 'json', description: 'Cleaned school record' }, + matches: { type: 'json', description: 'Identify match candidates with scores' }, + total: { type: 'number', description: 'Total matches in dataset (search)' }, + scroll_token: { type: 'string', description: 'Pagination token to fetch the next page' }, + results: { type: 'json', description: 'Search or bulk result records' }, + suggestions: { type: 'json', description: 'Autocomplete suggestions' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f5d9b40fd3..d458289879 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -148,6 +148,7 @@ import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' import { PagerDutyBlock } from '@/blocks/blocks/pagerduty' import { ParallelBlock } from '@/blocks/blocks/parallel' +import { PeopleDataLabsBlock } from '@/blocks/blocks/peopledatalabs' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' import { PipedriveBlock } from '@/blocks/blocks/pipedrive' @@ -399,6 +400,7 @@ export const registry: Record = { outlook: OutlookBlock, pagerduty: PagerDutyBlock, parallel_ai: ParallelBlock, + peopledatalabs: PeopleDataLabsBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, pipedrive: PipedriveBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 4092f8c10a..dbc8b6638b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -895,6 +895,24 @@ export function YouTubeIcon(props: React.SVGProps) { ) } +export function PeopleDataLabsIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/peopledatalabs/autocomplete.ts b/apps/sim/tools/peopledatalabs/autocomplete.ts new file mode 100644 index 0000000000..c9306e6461 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/autocomplete.ts @@ -0,0 +1,95 @@ +import type { PdlAutocompleteParams, PdlAutocompleteResponse } from '@/tools/peopledatalabs/types' +import { buildQueryString } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const SUGGESTION_PROPERTIES = { + name: { type: 'string', description: 'Suggestion value' }, + count: { type: 'number', description: 'Number of records matching this value' }, + meta: { + type: 'object', + description: 'Field-specific metadata (e.g., for `company`: id, website, industry)', + optional: true, + }, +} as const satisfies Record + +export const autocompleteTool: ToolConfig = { + id: 'pdl_autocomplete', + name: 'PDL Autocomplete', + description: + 'Get autocomplete suggestions for a PDL field (title, skill, company, industry, location, school, major, role, sub_role).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + field: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Field to autocomplete: all_location, class, company, country, industry, location_name, major, region, role, school, sub_role, skill, title, website', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search text prefix', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of suggestions to return (1-100, default 10)', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + field: params.field, + text: params.text, + size: params.size, + }) + return `https://api.peopledatalabs.com/v5/autocomplete${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = + (data.data as { name: string; count: number; meta?: Record }[]) ?? [] + return { + success: true, + output: { + suggestions: items.map((item) => ({ + name: item.name, + count: item.count ?? 0, + meta: item.meta ?? undefined, + })), + }, + } + }, + + outputs: { + suggestions: { + type: 'array', + description: 'Autocomplete suggestions ordered by frequency', + items: { type: 'object', properties: SUGGESTION_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts b/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts new file mode 100644 index 0000000000..278e8cfdb2 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts @@ -0,0 +1,112 @@ +import type { + PdlBulkCompanyEnrichParams, + PdlBulkCompanyEnrichResponse, + PdlBulkCompanyResultItem, +} from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const BULK_COMPANY_RESULT_PROPERTIES = { + status: { type: 'number', description: 'Per-record HTTP status (200 on match)' }, + matched: { type: 'boolean', description: 'Whether this record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood (1-10), null if no match', + optional: true, + }, + metadata: { + type: 'object', + description: 'Metadata echoed back from the request', + optional: true, + }, + company: { type: 'object', description: 'Matched company record', optional: true }, +} as const satisfies Record + +export const bulkCompanyEnrichTool: ToolConfig< + PdlBulkCompanyEnrichParams, + PdlBulkCompanyEnrichResponse +> = { + id: 'pdl_bulk_company_enrich', + name: 'PDL Bulk Company Enrich', + description: + 'Enrich up to 100 companies in a single call. Provide a JSON array of request objects, each with a `params` object. Reference: https://docs.peopledatalabs.com/docs/bulk-company-enrichment-api', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + requests: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of request objects, max 100. Each item: { "params": { name | website | profile | ticker | pdl_id }, "metadata": {...optional...} }', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression applied globally to every request', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/enrich/bulk', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + let parsed: unknown + try { + parsed = JSON.parse(params.requests) + } catch { + throw new Error('`requests` must be valid JSON (an array of request objects)') + } + if (!Array.isArray(parsed)) { + throw new Error('`requests` must be a JSON array') + } + const body: Record = { requests: parsed } + if (params.required) body.required = params.required + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const error = (data as { error?: { message?: string } })?.error?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = (Array.isArray(data) ? data : []) as Array> + const results: PdlBulkCompanyResultItem[] = items.map((item) => { + const status = (item.status as number) ?? 0 + const record = (item.data as Record) ?? null + return { + status, + likelihood: (item.likelihood as number) ?? null, + matched: status === 200 && record !== null, + metadata: (item.metadata as Record) ?? null, + company: record ? projectCompany(record) : null, + } + }) + + return { success: true, output: { results } } + }, + + outputs: { + results: { + type: 'array', + description: 'Per-record results in the same order as the input requests', + items: { type: 'object', properties: BULK_COMPANY_RESULT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts b/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts new file mode 100644 index 0000000000..8df13c55e2 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts @@ -0,0 +1,112 @@ +import type { + PdlBulkPersonEnrichParams, + PdlBulkPersonEnrichResponse, + PdlBulkPersonResultItem, +} from '@/tools/peopledatalabs/types' +import { projectPerson } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const BULK_PERSON_RESULT_PROPERTIES = { + status: { type: 'number', description: 'Per-record HTTP status (200 on match)' }, + matched: { type: 'boolean', description: 'Whether this record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood (1-10), null if no match', + optional: true, + }, + metadata: { + type: 'object', + description: 'Metadata echoed back from the request', + optional: true, + }, + person: { type: 'object', description: 'Matched person record', optional: true }, +} as const satisfies Record + +export const bulkPersonEnrichTool: ToolConfig< + PdlBulkPersonEnrichParams, + PdlBulkPersonEnrichResponse +> = { + id: 'pdl_bulk_person_enrich', + name: 'PDL Bulk Person Enrich', + description: + 'Enrich up to 100 person records in a single call. Provide a JSON array of request objects, each with a `params` object (and optional `metadata` echoed back). Reference: https://docs.peopledatalabs.com/docs/bulk-person-enrichment-api', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + requests: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of request objects, max 100. Each item: { "params": { email | profile | first_name+last_name+company | ... }, "metadata": {...optional...} }', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression applied globally to every request', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/person/bulk', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + let parsed: unknown + try { + parsed = JSON.parse(params.requests) + } catch { + throw new Error('`requests` must be valid JSON (an array of request objects)') + } + if (!Array.isArray(parsed)) { + throw new Error('`requests` must be a JSON array') + } + const body: Record = { requests: parsed } + if (params.required) body.required = params.required + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const error = (data as { error?: { message?: string } })?.error?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = (Array.isArray(data) ? data : []) as Array> + const results: PdlBulkPersonResultItem[] = items.map((item) => { + const status = (item.status as number) ?? 0 + const record = (item.data as Record) ?? null + return { + status, + matched: status === 200 && record !== null, + likelihood: (item.likelihood as number) ?? null, + metadata: (item.metadata as Record) ?? null, + person: record ? projectPerson(record) : null, + } + }) + + return { success: true, output: { results } } + }, + + outputs: { + results: { + type: 'array', + description: 'Per-record results in the same order as the input requests', + items: { type: 'object', properties: BULK_PERSON_RESULT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_clean.ts b/apps/sim/tools/peopledatalabs/company_clean.ts new file mode 100644 index 0000000000..65aaff4b87 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_clean.ts @@ -0,0 +1,89 @@ +import type { PdlCleanCompanyParams, PdlCleanCompanyResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanCompanyTool: ToolConfig = { + id: 'pdl_clean_company', + name: 'PDL Company Cleaner', + description: + 'Normalize a company string into a canonical company record. Provide at least one of name, website, or profile (LinkedIn URL).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Raw company name to normalize', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn company URL', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name) body.name = params.name + if (params.website) body.website = params.website + if (params.profile) body.profile = params.profile + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, company: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + company: hasFields ? projectCompany(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known company' }, + company: { + type: 'object', + description: 'Canonical company record', + optional: true, + properties: PDL_COMPANY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_enrich.ts b/apps/sim/tools/peopledatalabs/company_enrich.ts new file mode 100644 index 0000000000..66849d98cb --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_enrich.ts @@ -0,0 +1,150 @@ +import type { PdlCompanyEnrichParams, PdlCompanyEnrichResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const companyEnrichTool: ToolConfig = { + id: 'pdl_company_enrich', + name: 'PDL Company Enrich', + description: + 'Enrich a single company using People Data Labs. Match by name, website, LinkedIn URL, ticker, or PDL ID.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website domain', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn company URL', + }, + ticker: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Stock ticker', + }, + pdl_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'PDL company ID', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company location (helps disambiguate)', + }, + locality: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'City', + }, + region: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State/region', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country', + }, + min_likelihood: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum match likelihood (1-10)', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + name: params.name, + website: params.website, + profile: params.profile, + ticker: params.ticker, + pdl_id: params.pdl_id, + location: params.location, + locality: params.locality, + region: params.region, + country: params.country, + min_likelihood: params.min_likelihood, + required: params.required, + }) + return `https://api.peopledatalabs.com/v5/company/enrich${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, likelihood: null, company: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + likelihood: (data.likelihood as number) ?? null, + company: hasFields ? projectCompany(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a company record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood score (1-10), null if no match', + optional: true, + }, + company: { + type: 'object', + description: 'Matched company record', + optional: true, + properties: PDL_COMPANY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_search.ts b/apps/sim/tools/peopledatalabs/company_search.ts new file mode 100644 index 0000000000..524cce29e0 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_search.ts @@ -0,0 +1,110 @@ +import type { PdlCompanySearchParams, PdlCompanySearchResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const companySearchTool: ToolConfig = { + id: 'pdl_company_search', + name: 'PDL Company Search', + description: + 'Search the People Data Labs company dataset using SQL or Elasticsearch DSL. Returns up to 100 matching companies per call.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + sql: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "PDL SQL query (e.g., \"SELECT * FROM company WHERE industry='computer software' AND size='51-200'\")", + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Elasticsearch DSL query as JSON string', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (1-100, default 1)', + }, + scroll_token: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token returned from a prior response', + }, + dataset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dataset filter (e.g., all)', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/search', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.sql) body.sql = params.sql + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch { + body.query = params.query + } + } + if (params.size !== undefined) body.size = Number(params.size) + if (params.scroll_token) body.scroll_token = params.scroll_token + if (params.dataset) body.dataset = params.dataset + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const records = (data.data as Record[]) ?? [] + return { + success: true, + output: { + total: (data.total as number) ?? records.length, + scroll_token: (data.scroll_token as string) ?? null, + results: records.map(projectCompany), + }, + } + }, + + outputs: { + total: { type: 'number', description: 'Total matching companies in dataset' }, + scroll_token: { + type: 'string', + description: 'Pagination token to fetch the next page; null if no more results', + optional: true, + }, + results: { + type: 'array', + description: 'Company records matching the query', + items: { type: 'object', properties: PDL_COMPANY_OUTPUT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/index.ts b/apps/sim/tools/peopledatalabs/index.ts new file mode 100644 index 0000000000..8eee54c37e --- /dev/null +++ b/apps/sim/tools/peopledatalabs/index.ts @@ -0,0 +1,23 @@ +import { autocompleteTool } from '@/tools/peopledatalabs/autocomplete' +import { bulkCompanyEnrichTool } from '@/tools/peopledatalabs/bulk_company_enrich' +import { bulkPersonEnrichTool } from '@/tools/peopledatalabs/bulk_person_enrich' +import { cleanCompanyTool } from '@/tools/peopledatalabs/company_clean' +import { companyEnrichTool } from '@/tools/peopledatalabs/company_enrich' +import { companySearchTool } from '@/tools/peopledatalabs/company_search' +import { cleanLocationTool } from '@/tools/peopledatalabs/location_clean' +import { personEnrichTool } from '@/tools/peopledatalabs/person_enrich' +import { personIdentifyTool } from '@/tools/peopledatalabs/person_identify' +import { personSearchTool } from '@/tools/peopledatalabs/person_search' +import { cleanSchoolTool } from '@/tools/peopledatalabs/school_clean' + +export const pdlAutocompleteTool = autocompleteTool +export const pdlBulkCompanyEnrichTool = bulkCompanyEnrichTool +export const pdlBulkPersonEnrichTool = bulkPersonEnrichTool +export const pdlCleanCompanyTool = cleanCompanyTool +export const pdlCleanLocationTool = cleanLocationTool +export const pdlCleanSchoolTool = cleanSchoolTool +export const pdlCompanyEnrichTool = companyEnrichTool +export const pdlCompanySearchTool = companySearchTool +export const pdlPersonEnrichTool = personEnrichTool +export const pdlPersonIdentifyTool = personIdentifyTool +export const pdlPersonSearchTool = personSearchTool diff --git a/apps/sim/tools/peopledatalabs/location_clean.ts b/apps/sim/tools/peopledatalabs/location_clean.ts new file mode 100644 index 0000000000..e08e7a6f5a --- /dev/null +++ b/apps/sim/tools/peopledatalabs/location_clean.ts @@ -0,0 +1,71 @@ +import type { PdlCleanLocationParams, PdlCleanLocationResponse } from '@/tools/peopledatalabs/types' +import { PDL_LOCATION_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectLocation } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanLocationTool: ToolConfig = { + id: 'pdl_clean_location', + name: 'PDL Location Cleaner', + description: + 'Normalize a freeform location string into a structured locality/region/country record with coordinates.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + location: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Raw location string (e.g., "SF, CA")', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/location/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => ({ location: params.location }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, location: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.locality || data.country + return { + success: true, + output: { + matched: Boolean(hasFields), + location: hasFields ? projectLocation(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known location' }, + location: { + type: 'object', + description: 'Canonical location record', + optional: true, + properties: PDL_LOCATION_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_enrich.ts b/apps/sim/tools/peopledatalabs/person_enrich.ts new file mode 100644 index 0000000000..82d20cd5ad --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_enrich.ts @@ -0,0 +1,153 @@ +import type { PdlPersonEnrichParams, PdlPersonEnrichResponse } from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectPerson } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const personEnrichTool: ToolConfig = { + id: 'pdl_person_enrich', + name: 'PDL Person Enrich', + description: + 'Enrich a single person profile using People Data Labs. Match by email, phone, LinkedIn URL, or name + company/location. Returns work history, contact details, location, and skills.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address to match', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format preferred)', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL', + }, + lid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn numeric ID', + }, + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name (use with last_name + company or location)', + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name or website', + }, + school: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'School name', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location name (city, region, or country)', + }, + min_likelihood: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum match likelihood (1-10)', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression (e.g., "emails AND job_title")', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + email: params.email, + phone: params.phone, + profile: params.profile, + lid: params.lid, + first_name: params.first_name, + last_name: params.last_name, + company: params.company, + school: params.school, + location: params.location, + min_likelihood: params.min_likelihood, + required: params.required, + }) + return `https://api.peopledatalabs.com/v5/person/enrich${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { + success: true, + output: { matched: false, likelihood: null, person: null }, + } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const record = (data.data as Record) ?? null + return { + success: true, + output: { + matched: record !== null, + likelihood: (data.likelihood as number) ?? null, + person: record ? projectPerson(record) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a person record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood score (1-10), null if no match', + optional: true, + }, + person: { + type: 'object', + description: 'Matched person record', + optional: true, + properties: PDL_PERSON_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_identify.ts b/apps/sim/tools/peopledatalabs/person_identify.ts new file mode 100644 index 0000000000..cc840befc9 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_identify.ts @@ -0,0 +1,211 @@ +import type { + PdlPersonIdentifyParams, + PdlPersonIdentifyResponse, +} from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectPerson } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const IDENTIFY_MATCH_PROPERTIES = { + match_score: { type: 'number', description: 'Match confidence score (1-99)' }, + matched_on: { + type: 'array', + description: 'Fields that drove the match (only when include_if_matched=true)', + optional: true, + items: { type: 'string', description: 'Field name' }, + }, + person: { + type: 'object', + description: 'Person record', + properties: PDL_PERSON_OUTPUT_PROPERTIES, + }, +} as const satisfies Record + +export const personIdentifyTool: ToolConfig = { + id: 'pdl_person_identify', + name: 'PDL Person Identify', + description: + 'Return up to 20 candidate person matches with confidence scores. Useful when you want to see all plausible matches rather than the single best one. Reference: https://docs.peopledatalabs.com/docs/identify-api-quickstart', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + email: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Email' }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL', + }, + email_hash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'SHA-256 email hash', + }, + lid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn numeric ID', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name', + }, + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + middle_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Middle name', + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name or website', + }, + school: { type: 'string', required: false, visibility: 'user-or-llm', description: 'School' }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location', + }, + street_address: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Street address', + }, + locality: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'City', + }, + region: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State/region', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country', + }, + postal_code: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Postal code', + }, + birth_date: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Birth date (YYYY-MM-DD)', + }, + data_include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated fields to include in each match', + }, + include_if_matched: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include `matched_on` for each match', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + email: params.email, + phone: params.phone, + profile: params.profile, + email_hash: params.email_hash, + lid: params.lid, + name: params.name, + first_name: params.first_name, + middle_name: params.middle_name, + last_name: params.last_name, + company: params.company, + school: params.school, + location: params.location, + street_address: params.street_address, + locality: params.locality, + region: params.region, + country: params.country, + postal_code: params.postal_code, + birth_date: params.birth_date, + data_include: params.data_include, + include_if_matched: params.include_if_matched, + }) + return `https://api.peopledatalabs.com/v5/person/identify${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const matches = (data.matches as Array>) ?? [] + return { + success: true, + output: { + matches: matches.map((m) => ({ + match_score: (m.match_score as number) ?? 0, + matched_on: (m.matched_on as string[]) ?? undefined, + person: projectPerson((m.data as Record) ?? {}), + })), + }, + } + }, + + outputs: { + matches: { + type: 'array', + description: 'Up to 20 candidate matches, ordered by score', + items: { type: 'object', properties: IDENTIFY_MATCH_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_search.ts b/apps/sim/tools/peopledatalabs/person_search.ts new file mode 100644 index 0000000000..a4983aee77 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_search.ts @@ -0,0 +1,111 @@ +import type { PdlPersonSearchParams, PdlPersonSearchResponse } from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectPerson } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const personSearchTool: ToolConfig = { + id: 'pdl_person_search', + name: 'PDL Person Search', + description: + 'Search the People Data Labs person dataset using SQL or Elasticsearch DSL. Returns up to 100 matching records per call.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + sql: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "PDL SQL query (e.g., \"SELECT * FROM person WHERE job_title='engineer' AND location_country='united states'\")", + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Elasticsearch DSL query as JSON string. Use either sql or query, not both.', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (1-100, default 1)', + }, + scroll_token: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token returned from a prior response', + }, + dataset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Dataset filter: all, resume, email, phone, mobile_phone, street_address, consumer_social, developer (combinable with commas, exclude with `-` prefix)', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/person/search', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.sql) body.sql = params.sql + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch { + body.query = params.query + } + } + if (params.size !== undefined) body.size = Number(params.size) + if (params.scroll_token) body.scroll_token = params.scroll_token + if (params.dataset) body.dataset = params.dataset + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const records = (data.data as Record[]) ?? [] + return { + success: true, + output: { + total: (data.total as number) ?? records.length, + scroll_token: (data.scroll_token as string) ?? null, + results: records.map(projectPerson), + }, + } + }, + + outputs: { + total: { type: 'number', description: 'Total matching records in dataset' }, + scroll_token: { + type: 'string', + description: 'Pagination token to fetch the next page; null if no more results', + optional: true, + }, + results: { + type: 'array', + description: 'Person records matching the query', + items: { type: 'object', properties: PDL_PERSON_OUTPUT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/school_clean.ts b/apps/sim/tools/peopledatalabs/school_clean.ts new file mode 100644 index 0000000000..5ae7cc3d50 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/school_clean.ts @@ -0,0 +1,89 @@ +import type { PdlCleanSchoolParams, PdlCleanSchoolResponse } from '@/tools/peopledatalabs/types' +import { PDL_SCHOOL_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectSchool } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanSchoolTool: ToolConfig = { + id: 'pdl_clean_school', + name: 'PDL School Cleaner', + description: + 'Normalize a school string into a canonical school record. Provide at least one of name, website, or profile (LinkedIn URL).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Raw school name to normalize', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'School website', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn school URL', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/school/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name) body.name = params.name + if (params.website) body.website = params.website + if (params.profile) body.profile = params.profile + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, school: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + school: hasFields ? projectSchool(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known school' }, + school: { + type: 'object', + description: 'Canonical school record', + optional: true, + properties: PDL_SCHOOL_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/types.ts b/apps/sim/tools/peopledatalabs/types.ts new file mode 100644 index 0000000000..954793a63a --- /dev/null +++ b/apps/sim/tools/peopledatalabs/types.ts @@ -0,0 +1,477 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +/** + * Output property definitions for People Data Labs API responses. + * Reference: https://docs.peopledatalabs.com/docs + */ + +export const PDL_PERSON_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL person ID', optional: true }, + full_name: { type: 'string', description: 'Full name', optional: true }, + first_name: { type: 'string', description: 'First name', optional: true }, + last_name: { type: 'string', description: 'Last name', optional: true }, + gender: { type: 'string', description: 'Gender', optional: true }, + birth_year: { type: 'number', description: 'Birth year', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn profile URL', optional: true }, + linkedin_username: { type: 'string', description: 'LinkedIn username', optional: true }, + twitter_url: { type: 'string', description: 'Twitter profile URL', optional: true }, + github_url: { type: 'string', description: 'GitHub profile URL', optional: true }, + facebook_url: { type: 'string', description: 'Facebook profile URL', optional: true }, + work_email: { type: 'string', description: 'Primary work email', optional: true }, + personal_emails: { + type: 'array', + description: 'Personal email addresses', + optional: true, + items: { type: 'string', description: 'Email address' }, + }, + emails: { + type: 'array', + description: 'All known email addresses', + optional: true, + items: { type: 'object', description: 'Email entry' }, + }, + phone_numbers: { + type: 'array', + description: 'Known phone numbers', + optional: true, + items: { type: 'string', description: 'Phone number' }, + }, + mobile_phone: { type: 'string', description: 'Mobile phone number', optional: true }, + job_title: { type: 'string', description: 'Current job title', optional: true }, + job_title_role: { type: 'string', description: 'Normalized job role', optional: true }, + job_title_sub_role: { + type: 'string', + description: 'Normalized job sub-role', + optional: true, + }, + job_title_levels: { + type: 'array', + description: 'Seniority levels (e.g., manager, director)', + optional: true, + items: { type: 'string', description: 'Level' }, + }, + job_company_name: { type: 'string', description: 'Current employer name', optional: true }, + job_company_website: { + type: 'string', + description: 'Current employer website', + optional: true, + }, + job_company_industry: { + type: 'string', + description: 'Current employer industry', + optional: true, + }, + job_company_size: { type: 'string', description: 'Current employer size band', optional: true }, + job_company_linkedin_url: { + type: 'string', + description: "Current employer's LinkedIn URL", + optional: true, + }, + job_start_date: { + type: 'string', + description: 'Start date at current employer (YYYY-MM)', + optional: true, + }, + location_name: { type: 'string', description: 'Full location name', optional: true }, + location_locality: { type: 'string', description: 'City', optional: true }, + location_region: { type: 'string', description: 'State/region', optional: true }, + location_country: { type: 'string', description: 'Country', optional: true }, + location_continent: { type: 'string', description: 'Continent', optional: true }, + industry: { type: 'string', description: 'Industry', optional: true }, + skills: { + type: 'array', + description: 'Skills', + optional: true, + items: { type: 'string', description: 'Skill name' }, + }, + interests: { + type: 'array', + description: 'Interests', + optional: true, + items: { type: 'string', description: 'Interest' }, + }, + experience: { + type: 'array', + description: 'Work history entries', + optional: true, + items: { type: 'object', description: 'Job experience' }, + }, + education: { + type: 'array', + description: 'Education history', + optional: true, + items: { type: 'object', description: 'Education entry' }, + }, +} as const satisfies Record + +export const PDL_COMPANY_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL company ID', optional: true }, + name: { type: 'string', description: 'Company name', optional: true }, + display_name: { type: 'string', description: 'Display name', optional: true }, + website: { type: 'string', description: 'Website domain', optional: true }, + ticker: { type: 'string', description: 'Stock ticker', optional: true }, + type: { type: 'string', description: 'Company type (public, private, etc.)', optional: true }, + industry: { type: 'string', description: 'Industry', optional: true }, + size: { type: 'string', description: 'Employee size band', optional: true }, + employee_count: { type: 'number', description: 'Estimated employee count', optional: true }, + founded: { type: 'number', description: 'Year founded', optional: true }, + headline: { type: 'string', description: 'Company headline/tagline', optional: true }, + summary: { type: 'string', description: 'Company description', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + linkedin_id: { type: 'string', description: 'LinkedIn ID', optional: true }, + twitter_url: { type: 'string', description: 'Twitter URL', optional: true }, + facebook_url: { type: 'string', description: 'Facebook URL', optional: true }, + location_name: { type: 'string', description: 'HQ location name', optional: true }, + location_locality: { type: 'string', description: 'HQ city', optional: true }, + location_region: { type: 'string', description: 'HQ state/region', optional: true }, + location_country: { type: 'string', description: 'HQ country', optional: true }, + tags: { + type: 'array', + description: 'Company tags', + optional: true, + items: { type: 'string', description: 'Tag' }, + }, + tickers: { + type: 'array', + description: 'All stock tickers', + optional: true, + items: { type: 'string', description: 'Ticker' }, + }, +} as const satisfies Record + +export interface PdlPersonRecord { + id?: string + full_name?: string + first_name?: string + last_name?: string + gender?: string + birth_year?: number + linkedin_url?: string + linkedin_username?: string + twitter_url?: string + github_url?: string + facebook_url?: string + work_email?: string + personal_emails?: string[] + emails?: unknown[] + phone_numbers?: string[] + mobile_phone?: string + job_title?: string + job_title_role?: string + job_title_sub_role?: string + job_title_levels?: string[] + job_company_name?: string + job_company_website?: string + job_company_industry?: string + job_company_size?: string + job_company_linkedin_url?: string + job_start_date?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string + location_continent?: string + industry?: string + skills?: string[] + interests?: string[] + experience?: unknown[] + education?: unknown[] +} + +export interface PdlCompanyRecord { + id?: string + name?: string + display_name?: string + website?: string + ticker?: string + type?: string + industry?: string + size?: string + employee_count?: number + founded?: number + headline?: string + summary?: string + linkedin_url?: string + linkedin_id?: string + twitter_url?: string + facebook_url?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string + tags?: string[] + tickers?: string[] +} + +export interface PdlPersonEnrichParams { + apiKey: string + email?: string + phone?: string + profile?: string + lid?: string + first_name?: string + last_name?: string + company?: string + school?: string + location?: string + min_likelihood?: number + required?: string +} + +export interface PdlPersonEnrichResponse extends ToolResponse { + output: { + matched: boolean + likelihood: number | null + person: PdlPersonRecord | null + } +} + +export interface PdlPersonSearchParams { + apiKey: string + sql?: string + query?: string + size?: number + scroll_token?: string + dataset?: string +} + +export interface PdlPersonSearchResponse extends ToolResponse { + output: { + total: number + scroll_token: string | null + results: PdlPersonRecord[] + } +} + +export interface PdlCompanyEnrichParams { + apiKey: string + name?: string + website?: string + profile?: string + ticker?: string + pdl_id?: string + location?: string + locality?: string + region?: string + country?: string + min_likelihood?: number + required?: string +} + +export interface PdlCompanyEnrichResponse extends ToolResponse { + output: { + matched: boolean + likelihood: number | null + company: PdlCompanyRecord | null + } +} + +export interface PdlCompanySearchParams { + apiKey: string + sql?: string + query?: string + size?: number + scroll_token?: string + dataset?: string +} + +export interface PdlCompanySearchResponse extends ToolResponse { + output: { + total: number + scroll_token: string | null + results: PdlCompanyRecord[] + } +} + +export interface PdlAutocompleteParams { + apiKey: string + field: string + text?: string + size?: number +} + +export interface PdlAutocompleteSuggestion { + name: string + count: number + meta?: Record +} + +export interface PdlAutocompleteResponse extends ToolResponse { + output: { + suggestions: PdlAutocompleteSuggestion[] + } +} + +export interface PdlBulkPersonEnrichParams { + apiKey: string + requests: string + required?: string +} + +export interface PdlBulkPersonResultItem { + status: number + likelihood: number | null + matched: boolean + metadata: Record | null + person: PdlPersonRecord | null +} + +export interface PdlBulkPersonEnrichResponse extends ToolResponse { + output: { + results: PdlBulkPersonResultItem[] + } +} + +export interface PdlBulkCompanyEnrichParams { + apiKey: string + requests: string + required?: string +} + +export interface PdlBulkCompanyResultItem { + status: number + likelihood: number | null + matched: boolean + metadata: Record | null + company: PdlCompanyRecord | null +} + +export interface PdlBulkCompanyEnrichResponse extends ToolResponse { + output: { + results: PdlBulkCompanyResultItem[] + } +} + +export interface PdlPersonIdentifyParams { + apiKey: string + email?: string + phone?: string + profile?: string + email_hash?: string + lid?: string + name?: string + first_name?: string + middle_name?: string + last_name?: string + company?: string + school?: string + location?: string + street_address?: string + locality?: string + region?: string + country?: string + postal_code?: string + birth_date?: string + data_include?: string + include_if_matched?: boolean +} + +export interface PdlPersonIdentifyMatch { + match_score: number + matched_on?: string[] + person: PdlPersonRecord +} + +export interface PdlPersonIdentifyResponse extends ToolResponse { + output: { + matches: PdlPersonIdentifyMatch[] + } +} + +export interface PdlCleanCompanyParams { + apiKey: string + name?: string + website?: string + profile?: string +} + +export interface PdlCleanCompanyResponse extends ToolResponse { + output: { + matched: boolean + company: PdlCompanyRecord | null + } +} + +export interface PdlCleanLocationParams { + apiKey: string + location: string +} + +export const PDL_LOCATION_OUTPUT_PROPERTIES = { + name: { type: 'string', description: 'Normalized location name', optional: true }, + locality: { type: 'string', description: 'City', optional: true }, + region: { type: 'string', description: 'State/region', optional: true }, + country: { type: 'string', description: 'Country', optional: true }, + continent: { type: 'string', description: 'Continent', optional: true }, + type: { type: 'string', description: 'Location type', optional: true }, + geo: { type: 'string', description: 'Latitude,longitude string', optional: true }, + latitude: { type: 'number', description: 'Latitude', optional: true }, + longitude: { type: 'number', description: 'Longitude', optional: true }, +} as const satisfies Record + +export interface PdlLocationRecord { + name?: string + locality?: string + region?: string + country?: string + continent?: string + type?: string + geo?: string + latitude?: number + longitude?: number +} + +export interface PdlCleanLocationResponse extends ToolResponse { + output: { + matched: boolean + location: PdlLocationRecord | null + } +} + +export interface PdlCleanSchoolParams { + apiKey: string + name?: string + website?: string + profile?: string +} + +export const PDL_SCHOOL_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL school ID', optional: true }, + name: { type: 'string', description: 'School name', optional: true }, + type: { + type: 'string', + description: 'School type (e.g., university, secondary)', + optional: true, + }, + website: { type: 'string', description: 'Website domain', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + linkedin_id: { type: 'string', description: 'LinkedIn ID', optional: true }, + facebook_url: { type: 'string', description: 'Facebook URL', optional: true }, + twitter_url: { type: 'string', description: 'Twitter URL', optional: true }, + location_name: { type: 'string', description: 'Location name', optional: true }, + location_locality: { type: 'string', description: 'City', optional: true }, + location_region: { type: 'string', description: 'State/region', optional: true }, + location_country: { type: 'string', description: 'Country', optional: true }, +} as const satisfies Record + +export interface PdlSchoolRecord { + id?: string + name?: string + type?: string + website?: string + linkedin_url?: string + linkedin_id?: string + facebook_url?: string + twitter_url?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string +} + +export interface PdlCleanSchoolResponse extends ToolResponse { + output: { + matched: boolean + school: PdlSchoolRecord | null + } +} diff --git a/apps/sim/tools/peopledatalabs/utils.ts b/apps/sim/tools/peopledatalabs/utils.ts new file mode 100644 index 0000000000..e21c902600 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/utils.ts @@ -0,0 +1,145 @@ +import type { + PdlCompanyRecord, + PdlLocationRecord, + PdlPersonRecord, + PdlSchoolRecord, +} from '@/tools/peopledatalabs/types' + +/** + * Build a query string from non-empty params. + */ +export function buildQueryString(params: Record): string { + const search = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue + const str = String(value) + if (str.length === 0) continue + search.append(key, str) + } + const qs = search.toString() + return qs ? `?${qs}` : '' +} + +/** + * Project a raw PDL person record onto our flat schema. + */ +export function projectPerson(raw: Record | null | undefined): PdlPersonRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + full_name: (r.full_name as string) ?? undefined, + first_name: (r.first_name as string) ?? undefined, + last_name: (r.last_name as string) ?? undefined, + gender: (r.gender as string) ?? undefined, + birth_year: (r.birth_year as number) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_username: (r.linkedin_username as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + github_url: (r.github_url as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + work_email: (r.work_email as string) ?? undefined, + personal_emails: (r.personal_emails as string[]) ?? undefined, + emails: (r.emails as unknown[]) ?? undefined, + phone_numbers: (r.phone_numbers as string[]) ?? undefined, + mobile_phone: (r.mobile_phone as string) ?? undefined, + job_title: (r.job_title as string) ?? undefined, + job_title_role: (r.job_title_role as string) ?? undefined, + job_title_sub_role: (r.job_title_sub_role as string) ?? undefined, + job_title_levels: (r.job_title_levels as string[]) ?? undefined, + job_company_name: (r.job_company_name as string) ?? undefined, + job_company_website: (r.job_company_website as string) ?? undefined, + job_company_industry: (r.job_company_industry as string) ?? undefined, + job_company_size: (r.job_company_size as string) ?? undefined, + job_company_linkedin_url: (r.job_company_linkedin_url as string) ?? undefined, + job_start_date: (r.job_start_date as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + location_continent: + (r.location_continent as string) ?? (location.continent as string) ?? undefined, + industry: (r.industry as string) ?? undefined, + skills: (r.skills as string[]) ?? undefined, + interests: (r.interests as string[]) ?? undefined, + experience: (r.experience as unknown[]) ?? undefined, + education: (r.education as unknown[]) ?? undefined, + } +} + +/** + * Project a raw PDL company record onto our flat schema. + */ +export function projectCompany(raw: Record | null | undefined): PdlCompanyRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + name: (r.name as string) ?? undefined, + display_name: (r.display_name as string) ?? undefined, + website: (r.website as string) ?? undefined, + ticker: (r.ticker as string) ?? undefined, + type: (r.type as string) ?? undefined, + industry: (r.industry as string) ?? undefined, + size: (r.size as string) ?? undefined, + employee_count: (r.employee_count as number) ?? undefined, + founded: (r.founded as number) ?? undefined, + headline: (r.headline as string) ?? undefined, + summary: (r.summary as string) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_id: (r.linkedin_id as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + tags: (r.tags as string[]) ?? undefined, + tickers: (r.tickers as string[]) ?? undefined, + } +} + +/** + * Project a raw PDL location record onto our flat schema. + */ +export function projectLocation( + raw: Record | null | undefined +): PdlLocationRecord { + const r = (raw ?? {}) as Record + return { + name: (r.name as string) ?? undefined, + locality: (r.locality as string) ?? undefined, + region: (r.region as string) ?? undefined, + country: (r.country as string) ?? undefined, + continent: (r.continent as string) ?? undefined, + type: (r.type as string) ?? undefined, + geo: (r.geo as string) ?? undefined, + latitude: (r.latitude as number) ?? undefined, + longitude: (r.longitude as number) ?? undefined, + } +} + +/** + * Project a raw PDL school record onto our flat schema. + */ +export function projectSchool(raw: Record | null | undefined): PdlSchoolRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + name: (r.name as string) ?? undefined, + type: (r.type as string) ?? undefined, + website: (r.website as string) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_id: (r.linkedin_id as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8da147da4e..4fbaff816b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1902,6 +1902,19 @@ import { pagerdutyUpdateIncidentTool, } from '@/tools/pagerduty' import { parallelDeepResearchTool, parallelExtractTool, parallelSearchTool } from '@/tools/parallel' +import { + pdlAutocompleteTool, + pdlBulkCompanyEnrichTool, + pdlBulkPersonEnrichTool, + pdlCleanCompanyTool, + pdlCleanLocationTool, + pdlCleanSchoolTool, + pdlCompanyEnrichTool, + pdlCompanySearchTool, + pdlPersonEnrichTool, + pdlPersonIdentifyTool, + pdlPersonSearchTool, +} from '@/tools/peopledatalabs' import { perplexityChatTool, perplexitySearchTool } from '@/tools/perplexity' import { pineconeFetchTool, @@ -4423,6 +4436,17 @@ export const tools: Record = { google_slides_create_table: googleSlidesCreateTableTool, google_slides_create_shape: googleSlidesCreateShapeTool, google_slides_insert_text: googleSlidesInsertTextTool, + pdl_person_enrich: pdlPersonEnrichTool, + pdl_person_search: pdlPersonSearchTool, + pdl_person_identify: pdlPersonIdentifyTool, + pdl_bulk_person_enrich: pdlBulkPersonEnrichTool, + pdl_company_enrich: pdlCompanyEnrichTool, + pdl_company_search: pdlCompanySearchTool, + pdl_bulk_company_enrich: pdlBulkCompanyEnrichTool, + pdl_clean_company: pdlCleanCompanyTool, + pdl_clean_location: pdlCleanLocationTool, + pdl_clean_school: pdlCleanSchoolTool, + pdl_autocomplete: pdlAutocompleteTool, perplexity_chat: perplexityChatTool, perplexity_search: perplexitySearchTool, profound_bot_logs: profoundBotLogsTool, From 05c3e9b0b1119cc9399018e11d2fcae94bde0ab8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 19:59:03 -0700 Subject: [PATCH 02/13] fix(peopledatalabs): narrow conditions for fields not used by every operation - min_likelihood now only shows for pdl_person_enrich (Person Identify ignores it) - ticker, pdl_id, company_location now only show for pdl_company_enrich (Company Cleaner only accepts name/website/profile) Addresses Greptile P1 review on PR #4513. --- apps/sim/blocks/blocks/peopledatalabs.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 7b5f192aff..32b232f886 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -95,7 +95,7 @@ export const PeopleDataLabsBlock: BlockConfig = { title: 'Min Likelihood', type: 'short-input', placeholder: '6', - condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + condition: { field: 'operation', value: 'pdl_person_enrich' }, mode: 'advanced', }, @@ -178,14 +178,14 @@ export const PeopleDataLabsBlock: BlockConfig = { title: 'Ticker', type: 'short-input', placeholder: 'AAPL', - condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + condition: { field: 'operation', value: 'pdl_company_enrich' }, mode: 'advanced', }, { id: 'pdl_id', title: 'PDL Company ID', type: 'short-input', - condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + condition: { field: 'operation', value: 'pdl_company_enrich' }, mode: 'advanced', }, { @@ -193,7 +193,7 @@ export const PeopleDataLabsBlock: BlockConfig = { title: 'Location', type: 'short-input', placeholder: 'San Francisco, CA', - condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + condition: { field: 'operation', value: 'pdl_company_enrich' }, mode: 'advanced', }, From 56fcbfb624fecf2b68af06c51c2eb16b2cff8863 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 20:06:14 -0700 Subject: [PATCH 03/13] fix(peopledatalabs): scope param renames to their operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Param renames (company_profile→profile, company_location→location, school_*→*, bulk_*_requests→requests, autocomplete_size→size, etc.) now run only when the matching operation is selected, and stale alternate-operation values are stripped from the request. This prevents values left over from a prior operation switch from leaking into the current API call (e.g. a company LinkedIn URL overwriting a person profile, or a stale search size overwriting autocomplete size). Addresses Cursor Bugbot review on PR #4513. --- apps/sim/blocks/blocks/peopledatalabs.ts | 141 +++++++++++++---------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 32b232f886..ce38711bb9 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -411,82 +411,95 @@ export const PeopleDataLabsBlock: BlockConfig = { }, params: (params) => { const result: Record = { ...params } + const op = params.operation - if (params.company_profile !== undefined) { - result.profile = params.company_profile - result.company_profile = undefined - } - if (params.company_location !== undefined) { - result.location = params.company_location - result.company_location = undefined + // Strip alternate-operation aliases so stale values from prior operations + // can't leak into the current request. + result.company_profile = undefined + result.company_location = undefined + result.autocomplete_size = undefined + result.identify_locality = undefined + result.identify_region = undefined + result.identify_country = undefined + result.identify_postal_code = undefined + result.identify_birth_date = undefined + result.bulk_person_requests = undefined + result.bulk_person_required = undefined + result.bulk_company_requests = undefined + result.bulk_company_required = undefined + result.clean_location_input = undefined + result.school_name = undefined + result.school_website = undefined + result.school_profile = undefined + + // Company Enrich uses company_* aliases for shared fields (profile, location) + if (op === 'pdl_company_enrich') { + if (params.company_profile !== undefined) result.profile = params.company_profile + if (params.company_location !== undefined) result.location = params.company_location } - if (params.autocomplete_size !== undefined) { - result.size = Number(params.autocomplete_size) - result.autocomplete_size = undefined + // Company Cleaner only accepts profile (LinkedIn URL) of the company aliases + if (op === 'pdl_clean_company') { + if (params.company_profile !== undefined) result.profile = params.company_profile } - if (params.size !== undefined) { + + // Autocomplete: use autocomplete_size, ignore stale search size + if (op === 'pdl_autocomplete') { + if (params.autocomplete_size !== undefined) { + result.size = Number(params.autocomplete_size) + } else { + result.size = undefined + } + } else if (params.size !== undefined) { result.size = Number(params.size) } - if (params.min_likelihood !== undefined) { - result.min_likelihood = Number(params.min_likelihood) - } - // Person Identify field renames - if (params.identify_locality !== undefined) { - result.locality = params.identify_locality - result.identify_locality = undefined - } - if (params.identify_region !== undefined) { - result.region = params.identify_region - result.identify_region = undefined - } - if (params.identify_country !== undefined) { - result.country = params.identify_country - result.identify_country = undefined - } - if (params.identify_postal_code !== undefined) { - result.postal_code = params.identify_postal_code - result.identify_postal_code = undefined - } - if (params.identify_birth_date !== undefined) { - result.birth_date = params.identify_birth_date - result.identify_birth_date = undefined + // min_likelihood is only honored by enrich endpoints + if (op === 'pdl_person_enrich' || op === 'pdl_company_enrich') { + if (params.min_likelihood !== undefined) { + result.min_likelihood = Number(params.min_likelihood) + } + } else { + result.min_likelihood = undefined } - // Bulk renames - if (params.bulk_person_requests !== undefined) { - result.requests = params.bulk_person_requests - result.bulk_person_requests = undefined - } - if (params.bulk_person_required !== undefined) { - result.required = params.bulk_person_required - result.bulk_person_required = undefined - } - if (params.bulk_company_requests !== undefined) { - result.requests = params.bulk_company_requests - result.bulk_company_requests = undefined - } - if (params.bulk_company_required !== undefined) { - result.required = params.bulk_company_required - result.bulk_company_required = undefined + if (op === 'pdl_person_identify') { + if (params.identify_locality !== undefined) result.locality = params.identify_locality + if (params.identify_region !== undefined) result.region = params.identify_region + if (params.identify_country !== undefined) result.country = params.identify_country + if (params.identify_postal_code !== undefined) { + result.postal_code = params.identify_postal_code + } + if (params.identify_birth_date !== undefined) { + result.birth_date = params.identify_birth_date + } } - // Cleaner renames - if (params.clean_location_input !== undefined) { - result.location = params.clean_location_input - result.clean_location_input = undefined + if (op === 'pdl_bulk_person_enrich') { + if (params.bulk_person_requests !== undefined) { + result.requests = params.bulk_person_requests + } + if (params.bulk_person_required !== undefined) { + result.required = params.bulk_person_required + } + } else if (op === 'pdl_bulk_company_enrich') { + if (params.bulk_company_requests !== undefined) { + result.requests = params.bulk_company_requests + } + if (params.bulk_company_required !== undefined) { + result.required = params.bulk_company_required + } } - if (params.school_name !== undefined) { - result.name = params.school_name - result.school_name = undefined - } - if (params.school_website !== undefined) { - result.website = params.school_website - result.school_website = undefined + + if (op === 'pdl_clean_location') { + if (params.clean_location_input !== undefined) { + result.location = params.clean_location_input + } } - if (params.school_profile !== undefined) { - result.profile = params.school_profile - result.school_profile = undefined + + if (op === 'pdl_clean_school') { + if (params.school_name !== undefined) result.name = params.school_name + if (params.school_website !== undefined) result.website = params.school_website + if (params.school_profile !== undefined) result.profile = params.school_profile } return result From f2a3c9f76e8fc56ad4e9b49d5a9f7c52948d4991 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 20:16:30 -0700 Subject: [PATCH 04/13] fix(peopledatalabs): use currentColor for icon fill The PeopleDataLabsIcon was hardcoded to white, leaving it invisible on light backgrounds when rendered outside its bgColor container (e.g., search results, menus, docs). Switch to currentColor so it inherits the surrounding text color. Addresses Cursor Bugbot review on PR #4513. --- apps/sim/components/icons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dbc8b6638b..88836b8317 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -905,7 +905,7 @@ export function PeopleDataLabsIcon(props: SVGProps) { {...props} > From c0b76c2facfb63e61a87b69214af842bfd4a2865 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 21:09:30 -0700 Subject: [PATCH 05/13] fix(peopledatalabs): scope shared fields (profile/location/name/website) per operation The block has subBlocks whose raw IDs collide with PDL API param names (profile, location for person; name, website for company). Their values persist across operation switches even though the UI hides them, so a person LinkedIn URL could leak into a Company Enrich request, etc. Reset these shared targets and repopulate them only from inputs that belong to the active operation. Addresses Greptile P1 review on PR #4513. --- apps/sim/blocks/blocks/peopledatalabs.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index ce38711bb9..ee54b73618 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -432,13 +432,31 @@ export const PeopleDataLabsBlock: BlockConfig = { result.school_website = undefined result.school_profile = undefined - // Company Enrich uses company_* aliases for shared fields (profile, location) + // Clear shared target fields and repopulate them per-operation. The raw + // `profile`/`location`/`name`/`website` subBlocks are scoped to specific + // operations in the UI, but their values persist when the user switches + // operations — without this reset, e.g. a person LinkedIn URL would + // leak into a Company Enrich request as the company profile. + result.profile = undefined + result.location = undefined + result.name = undefined + result.website = undefined + + if (op === 'pdl_person_enrich' || op === 'pdl_person_identify') { + if (params.profile !== undefined) result.profile = params.profile + } + if (op === 'pdl_person_enrich') { + if (params.location !== undefined) result.location = params.location + } if (op === 'pdl_company_enrich') { + if (params.name !== undefined) result.name = params.name + if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile if (params.company_location !== undefined) result.location = params.company_location } - // Company Cleaner only accepts profile (LinkedIn URL) of the company aliases if (op === 'pdl_clean_company') { + if (params.name !== undefined) result.name = params.name + if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile } From 9f86e7418e8d733351f50941cfc0ad5fbbd1a054 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 21:14:30 -0700 Subject: [PATCH 06/13] fix(peopledatalabs): scope `size` to search and autocomplete operations `size` is shared by the person/company search subBlock and the autocomplete_size alias. The previous logic still forwarded a stale search `size` to operations that don't accept it (e.g. enrich, clean, identify), and the autocomplete branch only cleared it when autocomplete_size was unset. Reset `size` up front and only repopulate it for the three operations that actually accept it. Found via final integration audit of PR #4513. --- apps/sim/blocks/blocks/peopledatalabs.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index ee54b73618..9f042e4262 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -460,15 +460,17 @@ export const PeopleDataLabsBlock: BlockConfig = { if (params.company_profile !== undefined) result.profile = params.company_profile } - // Autocomplete: use autocomplete_size, ignore stale search size + // `size` is shared by search and autocomplete subBlocks; reset and + // repopulate per-operation so a stale search size can't bleed into an + // autocomplete request (or vice versa) or into operations that don't + // accept `size` at all. + result.size = undefined if (op === 'pdl_autocomplete') { if (params.autocomplete_size !== undefined) { result.size = Number(params.autocomplete_size) - } else { - result.size = undefined } - } else if (params.size !== undefined) { - result.size = Number(params.size) + } else if (op === 'pdl_person_search' || op === 'pdl_company_search') { + if (params.size !== undefined) result.size = Number(params.size) } // min_likelihood is only honored by enrich endpoints From f55893aa1df182c2166937e9fe9fc2dda41cd0e0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 21:15:22 -0700 Subject: [PATCH 07/13] fix(peopledatalabs): restore `location` for Person Identify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Person Identify's tool accepts `location`, and the subBlock is shown for both Enrich and Identify, but the prior reset only repopulated `result.location` for Enrich — so any value entered on Identify was silently dropped before reaching the API. Addresses Greptile P1 review on PR #4513. --- apps/sim/blocks/blocks/peopledatalabs.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 9f042e4262..63b00cb4a9 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -444,8 +444,6 @@ export const PeopleDataLabsBlock: BlockConfig = { if (op === 'pdl_person_enrich' || op === 'pdl_person_identify') { if (params.profile !== undefined) result.profile = params.profile - } - if (op === 'pdl_person_enrich') { if (params.location !== undefined) result.location = params.location } if (op === 'pdl_company_enrich') { From 092dfba37ceeaed65aa485d00733c85ac8996009 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 21:39:15 -0700 Subject: [PATCH 08/13] fix(peopledatalabs): align endpoints + outputs with PDL API - person_identify: short-circuit on PDL 404 (no-match), matching the person_enrich pattern - company_search: drop unsupported `dataset` param (PDL company search docs do not list it) - block: expose `min_likelihood` for `pdl_company_enrich` (PDL Company Enrichment supports min_likelihood) - location_clean: surface `subregion`; drop phantom `latitude`/ `longitude` (PDL only returns `geo` as a "lat,lon" string) - school_clean: surface `domain` and `location_continent` from the nested `location` object - docs icon: switch fill to `currentColor` so the icon renders on light backgrounds --- apps/docs/components/icons.tsx | 2 +- apps/sim/blocks/blocks/peopledatalabs.ts | 2 +- apps/sim/tools/peopledatalabs/company_search.ts | 7 ------- apps/sim/tools/peopledatalabs/person_identify.ts | 5 +++++ apps/sim/tools/peopledatalabs/types.ts | 11 ++++++----- apps/sim/tools/peopledatalabs/utils.ts | 6 ++++-- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dbc8b6638b..88836b8317 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -905,7 +905,7 @@ export function PeopleDataLabsIcon(props: SVGProps) { {...props} > diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 63b00cb4a9..ca09fbe276 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -95,7 +95,7 @@ export const PeopleDataLabsBlock: BlockConfig = { title: 'Min Likelihood', type: 'short-input', placeholder: '6', - condition: { field: 'operation', value: 'pdl_person_enrich' }, + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_company_enrich'] }, mode: 'advanced', }, diff --git a/apps/sim/tools/peopledatalabs/company_search.ts b/apps/sim/tools/peopledatalabs/company_search.ts index 524cce29e0..a98569cbe4 100644 --- a/apps/sim/tools/peopledatalabs/company_search.ts +++ b/apps/sim/tools/peopledatalabs/company_search.ts @@ -42,12 +42,6 @@ export const companySearchTool: ToolConfig { const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matches: [] } } + } if (!response.ok) { const error = (data.error as { message?: string })?.message diff --git a/apps/sim/tools/peopledatalabs/types.ts b/apps/sim/tools/peopledatalabs/types.ts index 954793a63a..274ce75716 100644 --- a/apps/sim/tools/peopledatalabs/types.ts +++ b/apps/sim/tools/peopledatalabs/types.ts @@ -272,7 +272,6 @@ export interface PdlCompanySearchParams { query?: string size?: number scroll_token?: string - dataset?: string } export interface PdlCompanySearchResponse extends ToolResponse { @@ -401,24 +400,22 @@ export const PDL_LOCATION_OUTPUT_PROPERTIES = { name: { type: 'string', description: 'Normalized location name', optional: true }, locality: { type: 'string', description: 'City', optional: true }, region: { type: 'string', description: 'State/region', optional: true }, + subregion: { type: 'string', description: 'Subregion (e.g., county)', optional: true }, country: { type: 'string', description: 'Country', optional: true }, continent: { type: 'string', description: 'Continent', optional: true }, type: { type: 'string', description: 'Location type', optional: true }, geo: { type: 'string', description: 'Latitude,longitude string', optional: true }, - latitude: { type: 'number', description: 'Latitude', optional: true }, - longitude: { type: 'number', description: 'Longitude', optional: true }, } as const satisfies Record export interface PdlLocationRecord { name?: string locality?: string region?: string + subregion?: string country?: string continent?: string type?: string geo?: string - latitude?: number - longitude?: number } export interface PdlCleanLocationResponse extends ToolResponse { @@ -448,10 +445,12 @@ export const PDL_SCHOOL_OUTPUT_PROPERTIES = { linkedin_id: { type: 'string', description: 'LinkedIn ID', optional: true }, facebook_url: { type: 'string', description: 'Facebook URL', optional: true }, twitter_url: { type: 'string', description: 'Twitter URL', optional: true }, + domain: { type: 'string', description: 'School domain', optional: true }, location_name: { type: 'string', description: 'Location name', optional: true }, location_locality: { type: 'string', description: 'City', optional: true }, location_region: { type: 'string', description: 'State/region', optional: true }, location_country: { type: 'string', description: 'Country', optional: true }, + location_continent: { type: 'string', description: 'Continent', optional: true }, } as const satisfies Record export interface PdlSchoolRecord { @@ -463,10 +462,12 @@ export interface PdlSchoolRecord { linkedin_id?: string facebook_url?: string twitter_url?: string + domain?: string location_name?: string location_locality?: string location_region?: string location_country?: string + location_continent?: string } export interface PdlCleanSchoolResponse extends ToolResponse { diff --git a/apps/sim/tools/peopledatalabs/utils.ts b/apps/sim/tools/peopledatalabs/utils.ts index e21c902600..bd37a47a04 100644 --- a/apps/sim/tools/peopledatalabs/utils.ts +++ b/apps/sim/tools/peopledatalabs/utils.ts @@ -112,12 +112,11 @@ export function projectLocation( name: (r.name as string) ?? undefined, locality: (r.locality as string) ?? undefined, region: (r.region as string) ?? undefined, + subregion: (r.subregion as string) ?? undefined, country: (r.country as string) ?? undefined, continent: (r.continent as string) ?? undefined, type: (r.type as string) ?? undefined, geo: (r.geo as string) ?? undefined, - latitude: (r.latitude as number) ?? undefined, - longitude: (r.longitude as number) ?? undefined, } } @@ -136,10 +135,13 @@ export function projectSchool(raw: Record | null | undefined): linkedin_id: (r.linkedin_id as string) ?? undefined, facebook_url: (r.facebook_url as string) ?? undefined, twitter_url: (r.twitter_url as string) ?? undefined, + domain: (r.domain as string) ?? undefined, location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, location_locality: (r.location_locality as string) ?? (location.locality as string) ?? undefined, location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + location_continent: + (r.location_continent as string) ?? (location.continent as string) ?? undefined, } } From 2be7c7609dde5df6e5288343adbd363196ae83f5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 21:53:24 -0700 Subject: [PATCH 09/13] fix(peopledatalabs): restore `name` for Person Enrich / Identify The shared `name` reset at the top of `tools.config.params` was only repopulated for the company-side operations, so any programmatic `name` input to `pdl_person_enrich` or `pdl_person_identify` was silently dropped. Both PDL endpoints accept `name` as a full-name match parameter. --- apps/sim/blocks/blocks/peopledatalabs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index ca09fbe276..244bd438c0 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -445,6 +445,7 @@ export const PeopleDataLabsBlock: BlockConfig = { if (op === 'pdl_person_enrich' || op === 'pdl_person_identify') { if (params.profile !== undefined) result.profile = params.profile if (params.location !== undefined) result.location = params.location + if (params.name !== undefined) result.name = params.name } if (op === 'pdl_company_enrich') { if (params.name !== undefined) result.name = params.name From 94832498e5eae369abf4a37e94cf9a32994a0658 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 22:45:38 -0700 Subject: [PATCH 10/13] fix(peopledatalabs): restore `name` for Person Enrich Add the `name` parameter to `PdlPersonEnrichParams`, the tool's params definition, and the URL builder. PDL Person Enrichment accepts `name` as a full-name match alternative to first_name + last_name; without it, programmatic `name` input was silently dropped before reaching the API. --- apps/sim/tools/peopledatalabs/person_enrich.ts | 7 +++++++ apps/sim/tools/peopledatalabs/types.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/sim/tools/peopledatalabs/person_enrich.ts b/apps/sim/tools/peopledatalabs/person_enrich.ts index 82d20cd5ad..4627fccefc 100644 --- a/apps/sim/tools/peopledatalabs/person_enrich.ts +++ b/apps/sim/tools/peopledatalabs/person_enrich.ts @@ -41,6 +41,12 @@ export const personEnrichTool: ToolConfig Date: Fri, 8 May 2026 10:21:24 -0700 Subject: [PATCH 11/13] fix(peopledatalabs): isolate company `name` UI from person ops Rename Company Name subBlock id from `name` to `company_name` so a stale company value can't leak into Person Enrich/Identify when the user switches operations. Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/peopledatalabs.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 244bd438c0..af6f760fb2 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -152,7 +152,7 @@ export const PeopleDataLabsBlock: BlockConfig = { // Company Enrich fields { - id: 'name', + id: 'company_name', title: 'Company Name', type: 'short-input', placeholder: 'Acme Inc', @@ -433,7 +433,7 @@ export const PeopleDataLabsBlock: BlockConfig = { result.school_profile = undefined // Clear shared target fields and repopulate them per-operation. The raw - // `profile`/`location`/`name`/`website` subBlocks are scoped to specific + // `profile`/`location`/`website` subBlocks are scoped to specific // operations in the UI, but their values persist when the user switches // operations — without this reset, e.g. a person LinkedIn URL would // leak into a Company Enrich request as the company profile. @@ -441,6 +441,7 @@ export const PeopleDataLabsBlock: BlockConfig = { result.location = undefined result.name = undefined result.website = undefined + result.company_name = undefined if (op === 'pdl_person_enrich' || op === 'pdl_person_identify') { if (params.profile !== undefined) result.profile = params.profile @@ -448,13 +449,15 @@ export const PeopleDataLabsBlock: BlockConfig = { if (params.name !== undefined) result.name = params.name } if (op === 'pdl_company_enrich') { - if (params.name !== undefined) result.name = params.name + if (params.company_name !== undefined) result.name = params.company_name + else if (params.name !== undefined) result.name = params.name if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile if (params.company_location !== undefined) result.location = params.company_location } if (op === 'pdl_clean_company') { - if (params.name !== undefined) result.name = params.name + if (params.company_name !== undefined) result.name = params.company_name + else if (params.name !== undefined) result.name = params.name if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile } From 98899a5478f9dc9dccac885e2e7ba5a23f85bb27 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 8 May 2026 10:49:57 -0700 Subject: [PATCH 12/13] fix(peopledatalabs): honor programmatic inputs for clean_location/school `pdl_clean_location` and `pdl_clean_school` were only restoring values from UI subBlock IDs (`clean_location_input`, `school_*`). Programmatic callers using the declared `location`/`name`/`website`/`profile` inputs had their values dropped after the shared-field reset. Add fallbacks so both UI and programmatic inputs flow through. Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/peopledatalabs.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index af6f760fb2..781d284d92 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -515,13 +515,18 @@ export const PeopleDataLabsBlock: BlockConfig = { if (op === 'pdl_clean_location') { if (params.clean_location_input !== undefined) { result.location = params.clean_location_input + } else if (params.location !== undefined) { + result.location = params.location } } if (op === 'pdl_clean_school') { if (params.school_name !== undefined) result.name = params.school_name + else if (params.name !== undefined) result.name = params.name if (params.school_website !== undefined) result.website = params.school_website + else if (params.website !== undefined) result.website = params.website if (params.school_profile !== undefined) result.profile = params.school_profile + else if (params.profile !== undefined) result.profile = params.profile } return result From f19404d86ec78feeb38eb6eab7a2fde4d2f90c88 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 8 May 2026 10:53:39 -0700 Subject: [PATCH 13/13] fix(peopledatalabs): programmatic input fallbacks + required autocomplete text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Company Enrich and Clean Company now fall back to programmatic `params.profile` / `params.location` when the UI-scoped `company_profile` / `company_location` are absent. Mirrors the fallback pattern already used for `name`. - Autocomplete `text` subBlock is now required when operation is autocomplete — PDL requires it for nearly all field values. Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/peopledatalabs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts index 781d284d92..b8572509bd 100644 --- a/apps/sim/blocks/blocks/peopledatalabs.ts +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -228,6 +228,7 @@ export const PeopleDataLabsBlock: BlockConfig = { type: 'short-input', placeholder: 'engin', condition: { field: 'operation', value: 'pdl_autocomplete' }, + required: { field: 'operation', value: 'pdl_autocomplete' }, }, { id: 'autocomplete_size', @@ -453,13 +454,16 @@ export const PeopleDataLabsBlock: BlockConfig = { else if (params.name !== undefined) result.name = params.name if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile + else if (params.profile !== undefined) result.profile = params.profile if (params.company_location !== undefined) result.location = params.company_location + else if (params.location !== undefined) result.location = params.location } if (op === 'pdl_clean_company') { if (params.company_name !== undefined) result.name = params.company_name else if (params.name !== undefined) result.name = params.name if (params.website !== undefined) result.website = params.website if (params.company_profile !== undefined) result.profile = params.company_profile + else if (params.profile !== undefined) result.profile = params.profile } // `size` is shared by search and autocomplete subBlocks; reset and