From b96bd0ec3e9478cb8f4aa7a9ed7847aebef00334 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 10 Jul 2026 14:09:15 -0700 Subject: [PATCH 1/2] fix(enrichments): remove Icypeas providers and show Running on in-flight cells --- .../table-grid/cells/cell-render.tsx | 4 +++ .../email-verification.test.ts | 26 ------------------- .../email-verification/email-verification.ts | 22 +--------------- .../enrichments/work-email/work-email.test.ts | 20 -------------- apps/sim/enrichments/work-email/work-email.ts | 22 +--------------- 5 files changed, 6 insertions(+), 88 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 4b16f2bceac..0c24b531040 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -73,6 +73,10 @@ export function resolveCellRender({ const inFlight = exec?.status === 'running' || exec?.status === 'queued' || exec?.status === 'pending' if (inFlight && blockRunning) return { kind: 'running' } + // Enrichment outputs share an empty blockId, so `runningBlockIds` can never + // match them — the group-level `running` status is the only "worker picked + // this up" signal an enrichment cell has. + if (isEnrichmentOutput && exec?.status === 'running') return { kind: 'running' } // Value wins over pending-upstream: a finished column stays finished even // while other blocks in the group are still running. An empty string is not diff --git a/apps/sim/enrichments/email-verification/email-verification.test.ts b/apps/sim/enrichments/email-verification/email-verification.test.ts index 694c8dd0349..ae547dc4e9d 100644 --- a/apps/sim/enrichments/email-verification/email-verification.test.ts +++ b/apps/sim/enrichments/email-verification/email-verification.test.ts @@ -19,7 +19,6 @@ describe('email-verification enrichment cascade', () => { 'zerobounce', 'neverbounce', 'millionverifier', - 'icypeas', 'enrow', ]) }) @@ -39,31 +38,6 @@ describe('email-verification enrichment cascade', () => { }) }) - describe('icypeas', () => { - const p = provider('icypeas') - it('maps FOUND/DEBITED to deliverable and NOT_FOUND to undeliverable', () => { - expect(p.toolId).toBe('icypeas_verify_email') - expect(p.buildParams(emailInput)).toEqual({ email: 'john@acme.com' }) - expect(p.buildParams({ email: '' })).toBeNull() - expect(p.mapOutput({ status: 'FOUND' })).toEqual({ status: 'valid', deliverable: true }) - expect(p.mapOutput({ status: 'DEBITED' })).toEqual({ status: 'valid', deliverable: true }) - expect(p.mapOutput({ status: 'NOT_FOUND' })).toEqual({ - status: 'invalid', - deliverable: false, - }) - expect(p.mapOutput({ status: 'DEBITED_NOT_FOUND' })).toEqual({ - status: 'invalid', - deliverable: false, - }) - }) - it('falls through on inconclusive statuses', () => { - expect(p.mapOutput({ status: 'BAD_INPUT' })).toBeNull() - expect(p.mapOutput({ status: 'INSUFFICIENT_FUNDS' })).toBeNull() - expect(p.mapOutput({ status: 'ABORTED' })).toBeNull() - expect(p.mapOutput({})).toBeNull() - }) - }) - describe('enrow', () => { const p = provider('enrow') it('maps the valid/invalid qualifier and falls through otherwise', () => { diff --git a/apps/sim/enrichments/email-verification/email-verification.ts b/apps/sim/enrichments/email-verification/email-verification.ts index bd78306813e..e9c8048689c 100644 --- a/apps/sim/enrichments/email-verification/email-verification.ts +++ b/apps/sim/enrichments/email-verification/email-verification.ts @@ -5,7 +5,7 @@ import type { EnrichmentConfig } from '@/enrichments/types' /** * Email Verification enrichment. Checks an email address's deliverability via a * verifier waterfall — ZeroBounce first (highest coverage), then NeverBounce, - * then MillionVerifier, then Icypeas, then Enrow. A provider that returns a + * then MillionVerifier, then Enrow. A provider that returns a * definitive verdict (valid / invalid / catch_all / disposable / etc.) fills the * cell; a provider that can only return `unknown` falls through to the next so * the row gets the most confident answer available. All providers support hosted @@ -68,26 +68,6 @@ export const emailVerificationEnrichment: EnrichmentConfig = { return { status, deliverable: output.deliverable === true } }, }), - toolProvider({ - id: 'icypeas', - label: 'Icypeas', - toolId: 'icypeas_verify_email', - buildParams: (inputs) => { - const email = str(inputs.email) - if (!email) return null - return { email } - }, - mapOutput: (output) => { - // FOUND/DEBITED → deliverable, NOT_FOUND/DEBITED_NOT_FOUND → undeliverable. - // Bad input / insufficient funds / aborted are inconclusive → fall through. - const status = str(output.status) - if (status === 'FOUND' || status === 'DEBITED') - return { status: 'valid', deliverable: true } - if (status === 'NOT_FOUND' || status === 'DEBITED_NOT_FOUND') - return { status: 'invalid', deliverable: false } - return null - }, - }), toolProvider({ id: 'enrow', label: 'Enrow', diff --git a/apps/sim/enrichments/work-email/work-email.test.ts b/apps/sim/enrichments/work-email/work-email.test.ts index 9f3f6801f17..3a398a6bca6 100644 --- a/apps/sim/enrichments/work-email/work-email.test.ts +++ b/apps/sim/enrichments/work-email/work-email.test.ts @@ -26,7 +26,6 @@ describe('work-email enrichment cascade', () => { 'datagma', 'leadmagic', 'dropcontact', - 'icypeas', 'enrow', ]) }) @@ -130,25 +129,6 @@ describe('work-email enrichment cascade', () => { }) }) - describe('icypeas', () => { - const p = provider('icypeas') - it('splits the name when possible and keeps mononym rows', () => { - expect(p.toolId).toBe('icypeas_find_email') - expect(p.buildParams(nameDomain)).toEqual({ - firstname: 'John', - lastname: 'Doe', - domainOrCompany: 'acme.com', - }) - // single-token name runs with firstname alone (lastname is optional) - expect(p.buildParams({ fullName: 'Cher', companyDomain: 'acme.com' })).toEqual({ - firstname: 'Cher', - domainOrCompany: 'acme.com', - }) - expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() - expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) - }) - }) - describe('enrow', () => { const p = provider('enrow') it('maps full name + company domain', () => { diff --git a/apps/sim/enrichments/work-email/work-email.ts b/apps/sim/enrichments/work-email/work-email.ts index f965a8544f5..5fe87917e36 100644 --- a/apps/sim/enrichments/work-email/work-email.ts +++ b/apps/sim/enrichments/work-email/work-email.ts @@ -8,7 +8,7 @@ import type { EnrichmentConfig } from '@/enrichments/types' * available identifiers (company domain, LinkedIn URL) via a provider waterfall: * deterministic finders first (Hunter, Findymail by name then by LinkedIn), then * enrichment/reveal providers (Prospeo, Wiza), then People Data Labs as a broad - * record-match fallback, then Datagma, LeadMagic, Dropcontact, Icypeas, and Enrow + * record-match fallback, then Datagma, LeadMagic, Dropcontact, and Enrow * as additional finders. Each provider opportunistically uses whatever * identifiers the row provides and self-skips when it has none usable, so adding * more inputs widens coverage. First email wins; all providers support hosted keys. @@ -189,26 +189,6 @@ export const workEmailEnrichment: EnrichmentConfig = { return email ? { email } : null }, }), - toolProvider({ - id: 'icypeas', - label: 'Icypeas', - toolId: 'icypeas_find_email', - buildParams: (inputs) => { - // Icypeas only requires domainOrCompany; firstname/lastname are optional, - // so a mononym still runs with firstname alone rather than self-skipping. - const fullName = str(inputs.fullName) - const domainOrCompany = normalizeDomain(inputs.companyDomain) - if (!fullName || !domainOrCompany) return null - const name = splitName(inputs.fullName) - return name - ? { firstname: name.firstName, lastname: name.lastName, domainOrCompany } - : { firstname: fullName, domainOrCompany } - }, - mapOutput: (output) => { - const email = str(output.email) - return email ? { email } : null - }, - }), toolProvider({ id: 'enrow', label: 'Enrow', From c2860a91bc4837ae7298f4951994cd3fb29e03b7 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 10 Jul 2026 15:06:02 -0700 Subject: [PATCH 2/2] fix(enrichments): keep previous value visible while an enrichment cell reruns --- .../components/table-grid/cells/cell-render.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 0c24b531040..81613b3d5db 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -73,10 +73,6 @@ export function resolveCellRender({ const inFlight = exec?.status === 'running' || exec?.status === 'queued' || exec?.status === 'pending' if (inFlight && blockRunning) return { kind: 'running' } - // Enrichment outputs share an empty blockId, so `runningBlockIds` can never - // match them — the group-level `running` status is the only "worker picked - // this up" signal an enrichment cell has. - if (isEnrichmentOutput && exec?.status === 'running') return { kind: 'running' } // Value wins over pending-upstream: a finished column stays finished even // while other blocks in the group are still running. An empty string is not @@ -89,6 +85,12 @@ export function resolveCellRender({ return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'value', text } } + // Enrichment outputs share an empty blockId, so `runningBlockIds` can never + // match them — the group-level `running` status is the only "worker picked + // this up" signal an enrichment cell has. Checked after the value so a + // rerun keeps showing the previous output until the new result lands. + if (isEnrichmentOutput && exec?.status === 'running') return { kind: 'running' } + if (inFlight && !(groupHasBlockErrors && !blockRunning)) { // A `pending` cell whose jobId starts with `paused-` is mid-pause // (workflow yielded for human-in-the-loop). Render as Pending rather