Skip to content

Commit 6d26638

Browse files
fix(enrichment): rearm waiting cells on input changes
1 parent 3b92819 commit 6d26638

8 files changed

Lines changed: 98 additions & 11 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ export const DataRow = React.memo(function DataRow({
199199
for (const id of unmet.columns) labels.add(nameByColumnId.get(id) ?? id)
200200
}
201201

202-
if (group.type === 'enrichment') {
202+
const hasAttemptedManualRun =
203+
group.autoRun === false && row.executions?.[group.id] !== undefined
204+
if (group.type === 'enrichment' && (group.autoRun !== false || hasAttemptedManualRun)) {
203205
const enrichment = getEnrichment(group.enrichmentId)
204206
if (enrichment) {
205207
const inputColumnById = new Map<string, string>()

apps/sim/background/workflow-column-execution.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,6 @@ async function runWorkflowAndWriteTerminal(
435435
)
436436
return true
437437
}
438-
)
439-
return true
440-
}
441438

442439
// Enrichment groups call a registry function directly instead of running a
443440
// workflow, reusing the same pickup → run → terminal-write status flow. The

apps/sim/enrichments/readiness.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5+
import { companyInfoEnrichment } from '@/enrichments/company-info/company-info'
56
import { getEnrichmentReadiness } from '@/enrichments/readiness'
67
import { workEmailEnrichment } from '@/enrichments/work-email/work-email'
78

@@ -39,4 +40,13 @@ describe('getEnrichmentReadiness', () => {
3940
expect(readiness.ready).toBe(false)
4041
expect(readiness.missingInputs.map((input) => input.id)).toEqual(['fullName'])
4142
})
43+
44+
it('reports actionable inputs when present values are rejected by every provider', () => {
45+
const readiness = getEnrichmentReadiness(companyInfoEnrichment, {
46+
domain: 'https://',
47+
})
48+
49+
expect(readiness.ready).toBe(false)
50+
expect(readiness.missingInputs.map((input) => input.id)).toEqual(['domain'])
51+
})
4252
})

apps/sim/enrichments/readiness.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ export function getEnrichmentReadiness(
2525
const ready = enrichment.providers.some((provider) => provider.buildParams(inputs) !== null)
2626
if (ready) return { ready: true, missingInputs: [] }
2727

28+
const missingInputs = enrichment.inputs.filter((input) => isEmpty(inputs[input.id]))
2829
return {
2930
ready: false,
30-
missingInputs: enrichment.inputs.filter((input) => isEmpty(inputs[input.id])),
31+
/** If every input is present but every provider rejects the values, surface
32+
* all fields as actionable rather than rendering a reasonless wait state. */
33+
missingInputs: missingInputs.length > 0 ? missingInputs : enrichment.inputs,
3134
}
3235
}

apps/sim/lib/table/deps.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@ describe('optimisticallyScheduleNewlyEligibleGroups — checkbox toggle', () =>
7474
})
7575
})
7676

77+
describe('optimisticallyScheduleNewlyEligibleGroups — enrichment inputs', () => {
78+
const group = makeGroup({
79+
id: 'g1',
80+
type: 'enrichment',
81+
autoRun: true,
82+
inputMappings: [{ inputName: 'companyDomain', columnName: 'domain' }],
83+
})
84+
85+
it('flips a completed enrichment to pending when a mapped input changes', () => {
86+
const before = makeRow(
87+
{ domain: 'old.example', g1_out: 'person@old.example' },
88+
{ g1: completedExec('wf-g1') }
89+
)
90+
91+
const next = optimisticallyScheduleNewlyEligibleGroups([group], before, {
92+
domain: 'new.example',
93+
})
94+
95+
expect(next?.g1?.status).toBe('pending')
96+
})
97+
})
98+
7799
function completedExec(workflowId: string): RowExecutionMetadata {
78100
return { status: 'completed', executionId: 'e1', jobId: null, workflowId, error: null }
79101
}

apps/sim/lib/table/deps.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@ export function getUnmetGroupDeps(group: WorkflowGroup, row: TableRow): UnmetDep
118118
return { columns }
119119
}
120120

121+
/**
122+
* Columns whose edits invalidate a group's execution. Enrichments consume
123+
* their input mappings directly, so those mappings re-arm the cell even when
124+
* they are not also configured as automatic-run dependencies.
125+
*/
126+
export function getGroupInvalidationColumns(group: WorkflowGroup): string[] {
127+
const columns = new Set(group.dependencies?.columns ?? [])
128+
if (group.type === 'enrichment') {
129+
for (const mapping of group.inputMappings ?? []) columns.add(mapping.columnName)
130+
}
131+
return [...columns]
132+
}
133+
121134
/**
122135
* Optimistic mirror of the server's row-update→scheduler cascade: for every
123136
* workflow group whose deps were unmet *before* the patch and are satisfied
@@ -168,7 +181,9 @@ export function optimisticallyScheduleNewlyEligibleGroups(
168181
// flight downstream groups, so optimistically flip to `pending`
169182
// regardless of current exec status (queued/running included — they're
170183
// about to be cancelled and re-run).
171-
const depTouched = (group.dependencies?.columns ?? []).some((d) => patchedColumns.has(d))
184+
const depTouched = getGroupInvalidationColumns(group).some((columnId) =>
185+
patchedColumns.has(columnId)
186+
)
172187

173188
if (!depTouched && (exec?.status === 'queued' || exec?.status === 'running')) {
174189
skipped++

apps/sim/lib/table/rows/executions.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
*/
44
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
6-
import { writeExecutionsPatch } from '@/lib/table/rows/executions'
7-
import type { RowExecutionMetadata } from '@/lib/table/types'
6+
import { deriveExecClearsForDataPatch, writeExecutionsPatch } from '@/lib/table/rows/executions'
7+
import type { RowExecutionMetadata, TableSchema, WorkflowGroup } from '@/lib/table/types'
88

99
const EXECUTION_STATE: RowExecutionMetadata = {
1010
status: 'running',
@@ -14,6 +14,16 @@ const EXECUTION_STATE: RowExecutionMetadata = {
1414
error: null,
1515
}
1616

17+
const MANUAL_ENRICHMENT_GROUP: WorkflowGroup = {
18+
id: 'enrichment-group',
19+
workflowId: '',
20+
enrichmentId: 'work-email',
21+
type: 'enrichment',
22+
autoRun: false,
23+
inputMappings: [{ inputName: 'companyDomain', columnName: 'domain' }],
24+
outputs: [{ blockId: '', path: '', outputId: 'email', columnName: 'email' }],
25+
}
26+
1727
function renderCondition(value: unknown): string {
1828
if (!value || typeof value !== 'object') return ''
1929
const record = value as Record<string, unknown>
@@ -110,3 +120,31 @@ describe('writeExecutionsPatch guards', () => {
110120
).resolves.toBe('wrote')
111121
})
112122
})
123+
124+
describe('deriveExecClearsForDataPatch enrichment inputs', () => {
125+
it('re-arms a completed manual enrichment when a mapped input changes', () => {
126+
const schema: TableSchema = {
127+
columns: [
128+
{ id: 'domain', name: 'Domain', type: 'string' },
129+
{
130+
id: 'email',
131+
name: 'Email',
132+
type: 'string',
133+
workflowGroupId: MANUAL_ENRICHMENT_GROUP.id,
134+
},
135+
],
136+
workflowGroups: [MANUAL_ENRICHMENT_GROUP],
137+
}
138+
const completed = { ...EXECUTION_STATE, status: 'completed' as const }
139+
140+
const result = deriveExecClearsForDataPatch(
141+
{ domain: 'new.example' },
142+
schema,
143+
{ [MANUAL_ENRICHMENT_GROUP.id]: completed },
144+
undefined,
145+
{ domain: 'new.example', email: '' }
146+
)
147+
148+
expect(result.executionsPatch).toEqual({ [MANUAL_ENRICHMENT_GROUP.id]: null })
149+
})
150+
})

apps/sim/lib/table/rows/executions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { tableRowExecutions } from '@sim/db/schema'
99
import { and, eq, inArray, type SQL, sql } from 'drizzle-orm'
1010
import type { DbOrTx } from '@/lib/db/types'
1111
import { getColumnId } from '@/lib/table/column-keys'
12-
import { areGroupDepsSatisfied } from '@/lib/table/deps'
12+
import { areGroupDepsSatisfied, getGroupInvalidationColumns } from '@/lib/table/deps'
1313
import type {
1414
EnrichmentRunDetail,
1515
RowData,
@@ -152,8 +152,8 @@ export function deriveExecClearsForDataPatch(
152152
const groups = schema.workflowGroups ?? []
153153
const afterRow = { data: mergedData } as TableRow
154154
for (const group of groups) {
155-
const deps = group.dependencies?.columns ?? []
156-
const depMatched = deps.some((d) => dirtied.has(d))
155+
const invalidationColumns = getGroupInvalidationColumns(group)
156+
const depMatched = invalidationColumns.some((columnId) => dirtied.has(columnId))
157157
if (!depMatched) continue
158158

159159
// A dep column changed, but if the group's deps are no longer satisfied

0 commit comments

Comments
 (0)