Skip to content

feat(tables): Add enrichment table column type#4752

Merged
TheodoreSpeaks merged 19 commits into
stagingfrom
feat/enrichment-ui
May 27, 2026
Merged

feat(tables): Add enrichment table column type#4752
TheodoreSpeaks merged 19 commits into
stagingfrom
feat/enrichment-ui

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

Summary

  • Code-defined enrichments: a TS registry (apps/sim/enrichments/) that runs directly per table row on the existing workflow-group rails — no workflow needed
  • Provider fallback cascade (first-result-wins) calling real data tools via executeTool with hosted-key/BYOK injection. v1: Work Email (Hunter→PDL), Phone (PDL), Company Domain (PDL), Company Info (PDL→Hunter)
  • Bill hosted-key cost to the table owner via recordUsage (new enrichment usage source + migration); all-providers-errored surfaces an errored cell; abort-safe
  • Table UI: enrichments catalog/sidebar, per-row input mapping, columns show the enrichment name + icon, configure opens the enrichments sidebar (edit mode), plain column editor for output columns, per-output column naming
  • Copilot: list_enrichments + add_enrichment tools so Mothership can add enrichment columns

Type of Change

  • New feature

Testing

Tested manually; tsc clean, biome check clean, bun run check:api-validation:strict passes, copilot tool tests pass (13/13)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 10 commits May 26, 2026 15:09
Add a Clay-style enrichments catalog to the table view and wire per-row
input mapping into workflow-backed columns.

- New "Enrichments" entry in the New-column dropdown opens a sliding panel
  listing curated enrichment templates; picking one swaps to the workflow
  config in-place (no cross-slide) with a back button.
- Type the workflow sidebar as manual | enrichment; enrichment hides the
  launch + add-column-inputs affordances.
- Add a "Workflow inputs" advanced panel mapping Start-block input fields to
  table columns (left-of-workflow columns only), with name-match auto-fill
  and collapsible input-mapping-style rows.
- Persist type + inputMappings on the workflow group (types, contract, route,
  service, hook) — jsonb, no migration.
- Consume inputMappings at run time: when present, feed Start-block fields
  from the mapped columns; otherwise fall back to name-match spread.
- Clean up inputMappings on column rename/delete (stripGroupDeps + renameColumn).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the collapsible field-card markup (surface-4 header + surface-2 body,
click/keyboard toggle, truncated title + optional badge) into a shared
`CollapsibleCard` emcn component, and use it in the workflow-builder input
mapping rows and the table sidebar's input-mapping panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enrichments are now TS configs in apps/sim/enrichments/ (registry, like
connectors) that run directly per table row via the existing run/dispatch/
cell-write rails — no workflow execution.

- enrichments/{types,registry} + work-email (heuristic) and phone-number (stub).
- WorkflowGroup gains enrichmentId; WorkflowGroupOutput gains outputId
  (workflowId/blockId/path kept required, '' for enrichment groups).
- Executor branches on group.type === 'enrichment' → maps inputMappings →
  enrich() → outputs by outputId → cell-write. Missing required inputs skip
  (blank cell) instead of erroring.
- Sidebar lists the registry; enrichment-config panel maps inputs to columns
  and creates the enrichment group (no workflow UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace each enrichment's single enrich() with an ordered providers[]
fallback cascade. Providers are plain data ({ id, label, toolId,
buildParams, mapOutput }) so the catalog stays client-safe; the
server-only runner (run.ts) calls executeTool per provider, first
non-empty result wins, misses/errors fall through, all-miss = blank cell.

Wire four enrichments on the hosted-safe providers (Hunter, PDL):
- Work Email (fullName, companyDomain): Hunter -> PDL
- Phone Number (fullName, companyDomain): PDL
- Company Domain (companyName): PDL
- Company Info (domain): PDL -> Hunter

Person enrichments take a single canonical fullName (Clay-style); Hunter
gets first/last via splitName(), PDL takes name directly.

Add 'enrichment' to usage_log_source enum (+ migration) so hosted-key
tool cost from these per-row calls can be billed to the table owner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt safety

- runEnrichment now returns { result, cost, error }: accumulates hosted-key
  cost across the cascade, and sets `error` only when every provider that ran
  errored (auth/rate-limit/outage) vs a clean miss.
- Executor records the cost to the table owner (createdBy) via recordUsage
  (source 'enrichment'); billing failures are logged, never error the cell.
- F1: all-providers-errored now writes status 'error' instead of a blank
  'completed' cell that looked like "no data found".
- F2: re-check the abort signal after the cascade so a cancel mid-tool-call
  isn't recorded as a completed empty cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Meta-header shows the enrichment's name + icon (Mail/Phone/Globe/Building2)
  instead of "Workflow" + a color chip.
- Per-column header icon uses the enrichment's icon (via columnSourceInfo)
  instead of the generic play icon.
- Hide "View execution" for enrichment cells in both the row context menu and
  the action bar (no workflow execution exists to open); also hide the
  meta-menu "View workflow" item for enrichment groups.
- Clicking an enrichment column header now opens the enrichments sidebar in
  edit mode (pre-filled input mappings, Update via useUpdateWorkflowGroup)
  instead of the workflow "Configure workflow" sidebar.
- Enrichment config lets the user name each output column (editable per-output,
  deduped defaults) since enrichments can produce multiple columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the per-column enrichment icon (it duplicated the meta-header icon).
  Enrichment output columns now render the standard column-type icon (Text,
  etc.) — the enrichment's icon stays only on the group meta-header.
- Make output column names editable in the enrichment config edit mode too;
  changed names rename their columns via useUpdateColumn (the rename cascades
  into the group's output refs server-side). Validation excludes the output's
  own current name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Edit column on an enrichment output now opens the normal column-config sidebar
(rename / type / unique) instead of the workflow 'Configure output column'
panel, which showed workflow-only fields and blocked a simple rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Let the copilot enumerate the code-defined enrichment registry and add an
enrichment column to a table (validating required input mappings against the
table's columns), backed by the same workflow-group machinery the UI uses.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 27, 2026 7:12am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

High Risk
Touches per-row execution, third-party API calls, and hosted-key billing; incorrect cascade or mapping logic could charge users or write wrong cell states at scale.

Overview
Adds code-defined table enrichments that run per row without a workflow, reusing workflow-group storage and the column execution pipeline.

A new apps/sim/enrichments/ registry defines four v1 enrichments (work email, phone, company domain, company info) with ordered provider cascades via executeTool (hosted keys / BYOK). The background executor branches for type: 'enrichment', maps inputs from columns, bills enrichment usage, and distinguishes misses ("Not found") from all-provider failures (error cells).

Table UX: + New column → Enrichments opens a catalog sidebar and config (input mapping, output column names, auto-run deps). Groups persist enrichmentId, inputMappings, and optional type; API PATCH accepts inputMappings / type. Enrichment groups show registry icons in the meta header; workflow sidebar gains optional Start-block input mappings for manual groups.

Copilot catalog gains list_enrichments / add_enrichment; internal docs add /add-enrichment. Shared CollapsibleCard refactors workflow input-mapping UI.

Reviewed by Cursor Bugbot for commit f859a0c. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/background/workflow-column-execution.ts
Comment thread apps/sim/background/workflow-column-execution.ts
Comment thread apps/sim/background/workflow-column-execution.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR introduces code-defined enrichment columns for tables: a TypeScript registry (apps/sim/enrichments/) of data-enrichment providers (Work Email via Hunter→PDL, Phone via PDL, Company Domain via PDL, Company Info via PDL→Hunter) that run per table row on the existing workflow-group execution rail without needing a workflow. A new enrichments sidebar, copilot tools (list_enrichments, add_enrichment), and a usage_log_source DB migration round out the feature.

  • Enrichment execution (enrichments/run.ts, background/workflow-column-execution.ts): provider cascade (first-result-wins), 404-as-clean-miss handling, hosted-key billing via recordUsage, and abort-safe status writes all reuse the existing workflow-group cell execution infrastructure.
  • UI (enrichments-sidebar/, table-grid/, workflow-sidebar/): catalog list, per-input column mapping, per-output column naming, and edit mode opening from the group header; enrichment columns route to plain column editor, not the workflow config panel.
  • isHosted = true hardcoded in feature-flags.ts — a debugging leftover that must be reverted before merge (see inline comment).

Confidence Score: 2/5

Not safe to merge until the hardcoded isHosted = true in feature-flags.ts is reverted — every deployment will inject Sim's hosted API keys for PDL and Hunter enrichment calls.

The isHosted = true debug line in feature-flags.ts causes injectHostedKeyIfNeeded in tools/index.ts to skip the !isHosted early-return and inject Sim's own PDL/Hunter credentials on every enrichment call across all deployments, including self-hosted instances. This would leak Sim's hosted-key quota to arbitrary deployments and could trigger rate limits or unexpected billing against Sim's own accounts.

apps/sim/lib/core/config/feature-flags.ts — the hardcoded isHosted = true must be reverted before this ships.

Security Review

  • Hosted-key injection on all deployments (apps/sim/lib/core/config/feature-flags.ts): isHosted is hardcoded to true, causing injectHostedKeyIfNeeded in tools/index.ts to inject Sim's own PDL and Hunter API credentials for every enrichment call on every deployment — including self-hosted instances. Self-hosted users would silently consume Sim's hosted-key quota with no billing back to them.

Important Files Changed

Filename Overview
apps/sim/lib/core/config/feature-flags.ts isHosted hardcoded to true — a debugging artifact that causes every deployment to inject Sim's hosted API keys for PDL/Hunter enrichment calls.
apps/sim/enrichments/run.ts New enrichment runner with provider cascade logic; 404 is treated as a clean no-match (correct), cost is accumulated correctly for successful calls.
apps/sim/background/workflow-column-execution.ts Adds enrichment execution path alongside the existing workflow path; correctly skips own output columns from input mapping, handles abort signals, and wraps billing failures so they don't error the cell.
apps/sim/lib/table/service.ts Cascades column renames into inputMappings and uses outputId-keyed diff for enrichment outputs; correct output key fallback logic.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx New enrichment config panel with input mapping, output column naming, and auto-run settings; handles both create and edit modes.
apps/sim/lib/copilot/tools/server/table/user-table.ts Adds list_enrichments and add_enrichment copilot operations; validates required inputs, column existence, and output column naming before calling addWorkflowGroup.
packages/db/migrations/0213_wealthy_sue_storm.sql Adds enrichment to the usage_log_source enum; migration is correct and schema.ts is kept in sync.
apps/sim/tools/index.ts Surfaces HTTP status on the error output object so run.ts can distinguish 404 no-match from auth/rate-limit failures; targeted and correct change.

Sequence Diagram

sequenceDiagram
    participant Scheduler
    participant CellExecutor as workflow-column-execution
    participant RunEnrichment as enrichments/run.ts
    participant ExecuteTool as tools/index.ts
    participant Provider as PDL / Hunter API
    participant UsageLog as billing/usage-log

    Scheduler->>CellExecutor: runWorkflowAndWriteTerminal(group)
    CellExecutor->>CellExecutor: "group.type === enrichment?"
    CellExecutor->>CellExecutor: map inputMappings to enrichInputs
    CellExecutor->>CellExecutor: check missingRequired, skip row if true
    CellExecutor->>RunEnrichment: runEnrichment(enrichment, inputs, ctx)
    loop Provider cascade (first-result-wins)
        RunEnrichment->>ExecuteTool: executeTool(toolId, params + workspaceId)
        ExecuteTool->>Provider: HTTP request (BYOK or hosted key)
        Provider-->>ExecuteTool: response
        ExecuteTool-->>RunEnrichment: success, output, error
        alt 404 clean miss
            RunEnrichment->>RunEnrichment: continue to next provider
        else success + non-empty result
            RunEnrichment-->>CellExecutor: result, cost, error null
        else error
            RunEnrichment->>RunEnrichment: errorCount++ try next
        end
    end
    RunEnrichment-->>CellExecutor: result empty, cost, error
    alt "cost > 0"
        CellExecutor->>UsageLog: recordUsage(enrichment cost)
    end
    CellExecutor->>CellExecutor: writeState completed or error with dataPatch
Loading

Reviews (3): Last reviewed commit: "fix lint" | Re-trigger Greptile

- Guard the enrichment cell path on `enrichmentId` so a group typed
  'enrichment' without a registry id falls through to the workflow path
  instead of erroring.
- Clear stale output values when skipping a row for missing required inputs,
  so the auto cascade re-enriches once inputs return (was left completed+filled).
- Write a terminal state on abort in the enrichment path (matches the workflow
  path) so a cancel between run and terminal-write can't leave the cell running.
- Edit mode: apply the group update (mappings/deps/auto-run) before column
  renames so the primary edit lands even if a rename fails.
- Disable Save once validation has surfaced a missing required input.
- Use the workflowGroupById map instead of O(n) find in the context-menu and
  action-bar hot paths.
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 0ce8bea05:

Cursor Bugbot

  • Wrong branch for enrichment type (High): the enrichment cell path is now guarded on group.enrichmentId too, so a group typed 'enrichment' without a registry id falls through to the workflow path instead of erroring. (In practice type: 'enrichment' is only ever set alongside an enrichmentId, but the guard makes it robust.)
  • Skip leaves stale output values (Med): the missing-required-input skip now clears any prior output values, so the cell isn't left completed-and-filled and the auto cascade re-enriches once inputs return.
  • Abort leaves running status (Med): the enrichment path now writes a terminal state on abort (matching the workflow path), so a cancel between the cascade returning and the terminal write can't leave the cell stuck running.

Greptile

  • Save button gating: saveDisabled now includes showValidation && missingRequired, consistent with outputsInvalid.
  • Partial rename on multi-output edit: the group update (mappings / deps / auto-run) now runs before the per-column renames, so the primary edit always lands even if a later rename fails. (No atomic multi-rename API exists; client-side validation still prevents the common collision/empty failures.)
  • O(n) find in hot paths: both the context-menu and action-bar lookups now use the pre-built workflowGroupById map for O(1) access.

tsc + biome clean.

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks TheodoreSpeaks changed the title feat(tables): native code-defined enrichments — registry, provider cascade, billing, UI, copilot feat(tables): Add enrichment table column type May 27, 2026
Comment thread apps/sim/lib/table/service.ts
Guides adding a code-defined table enrichment to the registry, with a required
step to verify each provider tool has hosted-key support and chain to
/add-hosted-key when it doesn't.
Comment thread apps/sim/background/workflow-column-execution.ts
- updateWorkflowGroup output diff now keys on outputId (falling back to
  blockId::path) so enrichment outputs — which share empty blockId/path —
  no longer collapse to one key and drop sibling columns.
- Enrichment terminal write now clears output columns absent from the result,
  so a partial/empty re-run doesn't leave stale values.
- Editing a group whose enrichment was removed from the registry shows an
  explanatory panel instead of silently falling through to the new-enrichment
  catalog.
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Addressed the second-pass review in `61343a11a`:

  • Output diff collapses enrichments (High): updateWorkflowGroup's output diff now keys on outputId (falling back to blockId::path for workflow outputs). Enrichment outputs all share empty blockId/path, so the old key collapsed every sibling into one entry — now each is distinct, so passing outputs for an enrichment group can't drop columns. (The edit flow doesn't send outputs, so this was latent, but it's fixed regardless.)
  • Miss leaves stale enrichment values (Med): the terminal write now writes every output column — the result value when present, else clears it — so a partial/empty re-run blanks the columns it didn't fill instead of leaving prior values.
  • Edit enrichment shows catalog (Med): editing a group whose enrichmentId is no longer in the registry now shows an explanatory panel instead of silently dropping into the new-enrichment catalog.

tsc + biome clean.

Comment thread apps/sim/lib/table/workflow-columns.ts
…ells

An enrichment that runs to completion but matches nothing now renders a gray
"Not found" badge (like the Queued/Waiting cell states) instead of a blank
cell, so a real miss is distinguishable from an unrun cell. Scoped to
enrichment output columns; an empty string no longer counts as a value.
if (!isNull) return { kind: 'value', text: stringifyValue(value) }
// while other blocks in the group are still running. An empty string is not
// a value — it falls through so a completed enrichment can show "Not found".
if (!isEmpty) return { kind: 'value', text: stringifyValue(value) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty-string workflow outputs now show incorrect running state

Medium Severity

Changing the guard from !isNull to !isEmpty affects all workflow-group columns, not just enrichment ones. When a regular workflow block finishes with an empty string output while other blocks in the group are still running, the cell now falls through to the inFlight branch and incorrectly renders as "queued" or "pending-upstream" instead of showing the completed (empty) value. The isEnrichmentOutput guard for "Not found" only helps enrichment columns — regular workflow columns with legitimate '' outputs regress to showing an active-running indicator after the block has finished.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4e7b751. Configure here.

… cascade

A completed enrichment with empty outputs is a real no-match result, not an
unfinished run. Eligibility now treats an enrichment's completed status as
terminal (regardless of output fill), so the auto cascade stops re-invoking
billable provider calls on every no-match row each dispatch. Input changes
still clear the exec entry, so genuine re-runs are unaffected; manual Run all
still re-runs.
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Latest review pass:

  • Blank miss re-eligible on run-all (Med) — fixed in `a3c92eaf1`. Eligibility now treats an enrichment's completed status as terminal even with empty outputs (a no-match is a real result), so the auto cascade no longer re-invokes billable provider calls on every no-match row each dispatch. Genuine input changes still clear the exec entry (deriveExecClearsForDataPatch), so real re-runs are unaffected; explicit "Run all" still re-runs.
  • Miss leaves stale enrichment values (Med) — already addressed in `61343a11a` (the bot re-flagged the pre-fix commit fb6bbd0cd). The terminal write now writes every output column, clearing any the result didn't fill.

Also shipped `4e7b75196`: completed-but-empty enrichment cells render a "Not found" badge instead of a blank.

tsc + biome clean.

Providers like People Data Labs signal 'no record found' with HTTP 404, which
executeTool surfaces as a failed ToolResponse (status on output.status). The
cascade now treats a 404 as a clean miss — falls through to the next provider
and lets the cell render 'Not found' — instead of marking the cell errored.
Auth/rate-limit/5xx still propagate as real errors.
executeTool's catch handled Error instances in its first branch and only
extracted status/statusText/data for non-Error object throws — so HTTP errors
(thrown as Error instances carrying .status) lost their status on the returned
output. Surface it for Error instances too, so callers can branch on the
status (e.g. the enrichment cascade treating a provider 404 as a no-match).
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/lib/core/config/feature-flags.ts Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f859a0c. Configure here.

Comment thread apps/sim/lib/api/contracts/tables.ts
@TheodoreSpeaks TheodoreSpeaks merged commit 65e2fe8 into staging May 27, 2026
14 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/enrichment-ui branch May 27, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant