Skip to content

Commit 8fa3568

Browse files
refactor(table): loop-in-cell cascade + dispatcher-everywhere routing
Two coupled changes: 1. Cell-task runs the row's full cascade in-process. executeWorkflowGroupCellJob acquires a Redis lock per (tableId, rowId) with heartbeat (10s/30s TTL), then loops through eligible workflow groups for the row. One cell-task = one row's full cascade, not N. Resume worker holds the same lock and continues the cascade after a HITL resume. Shared withCascadeLock helper in lib/table/cascade-lock.ts. 2. Every cell-enqueue goes through the dispatcher. The implicit scheduleRunsForRows reactor in service.ts is removed — 8 callsites (insertRow, batchInsertRows, upsertRow, updateRowsByFilter, batchUpdateRows, addWorkflowGroup, updateWorkflowGroup) now fire runWorkflowColumn with mode: 'incomplete', isManualRun: false. HTTP routes that call updateRow directly also fire runWorkflowColumn afterwards. scheduleRunsForTable / scheduleRunsForRowIds deleted; scheduleRunsForRows demoted to private (only the TRIGGER_DEV_ENABLED=false fallback uses it). skipScheduler flag dropped from UpdateRowData / BatchUpdateByIdData — no longer meaningful since there's nothing implicit to suppress. Plumbed isManualRun through the dispatch row (new is_manual_run column, default true) so auto-fire callers honor autoRun: false and don't re-run completed cells. Stamp 'pending' (not 'queued', executionId: null) before batchTriggerAndWait — cell-task writes its own 'queued' on lock acquire. Small UI polish: row gutter Play button spacing, "Delete workflow" → "Delete column" label, optimistic-pending cells now show Stop button (isExecInFlight no longer requires jobId).
1 parent af07b16 commit 8fa3568

17 files changed

Lines changed: 17657 additions & 362 deletions

File tree

apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
1515
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1616
import type { RowData } from '@/lib/table'
1717
import { deleteRow, updateRow } from '@/lib/table'
18+
import { runWorkflowColumn } from '@/lib/table/workflow-columns'
1819
import { accessError, checkAccess } from '@/app/api/table/utils'
1920

2021
const logger = createLogger('TableRowAPI')
@@ -136,6 +137,15 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: RowR
136137
// Only `null` when a `cancellationGuard` is supplied and the SQL guard
137138
// rejects the write — this route doesn't pass one, so reaching null is a bug.
138139
if (!updatedRow) throw new Error('updateRow returned null without a cancellationGuard')
140+
// Auto-fire any newly-eligible workflow groups (deps just became met).
141+
void runWorkflowColumn({
142+
tableId,
143+
workspaceId: validated.workspaceId,
144+
rowIds: [updatedRow.id],
145+
mode: 'incomplete',
146+
isManualRun: false,
147+
requestId,
148+
}).catch((err) => logger.error(`[${requestId}] auto-dispatch (row PATCH) failed:`, err))
139149

140150
return NextResponse.json({
141151
success: true,

apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
1414
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1515
import type { RowData } from '@/lib/table'
1616
import { updateRow } from '@/lib/table'
17+
import { runWorkflowColumn } from '@/lib/table/workflow-columns'
1718
import { accessError, checkAccess } from '@/app/api/table/utils'
1819
import {
1920
checkRateLimit,
@@ -144,6 +145,14 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: RowR
144145
if (!updatedRow) {
145146
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
146147
}
148+
void runWorkflowColumn({
149+
tableId,
150+
workspaceId: validated.workspaceId,
151+
rowIds: [updatedRow.id],
152+
mode: 'incomplete',
153+
isManualRun: false,
154+
requestId,
155+
}).catch((err) => logger.error(`[${requestId}] auto-dispatch (v1 row update) failed:`, err))
147156

148157
return NextResponse.json({
149158
success: true,

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ export const DataRow = React.memo(function DataRow({
178178
<td className={cn(CELL_CHECKBOX, 'cursor-pointer')}>
179179
<div
180180
className={cn(
181-
'flex items-center gap-1',
182-
hasWorkflowColumns ? 'justify-between' : 'justify-center'
181+
'flex items-center',
182+
hasWorkflowColumns ? 'justify-end gap-1.5 pr-1' : 'justify-center'
183183
)}
184184
>
185185
<div
@@ -221,10 +221,7 @@ export const DataRow = React.memo(function DataRow({
221221
size='sm'
222222
aria-label={runningCount > 0 ? `Stop ${runningCount} running` : 'Run row'}
223223
title={runningCount > 0 ? `Stop ${runningCount} running` : 'Run row'}
224-
// mr-px keeps the hover bg off the cell's right border — without
225-
// it the rounded-rect background paints over the divider line
226-
// while the button is hovered.
227-
className='mr-px size-[20px] shrink-0 p-0 text-[var(--text-primary)] hover-hover:bg-[var(--surface-2)]'
224+
className='size-[20px] shrink-0 p-0 text-[var(--text-primary)] hover-hover:bg-[var(--surface-2)]'
228225
onClick={() => {
229226
if (runningCount > 0) {
230227
onStopRow(row.id)

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,12 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
8989
? workflows?.find((w) => w.id === ownGroup.workflowId)
9090
: undefined
9191
// Workflow-output column with siblings → "Hide column" (non-destructive,
92-
// re-addable from sidebar). Last output of a group → "Delete workflow"
92+
// re-addable from sidebar). Last output of a group → "Delete column"
9393
// (removes the entire group). Plain column → undefined (default "Delete column").
9494
const deleteLabel = ownGroup
9595
? ownGroup.outputs.length > 1
9696
? 'Hide column'
97-
: 'Delete workflow'
97+
: 'Delete column'
9898
: undefined
9999
useEffect(() => {
100100
if (isRenaming && renameInputRef.current) {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ interface ColumnOptionsMenuProps {
3535
position: { x: number; y: number }
3636
column: DisplayColumn
3737
/** Override for the destructive item's label. Defaults to "Delete column"
38-
* (or "Delete workflow" when `onDeleteGroup` is set). Use "Hide column"
39-
* when the destructive action is non-lossy (workflow-output column where
40-
* removing it leaves the group with siblings). */
38+
* for both plain columns and workflow groups. Use "Hide column" when the
39+
* destructive action is non-lossy (workflow-output column where removing
40+
* it leaves the group with siblings). */
4141
deleteLabel?: string
4242
onOpenConfig: (columnName: string) => void
4343
onInsertLeft: (columnName: string) => void
@@ -156,7 +156,7 @@ export function ColumnOptionsMenu({
156156
onSelect={() => (onDeleteGroup ? onDeleteGroup() : onDeleteColumn(column.name))}
157157
>
158158
{deleteLabel === 'Hide column' ? <EyeOff /> : <Trash />}
159-
{deleteLabel ?? (onDeleteGroup ? 'Delete workflow' : 'Delete column')}
159+
{deleteLabel ?? 'Delete column'}
160160
</DropdownMenuItem>
161161
</DropdownMenuContent>
162162
</DropdownMenu>

0 commit comments

Comments
 (0)