From 3e3698f61d36836edc5d779f961bd1d9e1e7da72 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 27 May 2026 10:40:09 -0700 Subject: [PATCH 1/4] feat(enrichment): workflow Enrichment block + /api/tools/enrichment/run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a generic Enrichment workflow block that runs a code-defined enrichment (Work Email, Phone Number, Company Domain, Company Info, …) and returns its outputs — usable in workflows, not just tables. - New internal endpoint POST /api/tools/enrichment/run (checkInternalAuth + contract) runs the same runEnrichment provider cascade; injects the workspace's hosted/BYOK key via executeTool. - New tool enrichment_run posts to it and surfaces hosted-key cost on the output so the workflow logging session bills it. - New block blocks/blocks/enrichment.ts generated from the enrichment registry: operation dropdown = enrichments, per-enrichment conditional inputs, union of conditional outputs. New registry entries appear automatically. - EnrichmentRunContext.tableId/rowId made optional (workflow path has no row). - Register tool + block; bump api-validation route baseline; add EnrichmentIcon and generated docs page. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/content/docs/en/tools/enrichment.mdx | 48 +++++++ apps/docs/content/docs/en/tools/meta.json | 1 + .../sim/app/api/tools/enrichment/run/route.ts | 66 ++++++++++ apps/sim/blocks/blocks/enrichment.ts | 119 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 16 +++ apps/sim/enrichments/run.ts | 6 +- apps/sim/enrichments/types.ts | 10 +- .../sim/lib/api/contracts/tools/enrichment.ts | 29 +++++ apps/sim/tools/enrichment/index.ts | 1 + apps/sim/tools/enrichment/run.ts | 105 ++++++++++++++++ apps/sim/tools/enrichment/types.ts | 17 +++ apps/sim/tools/registry.ts | 2 + scripts/check-api-validation-contracts.ts | 4 +- 14 files changed, 419 insertions(+), 7 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/enrichment.mdx create mode 100644 apps/sim/app/api/tools/enrichment/run/route.ts create mode 100644 apps/sim/blocks/blocks/enrichment.ts create mode 100644 apps/sim/lib/api/contracts/tools/enrichment.ts create mode 100644 apps/sim/tools/enrichment/index.ts create mode 100644 apps/sim/tools/enrichment/run.ts create mode 100644 apps/sim/tools/enrichment/types.ts diff --git a/apps/docs/content/docs/en/tools/enrichment.mdx b/apps/docs/content/docs/en/tools/enrichment.mdx new file mode 100644 index 00000000000..06975f05a01 --- /dev/null +++ b/apps/docs/content/docs/en/tools/enrichment.mdx @@ -0,0 +1,48 @@ +--- +title: Enrichment +description: Enrich data with a Sim enrichment +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments. + + + +## Tools + +### `enrichment_run` + +Run a Sim enrichment (e.g. Work Email, Phone Number) and return its outputs + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `enrichmentId` | string | Yes | Registry enrichment id \(e.g. "work-email"\) | +| `inputs` | json | Yes | Map of the enrichment's input ids to values | + +#### Output + +The exact fields depend on which enrichment ran. `matched` and `provider` are always present. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matched` | boolean | Whether the enrichment found a result | +| `provider` | string | Provider whose result was returned (e.g. "Hunter", "People Data Labs"); `null` on no match | +| `email` | string | Work email address (Work Email enrichment) | +| `phone` | string | Phone number (Phone Number enrichment) | +| `domain` | string | Website domain (Company Domain enrichment) | +| `industry` | string | Industry (Company Info enrichment) | +| `employeeCount` | number | Employee count (Company Info enrichment) | +| `foundedYear` | number | Founded year (Company Info enrichment) | +| `description` | string | Company description (Company Info enrichment) | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 4ebb74aacca..f58d910c8b6 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -49,6 +49,7 @@ "elevenlabs", "emailbison", "enrich", + "enrichment", "evernote", "exa", "extend", diff --git a/apps/sim/app/api/tools/enrichment/run/route.ts b/apps/sim/app/api/tools/enrichment/run/route.ts new file mode 100644 index 00000000000..586dc7f735e --- /dev/null +++ b/apps/sim/app/api/tools/enrichment/run/route.ts @@ -0,0 +1,66 @@ +import { createLogger } from '@sim/logger' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { runEnrichmentContract } from '@/lib/api/contracts/tools/enrichment' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getEnrichment } from '@/enrichments/registry' +import { runEnrichment } from '@/enrichments/run' + +const logger = createLogger('EnrichmentRunAPI') + +/** + * POST /api/tools/enrichment/run + * + * Runs a registry enrichment's provider cascade and returns its outputs. Backs + * the Enrichment workflow block; called server-to-server by the executor, so it + * authenticates with the internal token. The cascade injects the workspace's + * BYOK / hosted key via `executeTool` using `workspaceId`. + */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest( + runEnrichmentContract, + request, + {}, + { + validationErrorResponse: (error) => + NextResponse.json( + { error: getValidationErrorMessage(error, 'Invalid request') }, + { + status: 400, + } + ), + } + ) + if (!parsed.success) return parsed.response + + const { enrichmentId, inputs, workspaceId } = parsed.data.body + const enrichment = getEnrichment(enrichmentId) + if (!enrichment) { + return NextResponse.json({ error: `Unknown enrichment "${enrichmentId}"` }, { status: 400 }) + } + + const { result, cost, error, provider } = await runEnrichment(enrichment, inputs, { + workspaceId, + signal: request.signal, + }) + + logger.info('Enrichment block run', { + enrichmentId, + matched: Object.keys(result).length > 0, + provider, + }) + return NextResponse.json({ + matched: Object.keys(result).length > 0, + result, + cost, + error, + provider, + }) +}) diff --git a/apps/sim/blocks/blocks/enrichment.ts b/apps/sim/blocks/blocks/enrichment.ts new file mode 100644 index 00000000000..5563f71c2ca --- /dev/null +++ b/apps/sim/blocks/blocks/enrichment.ts @@ -0,0 +1,119 @@ +import { EnrichmentIcon } from '@/components/icons' +import type { BlockConfig, OutputFieldDefinition, ParamType } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import { ALL_ENRICHMENTS, getEnrichment } from '@/enrichments' +import type { EnrichmentInputField, EnrichmentOutputField } from '@/enrichments/types' +import type { EnrichmentRunResponse } from '@/tools/enrichment/types' + +/** Maps an enrichment input/output column type to a block field type. */ +function fieldType(type: EnrichmentInputField['type'] | EnrichmentOutputField['type']): ParamType { + if (type === 'number') return 'number' + if (type === 'boolean') return 'boolean' + if (type === 'json') return 'json' + return 'string' +} + +/** Stable subBlock id for an enrichment input (unique across enrichments). */ +const inputFieldId = (enrichmentId: string, inputId: string) => `${enrichmentId}__${inputId}` + +// One input field per (enrichment, input), shown only for its enrichment. +const inputSubBlocks = ALL_ENRICHMENTS.flatMap((enrichment) => + enrichment.inputs.map((input) => ({ + id: inputFieldId(enrichment.id, input.id), + title: input.name, + type: 'short-input' as const, + placeholder: input.description ?? `Enter ${input.name.toLowerCase()}`, + condition: { field: 'operation', value: enrichment.id }, + required: input.required ? ({ field: 'operation', value: enrichment.id } as const) : undefined, + })) +) + +// Block input schema: the operation plus every per-enrichment input field. +const blockInputs: Record = { + operation: { type: 'string', description: 'Enrichment to run' }, +} +for (const enrichment of ALL_ENRICHMENTS) { + for (const input of enrichment.inputs) { + blockInputs[inputFieldId(enrichment.id, input.id)] = { + type: fieldType(input.type), + description: `${input.name} (for ${enrichment.name})`, + } + } +} + +// Union of all enrichment outputs, each shown only for the enrichment(s) that +// produce it. +const outputProducers = new Map() +for (const enrichment of ALL_ENRICHMENTS) { + for (const output of enrichment.outputs) { + const entry = outputProducers.get(output.id) ?? { field: output, operations: [] } + entry.operations.push(enrichment.id) + outputProducers.set(output.id, entry) + } +} +const blockOutputs: Record = { + matched: { type: 'boolean', description: 'Whether the enrichment found a result' }, + provider: { + type: 'string', + description: 'Provider whose result was returned (e.g. "Hunter", "People Data Labs")', + }, +} +for (const [id, { field, operations }] of outputProducers) { + blockOutputs[id] = { + type: fieldType(field.type), + description: field.name, + condition: { field: 'operation', value: operations }, + } +} + +/** + * Enrichment block — runs a code-defined Sim enrichment (Work Email, Phone + * Number, Company Domain, Company Info, …) and returns its outputs. Generated + * from the enrichment registry, so new enrichments appear automatically. Runs + * on the workspace's hosted / BYOK key (injected server-side); no credential. + */ +export const EnrichmentBlock: BlockConfig = { + type: 'enrichment', + name: 'Data Enrichment', + description: 'Enrich data with a Sim enrichment', + longDescription: + 'Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.', + docsLink: 'https://docs.sim.ai/tools/enrichment', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment'], + bgColor: '#9333EA', + icon: EnrichmentIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Enrichment', + type: 'dropdown', + options: ALL_ENRICHMENTS.map((e) => ({ label: e.name, id: e.id })), + value: () => ALL_ENRICHMENTS[0]?.id ?? '', + }, + ...inputSubBlocks, + ], + + tools: { + access: ['enrichment_run'], + config: { + tool: () => 'enrichment_run', + params: (params) => { + const enrichment = getEnrichment(params.operation) + const inputs: Record = {} + if (enrichment) { + for (const input of enrichment.inputs) { + const value = params[inputFieldId(enrichment.id, input.id)] + if (value !== undefined && value !== '') inputs[input.id] = value + } + } + return { enrichmentId: params.operation, inputs } + }, + }, + }, + + inputs: blockInputs, + outputs: blockOutputs, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 79e8191546f..08418b8c6d4 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -51,6 +51,7 @@ import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch' import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs' import { EmailBisonBlock } from '@/blocks/blocks/emailbison' import { EnrichBlock } from '@/blocks/blocks/enrich' +import { EnrichmentBlock } from '@/blocks/blocks/enrichment' import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { EvernoteBlock } from '@/blocks/blocks/evernote' import { ExaBlock } from '@/blocks/blocks/exa' @@ -303,6 +304,7 @@ export const registry: Record = { elevenlabs: ElevenLabsBlock, fathom: FathomBlock, enrich: EnrichBlock, + enrichment: EnrichmentBlock, evaluator: EvaluatorBlock, evernote: EvernoteBlock, exa: ExaBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index a35727905dd..15d449f12a2 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1,6 +1,22 @@ import type { SVGProps } from 'react' import { useId } from 'react' +export function EnrichmentIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function AgentMailIcon(props: SVGProps) { return ( diff --git a/apps/sim/enrichments/run.ts b/apps/sim/enrichments/run.ts index 6fe7420ffaa..5b9a16fadb7 100644 --- a/apps/sim/enrichments/run.ts +++ b/apps/sim/enrichments/run.ts @@ -17,6 +17,8 @@ export interface EnrichmentRunOutcome { * of blanking it — a genuine "no match" still leaves this `null`. */ error: string | null + /** Label of the provider whose result was returned, or `null` on no match. */ + provider: string | null } /** True when at least one output value in the result is non-empty. */ @@ -77,7 +79,7 @@ export async function runEnrichment( const result = provider.mapOutput(response.output) if (result && hasResult(result)) { logger.info('Enrichment hit', { enrichmentId: enrichment.id, provider: provider.id }) - return { result, cost, error: null } + return { result, cost, error: null, provider: provider.label } } } catch (err) { errorCount++ @@ -93,5 +95,5 @@ export async function runEnrichment( // No provider hit. Surface an error only when every provider that ran errored // (infra/auth/rate-limit) — a clean miss returns a blank result instead. const error = ranCount > 0 && errorCount === ranCount ? lastError : null - return { result: {}, cost, error } + return { result: {}, cost, error, provider: null } } diff --git a/apps/sim/enrichments/types.ts b/apps/sim/enrichments/types.ts index 60c0180d614..7080e3b8c88 100644 --- a/apps/sim/enrichments/types.ts +++ b/apps/sim/enrichments/types.ts @@ -21,10 +21,14 @@ export interface EnrichmentOutputField { type: ColumnDefinition['type'] } -/** Per-row execution context handed to a provider's `run()` (runs server-side). */ +/** + * Execution context for an enrichment run (runs server-side). `tableId`/`rowId` + * are present for the table per-row path but optional — the workflow block path + * (`/api/tools/enrichment/run`) has no table/row and passes only `workspaceId`. + */ export interface EnrichmentRunContext { - tableId: string - rowId: string + tableId?: string + rowId?: string workspaceId: string signal?: AbortSignal } diff --git a/apps/sim/lib/api/contracts/tools/enrichment.ts b/apps/sim/lib/api/contracts/tools/enrichment.ts new file mode 100644 index 00000000000..c3279dba681 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/enrichment.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const runEnrichmentBodySchema = z.object({ + enrichmentId: z.string().min(1, 'enrichmentId is required'), + /** Per-enrichment input map: enrichment input id → mapped value. */ + inputs: z.record(z.string(), z.unknown()).default({}), + workspaceId: z.string().min(1, 'workspaceId is required'), +}) + +const runEnrichmentResponseSchema = z.object({ + matched: z.boolean(), + // untyped-response: per-enrichment output map — keys and value types vary by enrichment + result: z.record(z.string(), z.unknown()), + cost: z.number(), + error: z.string().nullable(), + /** Label of the provider whose result was returned, null on no match. */ + provider: z.string().nullable(), +}) + +export const runEnrichmentContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/enrichment/run', + body: runEnrichmentBodySchema, + response: { mode: 'json', schema: runEnrichmentResponseSchema }, +}) + +export type RunEnrichmentBody = z.input +export type RunEnrichmentResponse = z.output diff --git a/apps/sim/tools/enrichment/index.ts b/apps/sim/tools/enrichment/index.ts new file mode 100644 index 00000000000..2d7ad39f662 --- /dev/null +++ b/apps/sim/tools/enrichment/index.ts @@ -0,0 +1 @@ +export { enrichmentRunTool } from './run' diff --git a/apps/sim/tools/enrichment/run.ts b/apps/sim/tools/enrichment/run.ts new file mode 100644 index 00000000000..00eb5f4127a --- /dev/null +++ b/apps/sim/tools/enrichment/run.ts @@ -0,0 +1,105 @@ +import { ALL_ENRICHMENTS } from '@/enrichments' +import type { EnrichmentOutputField } from '@/enrichments/types' +import type { EnrichmentRunParams, EnrichmentRunResponse } from '@/tools/enrichment/types' +import type { OutputProperty, OutputType, ToolConfig } from '@/tools/types' + +/** Maps an enrichment output's column type to a tool OutputType. */ +function toOutputType(type: EnrichmentOutputField['type']): OutputType { + switch (type) { + case 'number': + return 'number' + case 'boolean': + return 'boolean' + case 'json': + return 'json' + default: + return 'string' + } +} + +/** Union of every distinct output across all registry enrichments. */ +const enrichmentOutputs: Record = {} +for (const enrichment of ALL_ENRICHMENTS) { + for (const output of enrichment.outputs) { + if (!enrichmentOutputs[output.id]) { + enrichmentOutputs[output.id] = { + type: toOutputType(output.type), + description: `${output.name} (from the selected enrichment)`, + optional: true, + } + } + } +} + +/** + * Runs a registry enrichment via `/api/tools/enrichment/run`. Selected and fed + * by the Enrichment block; the route runs the provider cascade with the + * workspace's hosted / BYOK key. + */ +export const enrichmentRunTool: ToolConfig = { + id: 'enrichment_run', + name: 'Run Enrichment', + description: 'Run a Sim enrichment (e.g. Work Email, Phone Number) and return its outputs', + version: '1.0.0', + + params: { + enrichmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Registry enrichment id (e.g. "work-email")', + }, + inputs: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: "Map of the enrichment's input ids to values", + }, + }, + + request: { + url: '/api/tools/enrichment/run', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params: EnrichmentRunParams & { _context?: { workspaceId?: string } }) => ({ + enrichmentId: params.enrichmentId, + inputs: params.inputs ?? {}, + workspaceId: params._context?.workspaceId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok || data.error) { + return { + success: false, + output: { matched: false, provider: null }, + error: data.error || `Enrichment failed (${response.status})`, + } + } + const result = (data.result ?? {}) as Record + const cost = typeof data.cost === 'number' ? data.cost : 0 + const provider = typeof data.provider === 'string' ? data.provider : null + return { + success: true, + output: { + ...result, + matched: Boolean(data.matched), + provider, + // Surface hosted-key cost so the workflow logging session bills it, + // matching the convention used by hosted-key tools. + ...(cost > 0 ? { cost: { total: cost } } : {}), + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the enrichment found a result' }, + provider: { + type: 'string', + description: 'Provider whose result was returned (e.g. "Hunter", "People Data Labs")', + optional: true, + }, + ...enrichmentOutputs, + }, +} diff --git a/apps/sim/tools/enrichment/types.ts b/apps/sim/tools/enrichment/types.ts new file mode 100644 index 00000000000..51e9d526568 --- /dev/null +++ b/apps/sim/tools/enrichment/types.ts @@ -0,0 +1,17 @@ +import type { ToolResponse } from '@/tools/types' + +export interface EnrichmentRunParams { + /** Registry enrichment id (e.g. `work-email`). */ + enrichmentId: string + /** Map of the enrichment's input ids → values. */ + inputs: Record +} + +export interface EnrichmentRunResponse extends ToolResponse { + output: { + /** Whether the enrichment found a result. */ + matched: boolean + /** Label of the provider whose result was returned, null on no match. */ + provider: string | null + } & Record +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3133260d9cc..4996b9e9453 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -633,6 +633,7 @@ import { enrichSearchSimilarCompaniesTool, enrichVerifyEmailTool, } from '@/tools/enrich' +import { enrichmentRunTool } from '@/tools/enrichment' import { evernoteCopyNoteTool, evernoteCreateNotebookTool, @@ -4338,6 +4339,7 @@ export const tools: Record = { enrich_search_posts: enrichSearchPostsTool, enrich_search_similar_companies: enrichSearchSimilarCompaniesTool, enrich_verify_email: enrichVerifyEmailTool, + enrichment_run: enrichmentRunTool, extend_parser: extendParserTool, extend_parser_v2: extendParserV2Tool, exa_search: exaSearchTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index b3c105ab531..5b321a818d0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 756, - zodRoutes: 756, + totalRoutes: 758, + zodRoutes: 758, nonZodRoutes: 0, } as const From 90574206eb2effe5e84a7a092c619cbe0e720395 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 28 May 2026 18:33:52 -0700 Subject: [PATCH 2/4] chore: re-trigger CI From 870203cd7c843aaaa55976166c5770132ae4a0dc Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 28 May 2026 18:50:48 -0700 Subject: [PATCH 3/4] refactor(enrichment): share mapFieldType helper between block and tool --- apps/sim/blocks/blocks/enrichment.ts | 15 ++++----------- apps/sim/enrichments/providers.ts | 23 ++++++++++++++++++++++- apps/sim/tools/enrichment/run.ts | 20 +++----------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/sim/blocks/blocks/enrichment.ts b/apps/sim/blocks/blocks/enrichment.ts index 5563f71c2ca..d60ab1018a2 100644 --- a/apps/sim/blocks/blocks/enrichment.ts +++ b/apps/sim/blocks/blocks/enrichment.ts @@ -2,17 +2,10 @@ import { EnrichmentIcon } from '@/components/icons' import type { BlockConfig, OutputFieldDefinition, ParamType } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' import { ALL_ENRICHMENTS, getEnrichment } from '@/enrichments' -import type { EnrichmentInputField, EnrichmentOutputField } from '@/enrichments/types' +import { mapFieldType } from '@/enrichments/providers' +import type { EnrichmentOutputField } from '@/enrichments/types' import type { EnrichmentRunResponse } from '@/tools/enrichment/types' -/** Maps an enrichment input/output column type to a block field type. */ -function fieldType(type: EnrichmentInputField['type'] | EnrichmentOutputField['type']): ParamType { - if (type === 'number') return 'number' - if (type === 'boolean') return 'boolean' - if (type === 'json') return 'json' - return 'string' -} - /** Stable subBlock id for an enrichment input (unique across enrichments). */ const inputFieldId = (enrichmentId: string, inputId: string) => `${enrichmentId}__${inputId}` @@ -35,7 +28,7 @@ const blockInputs: Record = { for (const enrichment of ALL_ENRICHMENTS) { for (const input of enrichment.inputs) { blockInputs[inputFieldId(enrichment.id, input.id)] = { - type: fieldType(input.type), + type: mapFieldType(input.type), description: `${input.name} (for ${enrichment.name})`, } } @@ -60,7 +53,7 @@ const blockOutputs: Record = { } for (const [id, { field, operations }] of outputProducers) { blockOutputs[id] = { - type: fieldType(field.type), + type: mapFieldType(field.type), description: field.name, condition: { field: 'operation', value: operations }, } diff --git a/apps/sim/enrichments/providers.ts b/apps/sim/enrichments/providers.ts index 3a2202933c5..22982bc35ad 100644 --- a/apps/sim/enrichments/providers.ts +++ b/apps/sim/enrichments/providers.ts @@ -1,4 +1,25 @@ -import type { EnrichmentProvider } from '@/enrichments/types' +import type { + EnrichmentInputField, + EnrichmentOutputField, + EnrichmentProvider, +} from '@/enrichments/types' + +/** + * Narrow union of the field types enrichments declare. Assignable to both the + * tool `OutputType` and the block `ParamType` unions, so a single mapping + * function feeds both sides without per-call casts. + */ +export type EnrichmentFieldType = 'string' | 'number' | 'boolean' | 'json' + +/** Maps an enrichment input/output column type to a block/tool field type. */ +export function mapFieldType( + type: EnrichmentInputField['type'] | EnrichmentOutputField['type'] +): EnrichmentFieldType { + if (type === 'number') return 'number' + if (type === 'boolean') return 'boolean' + if (type === 'json') return 'json' + return 'string' +} /** Coerces an unknown input value to a trimmed string (`''` when nullish). */ export function str(value: unknown): string { diff --git a/apps/sim/tools/enrichment/run.ts b/apps/sim/tools/enrichment/run.ts index 00eb5f4127a..6e00e6dce97 100644 --- a/apps/sim/tools/enrichment/run.ts +++ b/apps/sim/tools/enrichment/run.ts @@ -1,21 +1,7 @@ import { ALL_ENRICHMENTS } from '@/enrichments' -import type { EnrichmentOutputField } from '@/enrichments/types' +import { mapFieldType } from '@/enrichments/providers' import type { EnrichmentRunParams, EnrichmentRunResponse } from '@/tools/enrichment/types' -import type { OutputProperty, OutputType, ToolConfig } from '@/tools/types' - -/** Maps an enrichment output's column type to a tool OutputType. */ -function toOutputType(type: EnrichmentOutputField['type']): OutputType { - switch (type) { - case 'number': - return 'number' - case 'boolean': - return 'boolean' - case 'json': - return 'json' - default: - return 'string' - } -} +import type { OutputProperty, ToolConfig } from '@/tools/types' /** Union of every distinct output across all registry enrichments. */ const enrichmentOutputs: Record = {} @@ -23,7 +9,7 @@ for (const enrichment of ALL_ENRICHMENTS) { for (const output of enrichment.outputs) { if (!enrichmentOutputs[output.id]) { enrichmentOutputs[output.id] = { - type: toOutputType(output.type), + type: mapFieldType(output.type), description: `${output.name} (from the selected enrichment)`, optional: true, } From 54fab2711172e4df34fc57fda40cc595435d029d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 28 May 2026 18:58:50 -0700 Subject: [PATCH 4/4] fix(enrichment): reserved output keys (matched/provider) win over enrichment outputs --- apps/sim/blocks/blocks/enrichment.ts | 19 ++++++++++++------- apps/sim/tools/enrichment/run.ts | 4 +++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/sim/blocks/blocks/enrichment.ts b/apps/sim/blocks/blocks/enrichment.ts index d60ab1018a2..12c700bc6b9 100644 --- a/apps/sim/blocks/blocks/enrichment.ts +++ b/apps/sim/blocks/blocks/enrichment.ts @@ -44,13 +44,10 @@ for (const enrichment of ALL_ENRICHMENTS) { outputProducers.set(output.id, entry) } } -const blockOutputs: Record = { - matched: { type: 'boolean', description: 'Whether the enrichment found a result' }, - provider: { - type: 'string', - description: 'Provider whose result was returned (e.g. "Hunter", "People Data Labs")', - }, -} +// Seed the enrichment outputs first so the reserved `matched` / `provider` +// keys (assigned below) always win if a future enrichment ever declares an +// output id that collides with them. +const blockOutputs: Record = {} for (const [id, { field, operations }] of outputProducers) { blockOutputs[id] = { type: mapFieldType(field.type), @@ -58,6 +55,14 @@ for (const [id, { field, operations }] of outputProducers) { condition: { field: 'operation', value: operations }, } } +blockOutputs.matched = { + type: 'boolean', + description: 'Whether the enrichment found a result', +} +blockOutputs.provider = { + type: 'string', + description: 'Provider whose result was returned (e.g. "Hunter", "People Data Labs")', +} /** * Enrichment block — runs a code-defined Sim enrichment (Work Email, Phone diff --git a/apps/sim/tools/enrichment/run.ts b/apps/sim/tools/enrichment/run.ts index 6e00e6dce97..de86e254590 100644 --- a/apps/sim/tools/enrichment/run.ts +++ b/apps/sim/tools/enrichment/run.ts @@ -79,13 +79,15 @@ export const enrichmentRunTool: ToolConfig