From d6ec115348d0581fc2e6729298db7f31c776d1d6 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 16:11:31 -0700 Subject: [PATCH 1/2] v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha --- apps/sim/app/(auth)/signup/signup-form.tsx | 11 +++-------- .../app/workspace/[workspaceId]/home/home.tsx | 12 ++++++++++-- .../w/[workflowId]/components/panel/panel.tsx | 19 ++++++++++++++++++- apps/sim/lib/posthog/events.ts | 5 +++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 55a0508ec1b..afb27cd729a 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -270,10 +270,8 @@ function SignupFormContent({ name: sanitizedName, }, { - fetchOptions: { - headers: { - ...(token ? { 'x-captcha-response': token } : {}), - }, + headers: { + ...(token ? { 'x-captcha-response': token } : {}), }, onError: (ctx) => { logger.error('Signup error:', ctx.error) @@ -282,10 +280,7 @@ function SignupFormContent({ let errorCode = 'unknown' if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) { errorCode = 'user_already_exists' - errorMessage.push( - 'An account with this email already exists. Please sign in instead.' - ) - setEmailError(errorMessage[0]) + setEmailError('An account with this email already exists. Please sign in instead.') } else if ( ctx.error.code?.includes('BAD_REQUEST') || ctx.error.message?.includes('Email and password sign up is not enabled') diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index d76f17ff454..38367339197 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -223,6 +223,14 @@ export function Home({ chatId }: HomeProps = {}) { posthogRef.current = posthog }, [posthog]) + const handleStopGeneration = useCallback(() => { + captureEvent(posthogRef.current, 'task_generation_aborted', { + workspace_id: workspaceId, + view: 'mothership', + }) + stopGeneration() + }, [stopGeneration, workspaceId]) + const handleSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() @@ -334,7 +342,7 @@ export function Home({ chatId }: HomeProps = {}) { defaultValue={initialPrompt} onSubmit={handleSubmit} isSending={isSending} - onStopGeneration={stopGeneration} + onStopGeneration={handleStopGeneration} userId={session?.user?.id} onContextAdd={handleContextAdd} /> @@ -359,7 +367,7 @@ export function Home({ chatId }: HomeProps = {}) { isSending={isSending} isReconnecting={isReconnecting} onSubmit={handleSubmit} - onStopGeneration={stopGeneration} + onStopGeneration={handleStopGeneration} messageQueue={messageQueue} onRemoveQueuedMessage={removeFromQueue} onSendQueuedMessage={sendNow} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 4d485c763ce..da51910789b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { History, Plus, Square } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { BubbleChatClose, @@ -33,6 +34,7 @@ import { import { Lock, Unlock, Upload } from '@/components/emcn/icons' import { VariableIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' +import { captureEvent } from '@/lib/posthog/client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components' @@ -101,6 +103,9 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel const params = useParams() const workspaceId = propWorkspaceId ?? (params.workspaceId as string) + const posthog = usePostHog() + const posthogRef = useRef(posthog) + const panelRef = useRef(null) const fileInputRef = useRef(null) const { activeTab, setActiveTab, panelWidth, _hasHydrated, setHasHydrated } = usePanelStore( @@ -264,6 +269,10 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel loadCopilotChats() }, [loadCopilotChats]) + useEffect(() => { + posthogRef.current = posthog + }, [posthog]) + const handleCopilotSelectChat = useCallback((chat: { id: string; title: string | null }) => { setCopilotChatId(chat.id) setCopilotChatTitle(chat.title) @@ -394,6 +403,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel [copilotEditQueuedMessage] ) + const handleCopilotStopGeneration = useCallback(() => { + captureEvent(posthogRef.current, 'task_generation_aborted', { + workspace_id: workspaceId, + view: 'copilot', + }) + copilotStopGeneration() + }, [copilotStopGeneration, workspaceId]) + const handleCopilotSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() @@ -833,7 +850,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel isSending={copilotIsSending} isReconnecting={copilotIsReconnecting} onSubmit={handleCopilotSubmit} - onStopGeneration={copilotStopGeneration} + onStopGeneration={handleCopilotStopGeneration} messageQueue={copilotMessageQueue} onRemoveQueuedMessage={copilotRemoveFromQueue} onSendQueuedMessage={copilotSendNow} diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 537a9864282..faf9895bf62 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -378,6 +378,11 @@ export interface PostHogEventMap { workspace_id: string } + task_generation_aborted: { + workspace_id: string + view: 'mothership' | 'copilot' + } + task_message_sent: { workspace_id: string has_attachments: boolean From ca2c44b16d1109b40013fb50e463e93baabba7f1 Mon Sep 17 00:00:00 2001 From: claude-dev-code <279680375+claude-dev-code@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:04:07 +0200 Subject: [PATCH 2/2] feat(integrations): add DataForB2B block and tools Add a native DataForB2B integration: search and enrich B2B companies and LinkedIn profiles. Six tools (search people, search companies, reasoning search, typeahead, enrich profile, enrich company) wired into a single multi-operation block, with icon, registries, generated docs and catalog metadata. Co-Authored-By: Claude Opus 4.8 --- apps/docs/components/icons.tsx | 15 + apps/docs/components/ui/icon-mapping.ts | 2 + .../docs/en/integrations/dataforb2b.mdx | 152 +++++++ .../content/docs/en/integrations/meta.json | 1 + apps/sim/blocks/blocks/dataforb2b.ts | 429 ++++++++++++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 15 + apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 43 ++ apps/sim/tools/dataforb2b/enrich_company.ts | 64 +++ apps/sim/tools/dataforb2b/enrich_profile.ts | 129 ++++++ apps/sim/tools/dataforb2b/index.ts | 7 + apps/sim/tools/dataforb2b/reasoning_search.ts | 122 +++++ apps/sim/tools/dataforb2b/search_companies.ts | 86 ++++ apps/sim/tools/dataforb2b/search_people.ts | 86 ++++ apps/sim/tools/dataforb2b/typeahead.ts | 82 ++++ apps/sim/tools/dataforb2b/types.ts | 237 ++++++++++ apps/sim/tools/registry.ts | 14 + 18 files changed, 1489 insertions(+) create mode 100644 apps/docs/content/docs/en/integrations/dataforb2b.mdx create mode 100644 apps/sim/blocks/blocks/dataforb2b.ts create mode 100644 apps/sim/tools/dataforb2b/enrich_company.ts create mode 100644 apps/sim/tools/dataforb2b/enrich_profile.ts create mode 100644 apps/sim/tools/dataforb2b/index.ts create mode 100644 apps/sim/tools/dataforb2b/reasoning_search.ts create mode 100644 apps/sim/tools/dataforb2b/search_companies.ts create mode 100644 apps/sim/tools/dataforb2b/search_people.ts create mode 100644 apps/sim/tools/dataforb2b/typeahead.ts create mode 100644 apps/sim/tools/dataforb2b/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 13fa62588bf..7cb0a353836 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -7981,3 +7981,18 @@ export function EnrowIcon(props: SVGProps) { ) } + +export function DataForB2BIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 22cf6c737db..7ff197d184e 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -45,6 +45,7 @@ import { DagsterIcon, DatabricksIcon, DatadogIcon, + DataForB2BIcon, DatagmaIcon, DaytonaIcon, DevinIcon, @@ -278,6 +279,7 @@ export const blockTypeToIconMap: Record = { dagster: DagsterIcon, databricks: DatabricksIcon, datadog: DatadogIcon, + dataforb2b: DataForB2BIcon, datagma: DatagmaIcon, daytona: DaytonaIcon, devin: DevinIcon, diff --git a/apps/docs/content/docs/en/integrations/dataforb2b.mdx b/apps/docs/content/docs/en/integrations/dataforb2b.mdx new file mode 100644 index 00000000000..8c510551ae9 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/dataforb2b.mdx @@ -0,0 +1,152 @@ +--- +title: DataForB2B +description: Search LinkedIn profiles & companies and find B2B emails +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrates DataForB2B into the workflow — a B2B data, LinkedIn enrichment and email-finder source for lead generation, sales prospecting, recruitment / candidate sourcing and CRM enrichment. Search people and companies on LinkedIn with structured filters (job title, company, industry, location, headcount, funding, skills, school, degree, years of experience), run a natural-language (reasoning) search from an ICP or candidate persona, resolve filter values with typeahead, and enrich a LinkedIn profile or company URL to get the full profile plus verified work email, personal email, phone and GitHub. + + + +## Actions + +### `dataforb2b_search_people` + +Search people and B2B leads by structured filters (job title, current company, location, industry, seniority, skills, school, funding and LinkedIn data) with DataForB2B. Find employees at a company, decision-makers and key contacts for sales prospecting. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `filters` | json | Yes | Structured filters as JSON: \{"op":"and"\|"or","conditions":\[\{"column","type","value","value2"?\}\]\}. Columns: first_name, last_name, profile_location, profile_country, profile_industry, follower_count, keyword, current_company, current_title, current_job_location, current_company_industry, current_company_category, current_company_size, current_company_id, current_employment_type, years_in_current_position, years_at_current_company, current_company_has_funding, current_company_funding_stage, current_company_investor, past_company, past_title, past_job_location, past_company_industry, past_company_size, past_company_id, past_employment_type, years_at_past_company, skill, school, degree, degree_level, field_of_study, language, language_iso, language_proficiency, certification, certification_authority, years_of_experience, num_total_jobs, is_currently_employed. Operators \(type\): =, !=, like, not_like, in, not_in, >, >=, <, <=, between. Use an array value for "in"/"not_in"; for "between" set value \(min\) and value2 \(max\). Example: \{"op":"and","conditions":\[\{"column":"current_title","type":"like","value":"Head of Growth"\},\{"column":"current_company_size","type":"in","value":\["51-200","201-500"\]\}\]\} | +| `count` | number | No | Max results to return \(default 25, max 100\) | +| `offset` | number | No | Pagination offset, e.g. 25, 50 \(default 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of matching people: each with profile, current role/company, location, industry, skills and LinkedIn URL | +| `total` | number | Total number of people matching the filters | +| `count` | number | Number of people returned in this page | + +### `dataforb2b_search_companies` + +Search companies and accounts by structured filters (industry, headcount/size, location, founded year, funding stage/amount, growth, keywords and LinkedIn data) with DataForB2B. Build target account lists for B2B sales and account-based marketing. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `filters` | json | Yes | Structured filters as JSON: \{"op":"and"\|"or","conditions":\[\{"column","type","value","value2"?\}\]\}. Columns: name, tagline, description, domain, universal_name, keyword, industry, employee_count, country_iso_code, city, region, office_country, office_city, office_region, employee_growth_1m, employee_growth_6m, employee_growth_12m, recent_hires_count, founded_year, company_type, follower_count, page_verified, category, last_funding_amount_usd, last_funding_date, funding_stage_normalized, has_funding. Operators \(type\): =, !=, like, not_like, in, not_in, >, >=, <, <=, between. Use an array value for "in"/"not_in"; for "between" set value \(min\) and value2 \(max\). Example: \{"op":"and","conditions":\[\{"column":"industry","type":"like","value":"software development"\},\{"column":"employee_count","type":"between","value":51,"value2":500\},\{"column":"funding_stage_normalized","type":"in","value":\["series_a","series_b"\]\}\]\} | +| `count` | number | No | Max results to return \(default 25, max 100\) | +| `offset` | number | No | Pagination offset, e.g. 25, 50 \(default 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of matching companies: each with name, domain, industry, headcount, location, funding and LinkedIn data | +| `total` | number | Total number of companies matching the filters | +| `count` | number | Number of companies returned in this page | + +### `dataforb2b_reasoning_search` + +Natural-language search for people, leads or companies with DataForB2B. Describe your ideal lead or ICP in plain English (e.g. \ + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `query` | string | No | Natural-language description of the ideal lead/company \(ICP\). Required on the first call. | +| `category` | string | No | Search "people" or "companies" \(default people\) | +| `max_results` | number | No | Max results, 1-100 \(default 25\) | +| `session_id` | string | No | Session id from a previous "needs_input" response, to refine the search. | +| `answers` | json | No | Answers to clarifying questions as a \{question_id: answer\} object, for a needs_input turn. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | "needs_input" when clarification is required, otherwise the search status | +| `results` | json | Array of matching people or companies | +| `total` | number | Total number of matches | +| `count` | number | Number of results returned | +| `session_id` | string | Session id to pass back with answers when status is needs_input | +| `questions` | json | Clarifying questions \[\{id, text, suggestions\}\] when status is needs_input | +| `applied_filters` | json | Structured filters the agent applied; reuse with search/people\|companies for paging | + +### `dataforb2b_typeahead` + +Resolve the exact stored value for a free-text filter (company, industry, job title, skill, school, location, investor, category) before a people or company search on DataForB2B. Use it to normalize free text, or when a search returns few or no results. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `type` | string | Yes | Which kind of value to resolve. One of: company, people_industry, company_industry, category, location, city, region, school, title, skill, investor | +| `q` | string | Yes | Free-text query \(1-100 chars\) to resolve to a stored value | +| `limit` | number | No | Max suggestions, 1-20 \(default 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Suggestions ordered by popularity; each has the exact stored value to use in a search filter | + +### `dataforb2b_enrich_profile` + +Look up and enrich a professional profile from a LinkedIn URL with DataForB2B. Returns the full profile (current role, experience, skills) plus work email, personal email, phone and GitHub. An email finder for lead enrichment. At least one enrich_* flag is used (defaults to the full profile). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `profile_identifier` | string | Yes | LinkedIn profile URL, public id \(e.g. john-doe\) or encoded id \(prof_...\). Encoded id recommended. | +| `enrich_profile` | boolean | No | Return the full profile \(role, experience, skills\) | +| `enrich_work_email` | boolean | No | Find the professional/work email | +| `enrich_personal_email` | boolean | No | Find the personal email | +| `enrich_phone` | boolean | No | Find the phone number | +| `enrich_github` | boolean | No | Find the GitHub profile | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `profile` | json | Enriched profile: identity, current role, experience, skills, education | +| `work_email` | json | Professional/work email \(when requested\) | +| `personal_email` | json | Personal email \(when requested\) | +| `phone` | json | Phone number \(when requested\) | +| `git_profile` | json | GitHub profile \(when requested\) | + +### `dataforb2b_enrich_company` + +Look up and enrich a company with DataForB2B from a company domain, name, slug or LinkedIn URL. Returns firmographics, headcount/size, industry, domain and social profiles. Account enrichment for B2B sales and CRM. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | DataForB2B API key \(https://app.dataforb2b.ai\) | +| `company_identifier` | string | Yes | Company slug \(e.g. google\), domain \(e.g. google.com\), name or LinkedIn company URL. Slugs resolve most reliably. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `company` | json | Enriched company: name, domain, industry, headcount/size, location, founded year, funding and social profiles | + + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 5e08cf704b0..2250e637664 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -42,6 +42,7 @@ "dagster", "databricks", "datadog", + "dataforb2b", "datagma", "daytona", "deployments", diff --git a/apps/sim/blocks/blocks/dataforb2b.ts b/apps/sim/blocks/blocks/dataforb2b.ts new file mode 100644 index 00000000000..72df7c684ff --- /dev/null +++ b/apps/sim/blocks/blocks/dataforb2b.ts @@ -0,0 +1,429 @@ +import { Users } from '@/components/emcn/icons' +import { DataForB2BIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { + DataForB2BEnrichCompanyResponse, + DataForB2BEnrichProfileResponse, + DataForB2BReasoningResponse, + DataForB2BSearchResponse, + DataForB2BTypeaheadResponse, +} from '@/tools/dataforb2b/types' + +type DataForB2BResponse = + | DataForB2BSearchResponse + | DataForB2BReasoningResponse + | DataForB2BTypeaheadResponse + | DataForB2BEnrichProfileResponse + | DataForB2BEnrichCompanyResponse + +const SEARCH_OPS = ['search_people', 'search_companies'] + +export const DataForB2BBlock: BlockConfig = { + type: 'dataforb2b', + name: 'DataForB2B', + description: 'Search LinkedIn profiles & companies and find B2B emails', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrates DataForB2B into the workflow — a B2B data, LinkedIn enrichment and email-finder source for lead generation, sales prospecting, recruitment / candidate sourcing and CRM enrichment. Search people and companies on LinkedIn with structured filters (job title, company, industry, location, headcount, funding, skills, school, degree, years of experience), run a natural-language (reasoning) search from an ICP or candidate persona, resolve filter values with typeahead, and enrich a LinkedIn profile or company URL to get the full profile plus verified work email, personal email, phone and GitHub.', + bestPractices: + 'Use this block for lead generation, LinkedIn enrichment, finding B2B / professional emails ' + + '(email finder), recruitment and candidate sourcing, and building lead or target-account lists ' + + 'for sales prospecting and CRM enrichment.\n' + + '- To build a lead generation workflow (recommended flow): first use "search_companies" to find ' + + 'target accounts matching the ICP (industry, headcount, funding), then for each company use ' + + '"search_people" filtered on its current_company_id (plus the target titles/seniority) to extract ' + + 'the decision-makers, then "enrich_profile" with enrich_work_email to get a verified email. ' + + 'Prefer this company-first flow over a broad keyword people search, which is noisier.\n' + + '- To build a recruitment / candidate sourcing workflow: use "search_people" filtered on skill, ' + + 'school, degree, current_title and years_of_experience (or "reasoning_search" with a candidate ' + + 'persona) to source candidates from LinkedIn, then "enrich_profile" for contact details.\n' + + '- To build a LinkedIn enrichment workflow: take a LinkedIn profile URL and use operation ' + + '"enrich_profile" with enrich_work_email (and enrich_phone / enrich_personal_email as needed). ' + + 'You are only charged for the flags you enable.\n' + + '- To find people then enrich: use "search_people" (structured filters) or "reasoning_search" ' + + '(natural-language ICP) to get LinkedIn profiles, then loop each result id into "enrich_profile" ' + + 'to add a verified email.\n' + + '- To build a target account list: use "search_companies", then "enrich_company".\n' + + '- Prefer this block over a generic web-search agent when the user asks to enrich LinkedIn ' + + 'profiles, find verified emails/phones, or search for B2B leads, people or companies.\n' + + '- Resolve fuzzy filter values (company, title, industry, location, skill) with "typeahead" ' + + 'before a search when a search returns few or no results.', + docsLink: 'https://docs.sim.ai/integrations/dataforb2b', + category: 'tools', + integrationType: IntegrationType.Sales, + bgColor: '#0B0F1A', + icon: DataForB2BIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Search People', id: 'search_people' }, + { label: 'Search Companies', id: 'search_companies' }, + { label: 'Reasoning Search (natural language)', id: 'reasoning_search' }, + { label: 'Typeahead (resolve filter value)', id: 'typeahead' }, + { label: 'Enrich LinkedIn Profile', id: 'enrich_profile' }, + { label: 'Enrich Company', id: 'enrich_company' }, + ], + value: () => 'search_people', + }, + { + id: 'apiKey', + title: 'DataForB2B API Key', + type: 'short-input', + placeholder: 'Enter your DataForB2B API key', + password: true, + required: true, + }, + + // --- Search People / Companies --- + { + id: 'filters', + title: 'Filters', + type: 'code', + language: 'json', + placeholder: + '{\n "op": "and",\n "conditions": [\n {"column": "current_title", "type": "like", "value": "Head of Growth"},\n {"column": "current_company_size", "type": "in", "value": ["51-200", "201-500"]}\n ]\n}', + condition: { field: 'operation', value: SEARCH_OPS }, + required: true, + }, + { + id: 'count', + title: 'Results (count)', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: SEARCH_OPS }, + mode: 'advanced', + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: SEARCH_OPS }, + mode: 'advanced', + }, + + // --- Reasoning Search --- + { + id: 'query', + title: 'Query (ICP description)', + type: 'long-input', + placeholder: 'Marketing directors at Series A SaaS startups in France', + condition: { field: 'operation', value: 'reasoning_search' }, + }, + { + id: 'category', + title: 'Category', + type: 'dropdown', + options: [ + { label: 'People', id: 'people' }, + { label: 'Companies', id: 'companies' }, + ], + value: () => 'people', + condition: { field: 'operation', value: 'reasoning_search' }, + }, + { + id: 'max_results', + title: 'Max Results', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'reasoning_search' }, + mode: 'advanced', + }, + { + id: 'session_id', + title: 'Session ID', + type: 'short-input', + placeholder: 'From a previous needs_input response', + condition: { field: 'operation', value: 'reasoning_search' }, + mode: 'advanced', + }, + { + id: 'answers', + title: 'Answers', + type: 'code', + language: 'json', + placeholder: '{"question_id": "answer"}', + condition: { field: 'operation', value: 'reasoning_search' }, + mode: 'advanced', + }, + + // --- Typeahead --- + { + id: 'type', + title: 'Type', + type: 'dropdown', + options: [ + { label: 'Company', id: 'company' }, + { label: 'People Industry', id: 'people_industry' }, + { label: 'Company Industry', id: 'company_industry' }, + { label: 'Category', id: 'category' }, + { label: 'Location (people)', id: 'location' }, + { label: 'City (company)', id: 'city' }, + { label: 'Region (company)', id: 'region' }, + { label: 'School', id: 'school' }, + { label: 'Job Title', id: 'title' }, + { label: 'Skill', id: 'skill' }, + { label: 'Investor', id: 'investor' }, + ], + value: () => 'company', + condition: { field: 'operation', value: 'typeahead' }, + }, + { + id: 'q', + title: 'Query', + type: 'short-input', + placeholder: 'e.g. salesf...', + condition: { field: 'operation', value: 'typeahead' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: 'typeahead' }, + mode: 'advanced', + }, + + // --- Enrich Profile --- + { + id: 'profile_identifier', + title: 'Profile Identifier', + type: 'short-input', + placeholder: 'LinkedIn URL, public id (john-doe) or prof_...', + condition: { field: 'operation', value: 'enrich_profile' }, + required: true, + }, + { + id: 'enrich_profile', + title: 'Full Profile (role, experience, skills)', + type: 'switch', + condition: { field: 'operation', value: 'enrich_profile' }, + }, + { + id: 'enrich_work_email', + title: 'Work Email', + type: 'switch', + condition: { field: 'operation', value: 'enrich_profile' }, + }, + { + id: 'enrich_personal_email', + title: 'Personal Email', + type: 'switch', + condition: { field: 'operation', value: 'enrich_profile' }, + }, + { + id: 'enrich_phone', + title: 'Phone', + type: 'switch', + condition: { field: 'operation', value: 'enrich_profile' }, + }, + { + id: 'enrich_github', + title: 'GitHub', + type: 'switch', + condition: { field: 'operation', value: 'enrich_profile' }, + }, + + // --- Enrich Company --- + { + id: 'company_identifier', + title: 'Company Identifier', + type: 'short-input', + placeholder: 'Slug (google), domain (google.com), name or LinkedIn URL', + condition: { field: 'operation', value: 'enrich_company' }, + required: true, + }, + ], + tools: { + access: [ + 'dataforb2b_search_people', + 'dataforb2b_search_companies', + 'dataforb2b_reasoning_search', + 'dataforb2b_typeahead', + 'dataforb2b_enrich_profile', + 'dataforb2b_enrich_company', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'search_people': + return 'dataforb2b_search_people' + case 'search_companies': + return 'dataforb2b_search_companies' + case 'reasoning_search': + return 'dataforb2b_reasoning_search' + case 'typeahead': + return 'dataforb2b_typeahead' + case 'enrich_profile': + return 'dataforb2b_enrich_profile' + case 'enrich_company': + return 'dataforb2b_enrich_company' + default: + throw new Error(`Invalid DataForB2B operation: ${params.operation}`) + } + }, + params: (params) => { + const { apiKey, operation, ...rest } = params + const parsed: Record = { apiKey, ...rest } + + // JSON object/array fields can arrive as strings from the UI editor. + for (const field of ['filters', 'answers']) { + const value = (rest as Record)[field] + if (typeof value === 'string' && value.trim() !== '') { + parsed[field] = JSON.parse(value) + } + } + + // Coerce numeric inputs. + for (const field of ['count', 'offset', 'max_results', 'limit']) { + const value = (rest as Record)[field] + if (typeof value === 'string' && value.trim() !== '') { + parsed[field] = Number(value) + } + } + + return parsed + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'DataForB2B operation to perform' }, + }, + outputs: { + results: { + type: 'json', + description: + 'Array of matching people or companies (search_people, search_companies, reasoning_search), or filter-value suggestions (typeahead)', + }, + total: { + type: 'number', + description: 'Total number of matches (search_people, search_companies, reasoning_search)', + }, + count: { + type: 'number', + description: 'Number of results returned in this page', + }, + status: { + type: 'string', + description: 'Reasoning search status — "needs_input" when clarification is required', + }, + session_id: { + type: 'string', + description: 'Reasoning search session id to pass back with answers on a needs_input turn', + }, + questions: { + type: 'json', + description: + 'Reasoning search clarifying questions [{id, text, suggestions}] on a needs_input turn', + }, + applied_filters: { + type: 'json', + description: 'Structured filters the reasoning agent applied; reuse with search for paging', + }, + profile: { + type: 'json', + description: + 'Enriched profile (enrich_profile): identity, role, experience, skills, education', + }, + work_email: { type: 'json', description: 'Work email (enrich_profile, when requested)' }, + personal_email: { + type: 'json', + description: 'Personal email (enrich_profile, when requested)', + }, + phone: { type: 'json', description: 'Phone number (enrich_profile, when requested)' }, + git_profile: { type: 'json', description: 'GitHub profile (enrich_profile, when requested)' }, + company: { + type: 'json', + description: + 'Enriched company (enrich_company): name, domain, industry, headcount, location, funding, socials', + }, + }, +} + +export const DataForB2BBlockMeta = { + tags: ['enrichment', 'sales-engagement', 'hiring'], + url: 'https://dataforb2b.ai', + templates: [ + { + icon: DataForB2BIcon, + title: 'LinkedIn enrichment workflow', + prompt: + 'Build a LinkedIn enrichment workflow: take a LinkedIn profile URL (or a list of them), enrich each with DataForB2B to get the full LinkedIn profile plus a verified work email, personal email and phone, and write the enriched leads to a table.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['linkedin', 'enrichment', 'email-finder', 'sales'], + featured: true, + }, + { + icon: Users, + title: 'Lead generation workflow', + prompt: + 'Build a lead generation workflow with DataForB2B: first run a company search for target accounts matching my ICP (industry, headcount, funding), then for each company run a people search on its current_company_id to extract the decision-makers (relevant titles/seniority), enrich each with a verified work email, and write the leads to a table.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['lead-generation', 'enrichment', 'sales', 'automation'], + featured: true, + }, + { + icon: Users, + title: 'Recruitment / candidate sourcing workflow', + prompt: + 'Build a recruitment workflow that sources candidates on LinkedIn with DataForB2B by skill, job title, school and years of experience, enriches each candidate with a verified email and phone, and writes the shortlist to a table for the hiring team.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['recruitment', 'sourcing', 'hiring', 'enrichment'], + }, + { + icon: DataForB2BIcon, + title: 'Target account builder', + prompt: + 'Create a workflow that runs a DataForB2B company search for accounts matching my ICP — industry, headcount and funding stage — enriches each company, and writes the target account list to a table for the SDR team.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'research', 'automation'], + }, + { + icon: DataForB2BIcon, + title: 'LinkedIn URL to verified email', + prompt: + 'Build a workflow that takes a LinkedIn profile URL, enriches it with DataForB2B to get the full profile plus a verified work email and phone, and creates or updates the matching contact in my CRM.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['sales', 'enrichment', 'crm'], + }, + { + icon: Users, + title: 'Buying committee mapper', + prompt: + 'Create a workflow that takes a target company, runs a DataForB2B people search across the relevant titles at that company, enriches each contact with a verified email, and writes the mapped buying committee to a table.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'research', 'crm'], + }, + ], + skills: [ + { + name: 'linkedin-enrichment', + description: + 'Build a LinkedIn enrichment workflow with DataForB2B: turn a LinkedIn profile URL into a full profile plus a verified work email, personal email and phone.', + content: + '# LinkedIn enrichment with DataForB2B\n\n' + + 'Use the DataForB2B block to build a LinkedIn enrichment workflow.\n\n' + + '## Enrich a single LinkedIn profile\n' + + '1. Add a DataForB2B block, operation **Enrich LinkedIn Profile**.\n' + + '2. Set `profile_identifier` to the LinkedIn URL, public id (e.g. `john-doe`) or encoded id (`prof_...`).\n' + + '3. Toggle the data you need: `enrich_profile` (full profile), `enrich_work_email`, `enrich_personal_email`, `enrich_phone`, `enrich_github`. You are only charged for what you request.\n' + + '4. Read the outputs: `profile`, `work_email`, `personal_email`, `phone`, `git_profile`.\n\n' + + '## Find the people first, then enrich\n' + + '- Use operation **Search People** (or **Reasoning Search** for a natural-language ICP) to get a list of LinkedIn profiles, then loop each `results[].id` into **Enrich LinkedIn Profile** to add a verified email.\n' + + '- Write the enriched leads to a table or push them to your CRM.\n\n' + + '## Tips\n' + + '- Resolve fuzzy filter values (company, title, industry, location) with **Typeahead** before searching.\n' + + '- Enrich an account with **Enrich Company** from a domain, slug or LinkedIn company URL.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 96d2f7f2f6e..0fc51b1defc 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -46,6 +46,7 @@ import { CursorBlock, CursorBlockMeta, CursorV2Block } from '@/blocks/blocks/cur import { DagsterBlock, DagsterBlockMeta } from '@/blocks/blocks/dagster' import { DatabricksBlock, DatabricksBlockMeta } from '@/blocks/blocks/databricks' import { DatadogBlock, DatadogBlockMeta } from '@/blocks/blocks/datadog' +import { DataForB2BBlock, DataForB2BBlockMeta } from '@/blocks/blocks/dataforb2b' import { DatagmaBlock, DatagmaBlockMeta } from '@/blocks/blocks/datagma' import { DaytonaBlock, DaytonaBlockMeta } from '@/blocks/blocks/daytona' import { DeploymentsBlock } from '@/blocks/blocks/deployments' @@ -350,6 +351,7 @@ const BLOCK_REGISTRY: Record = { apify: ApifyBlock, appconfig: AppConfigBlock, apollo: ApolloBlock, + dataforb2b: DataForB2BBlock, arxiv: ArxivBlock, asana: AsanaBlock, ashby: AshbyBlock, @@ -659,6 +661,7 @@ const BLOCK_META_REGISTRY: Record = { apify: ApifyBlockMeta, appconfig: AppConfigBlockMeta, apollo: ApolloBlockMeta, + dataforb2b: DataForB2BBlockMeta, arxiv: ArxivBlockMeta, asana: AsanaBlockMeta, ashby: AshbyBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 89f0b818ddc..4fdb85d1b72 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -7951,3 +7951,18 @@ export function EnrowIcon(props: SVGProps) { ) } + +export function DataForB2BIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index f290946186e..5b5205495cb 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -45,6 +45,7 @@ import { DagsterIcon, DatabricksIcon, DatadogIcon, + DataForB2BIcon, DatagmaIcon, DaytonaIcon, DevinIcon, @@ -274,6 +275,7 @@ export const blockTypeToIconMap: Record = { dagster: DagsterIcon, databricks: DatabricksIcon, datadog: DatadogIcon, + dataforb2b: DataForB2BIcon, datagma: DatagmaIcon, daytona: DaytonaIcon, devin: DevinIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 45ecb7990fd..0f02f8d8afe 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -3817,6 +3817,49 @@ "integrationType": "observability", "tags": ["monitoring", "incident-management", "error-tracking"] }, + { + "type": "dataforb2b", + "slug": "dataforb2b", + "name": "DataForB2B", + "description": "Search LinkedIn profiles & companies and find B2B emails", + "longDescription": "Integrates DataForB2B into the workflow — a B2B data, LinkedIn enrichment and email-finder source for lead generation, sales prospecting, recruitment / candidate sourcing and CRM enrichment. Search people and companies on LinkedIn with structured filters (job title, company, industry, location, headcount, funding, skills, school, degree, years of experience), run a natural-language (reasoning) search from an ICP or candidate persona, resolve filter values with typeahead, and enrich a LinkedIn profile or company URL to get the full profile plus verified work email, personal email, phone and GitHub.", + "bgColor": "#0B0F1A", + "iconName": "DataForB2BIcon", + "docsUrl": "https://docs.sim.ai/integrations/dataforb2b", + "operations": [ + { + "name": "Search People", + "description": "Search people and B2B leads by structured filters (job title, current company, location, industry, seniority, skills, school, funding and LinkedIn data) with DataForB2B. Find employees at a company, decision-makers and key contacts for sales prospecting." + }, + { + "name": "Search Companies", + "description": "Search companies and accounts by structured filters (industry, headcount/size, location, founded year, funding stage/amount, growth, keywords and LinkedIn data) with DataForB2B. Build target account lists for B2B sales and account-based marketing." + }, + { + "name": "Reasoning Search (natural language)", + "description": "Natural-language search for people, leads or companies with DataForB2B. Describe your ideal lead or ICP in plain English (e.g. \\" + }, + { + "name": "Typeahead (resolve filter value)", + "description": "Resolve the exact stored value for a free-text filter (company, industry, job title, skill, school, location, investor, category) before a people or company search on DataForB2B. Use it to normalize free text, or when a search returns few or no results." + }, + { + "name": "Enrich LinkedIn Profile", + "description": "Look up and enrich a professional profile from a LinkedIn URL with DataForB2B. Returns the full profile (current role, experience, skills) plus work email, personal email, phone and GitHub. An email finder for lead enrichment. At least one enrich_* flag is used (defaults to the full profile)." + }, + { + "name": "Enrich Company", + "description": "Look up and enrich a company with DataForB2B from a company domain, name, slug or LinkedIn URL. Returns firmographics, headcount/size, industry, domain and social profiles. Account enrichment for B2B sales and CRM." + } + ], + "operationCount": 6, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement", "hiring"] + }, { "type": "datagma", "slug": "datagma", diff --git a/apps/sim/tools/dataforb2b/enrich_company.ts b/apps/sim/tools/dataforb2b/enrich_company.ts new file mode 100644 index 00000000000..e43be90d553 --- /dev/null +++ b/apps/sim/tools/dataforb2b/enrich_company.ts @@ -0,0 +1,64 @@ +import { + API_BASE, + authHeaders, + type DataForB2BEnrichCompanyParams, + type DataForB2BEnrichCompanyResponse, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +export const dataforb2bEnrichCompanyTool: ToolConfig< + DataForB2BEnrichCompanyParams, + DataForB2BEnrichCompanyResponse +> = { + id: 'dataforb2b_enrich_company', + name: 'DataForB2B Enrich Company', + description: + 'Look up and enrich a company with DataForB2B from a company domain, name, slug or LinkedIn URL. Returns firmographics, headcount/size, industry, domain and social profiles. Account enrichment for B2B sales and CRM.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + company_identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Company slug (e.g. google), domain (e.g. google.com), name or LinkedIn company URL. Slugs resolve most reliably.', + }, + }, + + request: { + url: `${API_BASE}/enrich/company`, + method: 'POST', + headers: (params) => authHeaders(params.apiKey), + body: (params) => ({ company_identifier: params.company_identifier }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + // The API wraps the result as { company: {...} }. + return { + success: true, + output: { + company: data.company ?? data, + }, + } + }, + + outputs: { + company: { + type: 'json', + description: + 'Enriched company: name, domain, industry, headcount/size, location, founded year, funding and social profiles', + }, + }, +} diff --git a/apps/sim/tools/dataforb2b/enrich_profile.ts b/apps/sim/tools/dataforb2b/enrich_profile.ts new file mode 100644 index 00000000000..426ba4711ac --- /dev/null +++ b/apps/sim/tools/dataforb2b/enrich_profile.ts @@ -0,0 +1,129 @@ +import { + API_BASE, + authHeaders, + type DataForB2BEnrichProfileParams, + type DataForB2BEnrichProfileResponse, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +const ENRICH_FLAGS = [ + 'enrich_profile', + 'enrich_work_email', + 'enrich_personal_email', + 'enrich_phone', + 'enrich_github', +] as const + +export const dataforb2bEnrichProfileTool: ToolConfig< + DataForB2BEnrichProfileParams, + DataForB2BEnrichProfileResponse +> = { + id: 'dataforb2b_enrich_profile', + name: 'DataForB2B Enrich LinkedIn Profile', + description: + 'Look up and enrich a professional profile from a LinkedIn URL with DataForB2B. Returns the full profile (current role, experience, skills) plus work email, personal email, phone and GitHub. An email finder for lead enrichment. At least one enrich_* flag is used (defaults to the full profile).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + profile_identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'LinkedIn profile URL, public id (e.g. john-doe) or encoded id (prof_...). Encoded id recommended.', + }, + enrich_profile: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return the full profile (role, experience, skills)', + }, + enrich_work_email: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Find the professional/work email', + }, + enrich_personal_email: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Find the personal email', + }, + enrich_phone: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Find the phone number', + }, + enrich_github: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Find the GitHub profile', + }, + }, + + request: { + url: `${API_BASE}/enrich/profile`, + method: 'POST', + headers: (params) => authHeaders(params.apiKey), + body: (params) => { + const body: Record = { profile_identifier: params.profile_identifier } + let anyFlag = false + for (const flag of ENRICH_FLAGS) { + if (params[flag]) { + body[flag] = true + anyFlag = true + } + } + // The API requires at least one enrich_* flag — default to the full profile. + if (!anyFlag) body.enrich_profile = true + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + // The API returns { profile, work_email, personal_email, phone, git_profile }. + return { + success: true, + output: { + profile: data.profile ?? data, + work_email: data.work_email ?? null, + personal_email: data.personal_email ?? null, + phone: data.phone ?? null, + git_profile: data.git_profile ?? null, + }, + } + }, + + outputs: { + profile: { + type: 'json', + description: 'Enriched profile: identity, current role, experience, skills, education', + }, + work_email: { + type: 'json', + description: 'Professional/work email (when requested)', + optional: true, + }, + personal_email: { + type: 'json', + description: 'Personal email (when requested)', + optional: true, + }, + phone: { type: 'json', description: 'Phone number (when requested)', optional: true }, + git_profile: { type: 'json', description: 'GitHub profile (when requested)', optional: true }, + }, +} diff --git a/apps/sim/tools/dataforb2b/index.ts b/apps/sim/tools/dataforb2b/index.ts new file mode 100644 index 00000000000..8112fcfcc87 --- /dev/null +++ b/apps/sim/tools/dataforb2b/index.ts @@ -0,0 +1,7 @@ +export { dataforb2bEnrichCompanyTool } from './enrich_company' +export { dataforb2bEnrichProfileTool } from './enrich_profile' +export { dataforb2bReasoningSearchTool } from './reasoning_search' +export { dataforb2bSearchCompaniesTool } from './search_companies' +export { dataforb2bSearchPeopleTool } from './search_people' +export { dataforb2bTypeaheadTool } from './typeahead' +export type * from './types' diff --git a/apps/sim/tools/dataforb2b/reasoning_search.ts b/apps/sim/tools/dataforb2b/reasoning_search.ts new file mode 100644 index 00000000000..0cd0f692620 --- /dev/null +++ b/apps/sim/tools/dataforb2b/reasoning_search.ts @@ -0,0 +1,122 @@ +import { + API_BASE, + authHeaders, + type DataForB2BReasoningParams, + type DataForB2BReasoningResponse, + parseJson, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +export const dataforb2bReasoningSearchTool: ToolConfig< + DataForB2BReasoningParams, + DataForB2BReasoningResponse +> = { + id: 'dataforb2b_reasoning_search', + name: 'DataForB2B Reasoning Search', + description: + 'Natural-language search for people, leads or companies with DataForB2B. Describe your ideal lead or ICP in plain English (e.g. \'marketing directors at Series A SaaS startups in France\'). If the response status is "needs_input", call again with session_id and an answers object.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Natural-language description of the ideal lead/company (ICP). Required on the first call.', + }, + category: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search "people" or "companies" (default people)', + }, + max_results: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max results, 1-100 (default 25)', + }, + session_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Session id from a previous "needs_input" response, to refine the search.', + }, + answers: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Answers to clarifying questions as a {question_id: answer} object, for a needs_input turn.', + }, + }, + + request: { + url: `${API_BASE}/search/reasoning`, + method: 'POST', + headers: (params) => authHeaders(params.apiKey), + body: (params) => { + const body: Record = { + category: params.category || 'people', + max_results: Number(params.max_results) || 25, + enrich_live: false, + } + if (params.query) body.query = params.query + if (params.session_id) body.session_id = params.session_id + const answers = parseJson(params.answers) + if (answers) body.answers = answers + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + return { + success: true, + output: { + status: data.status ?? 'ok', + results: data.results || [], + total: data.total ?? 0, + count: data.count ?? (data.results?.length || 0), + session_id: data.session_id ?? null, + questions: data.questions || [], + applied_filters: data.applied_filters ?? null, + }, + } + }, + + outputs: { + status: { + type: 'string', + description: '"needs_input" when clarification is required, otherwise the search status', + }, + results: { type: 'json', description: 'Array of matching people or companies' }, + total: { type: 'number', description: 'Total number of matches' }, + count: { type: 'number', description: 'Number of results returned' }, + session_id: { + type: 'string', + description: 'Session id to pass back with answers when status is needs_input', + optional: true, + }, + questions: { + type: 'json', + description: 'Clarifying questions [{id, text, suggestions}] when status is needs_input', + }, + applied_filters: { + type: 'json', + description: + 'Structured filters the agent applied; reuse with search/people|companies for paging', + }, + }, +} diff --git a/apps/sim/tools/dataforb2b/search_companies.ts b/apps/sim/tools/dataforb2b/search_companies.ts new file mode 100644 index 00000000000..6fb0acac074 --- /dev/null +++ b/apps/sim/tools/dataforb2b/search_companies.ts @@ -0,0 +1,86 @@ +import { + API_BASE, + authHeaders, + type DataForB2BSearchParams, + type DataForB2BSearchResponse, + parseJson, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +export const dataforb2bSearchCompaniesTool: ToolConfig< + DataForB2BSearchParams, + DataForB2BSearchResponse +> = { + id: 'dataforb2b_search_companies', + name: 'DataForB2B Search Companies', + description: + 'Search companies and accounts by structured filters (industry, headcount/size, location, founded year, funding stage/amount, growth, keywords and LinkedIn data) with DataForB2B. Build target account lists for B2B sales and account-based marketing.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + filters: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Structured filters as JSON: {"op":"and"|"or","conditions":[{"column","type","value","value2"?}]}. Columns: name, tagline, description, domain, universal_name, keyword, industry, employee_count, country_iso_code, city, region, office_country, office_city, office_region, employee_growth_1m, employee_growth_6m, employee_growth_12m, recent_hires_count, founded_year, company_type, follower_count, page_verified, category, last_funding_amount_usd, last_funding_date, funding_stage_normalized, has_funding. Operators (type): =, !=, like, not_like, in, not_in, >, >=, <, <=, between. Use an array value for "in"/"not_in"; for "between" set value (min) and value2 (max). Example: {"op":"and","conditions":[{"column":"industry","type":"like","value":"software development"},{"column":"employee_count","type":"between","value":51,"value2":500},{"column":"funding_stage_normalized","type":"in","value":["series_a","series_b"]}]}', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max results to return (default 25, max 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination offset, e.g. 25, 50 (default 0)', + }, + }, + + request: { + url: `${API_BASE}/search/companies`, + method: 'POST', + headers: (params) => authHeaders(params.apiKey), + body: (params) => ({ + filters: parseJson(params.filters), + count: Math.min(Number(params.count) || 25, 100), + offset: Number(params.offset) || 0, + enrich_live: false, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + const results = data.results || [] + return { + success: true, + output: { + results, + total: data.total ?? 0, + count: data.count ?? results.length, + }, + } + }, + + outputs: { + results: { + type: 'json', + description: + 'Array of matching companies: each with name, domain, industry, headcount, location, funding and LinkedIn data', + }, + total: { type: 'number', description: 'Total number of companies matching the filters' }, + count: { type: 'number', description: 'Number of companies returned in this page' }, + }, +} diff --git a/apps/sim/tools/dataforb2b/search_people.ts b/apps/sim/tools/dataforb2b/search_people.ts new file mode 100644 index 00000000000..d1541d6890f --- /dev/null +++ b/apps/sim/tools/dataforb2b/search_people.ts @@ -0,0 +1,86 @@ +import { + API_BASE, + authHeaders, + type DataForB2BSearchParams, + type DataForB2BSearchResponse, + parseJson, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +export const dataforb2bSearchPeopleTool: ToolConfig< + DataForB2BSearchParams, + DataForB2BSearchResponse +> = { + id: 'dataforb2b_search_people', + name: 'DataForB2B Search People', + description: + 'Search people and B2B leads by structured filters (job title, current company, location, industry, seniority, skills, school, funding and LinkedIn data) with DataForB2B. Find employees at a company, decision-makers and key contacts for sales prospecting.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + filters: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Structured filters as JSON: {"op":"and"|"or","conditions":[{"column","type","value","value2"?}]}. Columns: first_name, last_name, profile_location, profile_country, profile_industry, follower_count, keyword, current_company, current_title, current_job_location, current_company_industry, current_company_category, current_company_size, current_company_id, current_employment_type, years_in_current_position, years_at_current_company, current_company_has_funding, current_company_funding_stage, current_company_investor, past_company, past_title, past_job_location, past_company_industry, past_company_size, past_company_id, past_employment_type, years_at_past_company, skill, school, degree, degree_level, field_of_study, language, language_iso, language_proficiency, certification, certification_authority, years_of_experience, num_total_jobs, is_currently_employed. Operators (type): =, !=, like, not_like, in, not_in, >, >=, <, <=, between. Use an array value for "in"/"not_in"; for "between" set value (min) and value2 (max). Example: {"op":"and","conditions":[{"column":"current_title","type":"like","value":"Head of Growth"},{"column":"current_company_size","type":"in","value":["51-200","201-500"]}]}', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max results to return (default 25, max 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination offset, e.g. 25, 50 (default 0)', + }, + }, + + request: { + url: `${API_BASE}/search/people`, + method: 'POST', + headers: (params) => authHeaders(params.apiKey), + body: (params) => ({ + filters: parseJson(params.filters), + count: Math.min(Number(params.count) || 25, 100), + offset: Number(params.offset) || 0, + enrich_live: false, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + const results = data.results || [] + return { + success: true, + output: { + results, + total: data.total ?? 0, + count: data.count ?? results.length, + }, + } + }, + + outputs: { + results: { + type: 'json', + description: + 'Array of matching people: each with profile, current role/company, location, industry, skills and LinkedIn URL', + }, + total: { type: 'number', description: 'Total number of people matching the filters' }, + count: { type: 'number', description: 'Number of people returned in this page' }, + }, +} diff --git a/apps/sim/tools/dataforb2b/typeahead.ts b/apps/sim/tools/dataforb2b/typeahead.ts new file mode 100644 index 00000000000..7d5a18a2b0b --- /dev/null +++ b/apps/sim/tools/dataforb2b/typeahead.ts @@ -0,0 +1,82 @@ +import { + API_BASE, + authHeaders, + type DataForB2BTypeaheadParams, + type DataForB2BTypeaheadResponse, +} from '@/tools/dataforb2b/types' +import type { ToolConfig } from '@/tools/types' + +export const dataforb2bTypeaheadTool: ToolConfig< + DataForB2BTypeaheadParams, + DataForB2BTypeaheadResponse +> = { + id: 'dataforb2b_typeahead', + name: 'DataForB2B Typeahead', + description: + 'Resolve the exact stored value for a free-text filter (company, industry, job title, skill, school, location, investor, category) before a people or company search on DataForB2B. Use it to normalize free text, or when a search returns few or no results.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'DataForB2B API key (https://app.dataforb2b.ai)', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Which kind of value to resolve. One of: company, people_industry, company_industry, category, location, city, region, school, title, skill, investor', + }, + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Free-text query (1-100 chars) to resolve to a stored value', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max suggestions, 1-20 (default 20)', + }, + }, + + request: { + url: (params) => { + const limit = Math.max(1, Math.min(Number(params.limit) || 20, 20)) + const qs = new URLSearchParams({ + type: String(params.type), + q: String(params.q), + limit: String(limit), + }) + return `${API_BASE}/typeahead?${qs.toString()}` + }, + method: 'GET', + headers: (params) => authHeaders(params.apiKey), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DataForB2B API error: ${response.status} - ${errorText}`) + } + const data = await response.json() + return { + success: true, + output: { + results: data.results || [], + }, + } + }, + + outputs: { + results: { + type: 'json', + description: + 'Suggestions ordered by popularity; each has the exact stored value to use in a search filter', + }, + }, +} diff --git a/apps/sim/tools/dataforb2b/types.ts b/apps/sim/tools/dataforb2b/types.ts new file mode 100644 index 00000000000..8b662c0b34e --- /dev/null +++ b/apps/sim/tools/dataforb2b/types.ts @@ -0,0 +1,237 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Shared types and constants for the DataForB2B tools. + * + * DataForB2B (https://api.dataforb2b.ai) is a B2B data API for searching and + * enriching companies and professional (LinkedIn) profiles. Auth is the + * `api_key` request header; get a key at https://app.dataforb2b.ai. + * + * Keeps parity with the Dify / Flowise / Langflow / n8n integrations: the same + * six endpoints and the same `{op, conditions:[{column,type,value,value2?}]}` + * filter shape. The Dify plugin is the source of truth for the contract. + */ + +export const API_BASE = 'https://api.dataforb2b.ai' + +export const FILTER_OPERATORS = [ + '=', + '!=', + 'like', + 'not_like', + 'in', + 'not_in', + '>', + '>=', + '<', + '<=', + 'between', +] as const + +// Source of truth: reference_dataforb2b_search_columns / the Dify plugin. +export const PEOPLE_COLUMNS = [ + 'first_name', + 'last_name', + 'profile_location', + 'profile_country', + 'profile_industry', + 'follower_count', + 'keyword', + 'current_company', + 'current_title', + 'current_job_location', + 'current_company_industry', + 'current_company_category', + 'current_company_size', + 'current_company_id', + 'current_employment_type', + 'years_in_current_position', + 'years_at_current_company', + 'current_company_has_funding', + 'current_company_funding_stage', + 'current_company_investor', + 'past_company', + 'past_title', + 'past_job_location', + 'past_company_industry', + 'past_company_size', + 'past_company_id', + 'past_employment_type', + 'years_at_past_company', + 'skill', + 'school', + 'degree', + 'degree_level', + 'field_of_study', + 'language', + 'language_iso', + 'language_proficiency', + 'certification', + 'certification_authority', + 'years_of_experience', + 'num_total_jobs', + 'is_currently_employed', +] as const + +export const COMPANY_COLUMNS = [ + 'name', + 'tagline', + 'description', + 'domain', + 'universal_name', + 'keyword', + 'industry', + 'employee_count', + 'country_iso_code', + 'city', + 'region', + 'office_country', + 'office_city', + 'office_region', + 'employee_growth_1m', + 'employee_growth_6m', + 'employee_growth_12m', + 'recent_hires_count', + 'founded_year', + 'company_type', + 'follower_count', + 'page_verified', + 'category', + 'last_funding_amount_usd', + 'last_funding_date', + 'funding_stage_normalized', + 'has_funding', +] as const + +export const TYPEAHEAD_TYPES = [ + 'company', + 'people_industry', + 'company_industry', + 'category', + 'location', + 'city', + 'region', + 'school', + 'title', + 'skill', + 'investor', +] as const + +export interface DataForB2BCondition { + column: string + type: string + value: unknown + value2?: unknown +} + +export interface DataForB2BFilters { + op?: 'and' | 'or' + conditions: DataForB2BCondition[] +} + +// --- Search (people / companies) --------------------------------------------- + +export interface DataForB2BSearchParams { + apiKey: string + filters: DataForB2BFilters | string + count?: number + offset?: number +} + +export interface DataForB2BSearchResponse extends ToolResponse { + output: { + results: unknown[] + total: number + count: number + } +} + +// --- Reasoning search -------------------------------------------------------- + +export interface DataForB2BReasoningParams { + apiKey: string + query?: string + category?: 'people' | 'companies' + max_results?: number + session_id?: string + answers?: Record | string +} + +export interface DataForB2BReasoningResponse extends ToolResponse { + output: { + status: string + results: unknown[] + total: number + count: number + session_id: string | null + questions: unknown[] + applied_filters: unknown + } +} + +// --- Typeahead --------------------------------------------------------------- + +export interface DataForB2BTypeaheadParams { + apiKey: string + type: string + q: string + limit?: number +} + +export interface DataForB2BTypeaheadResponse extends ToolResponse { + output: { + results: unknown[] + } +} + +// --- Enrich profile ---------------------------------------------------------- + +export interface DataForB2BEnrichProfileParams { + apiKey: string + profile_identifier: string + enrich_profile?: boolean + enrich_work_email?: boolean + enrich_personal_email?: boolean + enrich_phone?: boolean + enrich_github?: boolean +} + +export interface DataForB2BEnrichProfileResponse extends ToolResponse { + output: { + profile: Record + work_email: unknown + personal_email: unknown + phone: unknown + git_profile: unknown + } +} + +// --- Enrich company ---------------------------------------------------------- + +export interface DataForB2BEnrichCompanyParams { + apiKey: string + company_identifier: string +} + +export interface DataForB2BEnrichCompanyResponse extends ToolResponse { + output: { + company: Record + } +} + +// --- Shared helpers ---------------------------------------------------------- + +/** Accept either a parsed object or a JSON string for object/array params. */ +export function parseJson(value: T | string | undefined): T | undefined { + if (value === undefined || value === null || value === '') return undefined + if (typeof value === 'string') return JSON.parse(value) as T + return value +} + +export function authHeaders(apiKey: string): Record { + return { + api_key: apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 18ec21ad39b..2104e0e698a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -635,6 +635,14 @@ import { datadogSendLogsTool, datadogSubmitMetricsTool, } from '@/tools/datadog' +import { + dataforb2bEnrichCompanyTool, + dataforb2bEnrichProfileTool, + dataforb2bReasoningSearchTool, + dataforb2bSearchCompaniesTool, + dataforb2bSearchPeopleTool, + dataforb2bTypeaheadTool, +} from '@/tools/dataforb2b' import { datagmaEnrichCompanyTool, datagmaEnrichPersonTool, @@ -5932,6 +5940,12 @@ export const tools: Record = { apollo_task_create: apolloTaskCreateTool, apollo_task_search: apolloTaskSearchTool, apollo_email_accounts: apolloEmailAccountsTool, + dataforb2b_search_people: dataforb2bSearchPeopleTool, + dataforb2b_search_companies: dataforb2bSearchCompaniesTool, + dataforb2b_reasoning_search: dataforb2bReasoningSearchTool, + dataforb2b_typeahead: dataforb2bTypeaheadTool, + dataforb2b_enrich_profile: dataforb2bEnrichProfileTool, + dataforb2b_enrich_company: dataforb2bEnrichCompanyTool, mistral_parser: mistralParserTool, mistral_parser_v2: mistralParserV2Tool, mistral_parser_v3: mistralParserV3Tool,