Skip to content

improvement(executor): support nested loops/parallels#3398

Merged
waleedlatif1 merged 71 commits intostagingfrom
feat/loop_nesting
Mar 4, 2026
Merged

improvement(executor): support nested loops/parallels#3398
waleedlatif1 merged 71 commits intostagingfrom
feat/loop_nesting

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 2, 2026

Summary

  • Fix z-index bug preventing clicks on inner subflows when outer is selected
  • Fix nested parallel execution ("Parallel start not connected") by adding sentinel-based boundary resolution
  • Extract shared iteration context utilities, remove duplicated code across block-executor and workflow-handler
  • Add cycle guards to buildParentIterations, getNodeHierarchy, and isLoopNestedInside
  • Replace paste subflow blanket-block with cycle-only prevention
  • Use recursive helpers in terminal instead of hardcoded 2-level depth checks
  • Remove aliased imports, dead code, unused parameters, and tighten any types to unknown
  • Add edge construction tests for nested parallel and parallel-in-loop wiring

Fixes #3301

Type of Change

  • Bug fix
  • Improvement

Testing

  • All 601 executor tests pass
  • Clean type-check
  • Tested manually

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)

waleedlatif1 and others added 15 commits February 16, 2026 00:36
…stash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)

* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
… fixes, removed retired models, hex integration
…ogle tasks and bigquery integrations, workflow lock
Wire inner loop sentinel nodes into outer loop sentinel chains so that
nested loops execute correctly. Resolves boundary-node detection to use
effective sentinel IDs for nested loops, handles loop-exit edges from
inner sentinel-end to outer sentinel-end, and recursively clears
execution state for all nested loop scopes between iterations.

NOTE: loop-in-loop nesting only; parallel nesting is not yet supported.
Made-with: Cursor
…able resolution

Introduce ParentIteration to track ancestor loop state, build a
loopParentMap during DAG construction, and propagate parent iterations
through block execution and child workflow contexts.

Extend LoopResolver to support named loop references (e.g. <loop1.index>)
and add output property resolution (<loop1.result>). Named references
use the block's display name normalized to a tag-safe identifier,
enabling blocks inside nested loops to reference any ancestor loop's
iteration state.

NOTE: loop-in-loop nesting only; parallel nesting is not yet supported.
Made-with: Cursor
… and terminal display

Thread parentIterations through SSE block-started, block-completed, and
block-error events so the terminal can reconstruct nested loop
hierarchies. Update the entry tree builder to recursively nest inner
loop subflow nodes inside their parent iteration rows, using
parentIterations depth-stripping to support arbitrary nesting depth.

Display the block's store name for subflow container rows instead of
the generic "Loop" / "Parallel" label.

Made-with: Cursor
Remove the restriction that prevented subflow nodes from being dragged
into other subflow containers, enabling loop-in-loop nesting on the
canvas. Add cycle detection (isDescendantOf) to prevent a container
from being placed inside one of its own descendants.

Resize all ancestor containers when a nested child moves, collect
descendant blocks when removing from a subflow so boundary edges are
attributed correctly, and surface all ancestor loop tags in the tag
dropdown for blocks inside nested loops.

Made-with: Cursor
@vercel
Copy link

vercel bot commented Mar 2, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 4, 2026 3:01am

Request Review

@waleedlatif1 waleedlatif1 changed the title improvement(executor): audit and harden nested loop/parallel implementation improvement(executor): support nested loops/parallels Mar 2, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR comprehensively implements support for nested loop/parallel execution across all four nesting combinations. The implementation is thorough with proper sentinel-based boundary resolution, subflow graph cloning, unified parent maps, and scope cleanup. All tests pass.

Verified Fixes from Prior Review Rounds:

  • Cycle guards added to ancestor-chain traversals in workflow.tsx (lines 1024, 2641, 2645)
  • Stale cloned loop scopes and subflowParentMap entries are now cleaned up consistently in loop.ts
  • findEffectiveContainerId fallback scan removed; direct lookup only
  • Sentinel edge handles correctly selected (LOOP_EXIT vs PARALLEL_EXIT) in parallel-expansion.ts
  • Test coverage for loop-in-parallel wiring added to edges.test.ts

Two Remaining Findings:

  1. Missing cycle guard in nestingLevel useMemo (subflow-node.tsx) — mirrors guards added elsewhere for consistency
  2. Parallel-config scan in buildSubflowParentMap can silently overwrite loop-config entries — defensive logging recommended

Safe to merge with these two minor defensive hardening improvements.

Confidence Score: 4/5

  • Safe to merge; core functionality is solid with comprehensive test coverage (601 executor tests pass). Two minor defensive improvements recommended.
  • All major cycle-guard and cleanup issues identified in prior reviews have been addressed. The two remaining findings are minor and low-risk: (1) a missing cycle guard in a React useMemo that mirrors guards added elsewhere in the PR, and (2) an edge-case defensive improvement for invalid workflow data. The implementation is well-tested and properly handles all nesting combinations.
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx (missing cycle guard), apps/sim/executor/execution/executor.ts (defensive logging for config conflicts)

Last reviewed commit: 2a6b631

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

39 files reviewed, 10 comments

Edit Code Review Agent Settings | Greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 3, 2026

Additional Comments (1)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts
Empty workflowId in synthetic iteration entries when iterBlocks is empty

When a synthetic iteration group has no direct topLevelIterationEntries (iterBlocks is empty — only strippedNestedEntries exist), iterBlocks[0]?.workflowId evaluates to undefined, so workflowId falls back to ''. Since strippedNestedEntries always has at least one entry in this branch (otherwise allIterEntries.length === 0 returns null earlier), the workflowId and executionId could be sourced from there as a fallback.

          const syntheticIteration: ConsoleEntry = {
            id: `${idPrefix}iteration-${iterationType}-${iterGroup.iterationContainerId}-${iterGroup.iterationCurrent}-${iterBlocks[0]?.executionId ?? strippedNestedEntries[0]?.executionId ?? 'unknown'}`,
            timestamp: new Date(iterStartMs).toISOString(),
            workflowId: iterBlocks[0]?.workflowId || strippedNestedEntries[0]?.workflowId || '',
            blockId: `iteration-${iterGroup.iterationContainerId}-${iterGroup.iterationCurrent}`,
            blockName: `Iteration ${iterGroup.iterationCurrent}${iterGroup.iterationTotal !== undefined ? ` / ${iterGroup.iterationTotal}` : ''}`,
            blockType: iterationType,
            executionId: iterBlocks[0]?.executionId ?? strippedNestedEntries[0]?.executionId,
            startedAt: new Date(iterStartMs).toISOString(),
            executionOrder: iterExecutionOrder,
            endedAt: new Date(iterEndMs).toISOString(),
            durationMs: iterDisplayDuration,
            success: !allIterEntries.some((b) => b.error),
            iterationCurrent: iterGroup.iterationCurrent,
            iterationTotal: iterGroup.iterationTotal,
            iterationType: iterationType as 'loop' | 'parallel',
            iterationContainerId: iterGroup.iterationContainerId,
          }

waleedlatif1 and others added 3 commits March 2, 2026 22:02
* feat(agent): add MCP server discovery mode for agent tool input

* fix(tool-input): use type variant for MCP server tool count badge

* fix(mcp-dynamic-args): align label styling with standard subblock labels

* standardized inp format UI

* feat(tool-input): replace MCP server inline expand with drill-down navigation

* feat(tool-input): add chevron affordance and keyboard nav for MCP server drill-down

* fix(tool-input): handle mcp-server type in refresh, validation, badges, and usage control

* refactor(tool-validation): extract getMcpServerIssue, remove fake tool hack

* lint

* reorder dropdown

* perf(agent): parallelize MCP server tool creation with Promise.all

* fix(combobox): preserve cursor movement in search input, reset query on drilldown

* fix(combobox): route ArrowRight through handleSelect, remove redundant type guards

* fix(agent): rename mcpServers to mcpServerSelections to avoid shadowing DB import, route ArrowRight through handleSelect

* docs: update google integration docs

* fix(tool-input): reset drilldown state on tool selection to prevent stale view

* perf(agent): parallelize MCP server discovery across multiple servers
…s anti-pattern (#3357)

* improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern

- convert 51 test files from vi.resetModules/vi.doMock/dynamic import to vi.hoisted/vi.mock/static import
- add global @sim/db mock to vitest.setup.ts
- switch 4 test files from jsdom to node environment
- remove all vi.importActual calls that loaded heavy modules (200+ block files)
- remove slow mockConsoleLogger/mockAuth/setupCommonApiMocks helpers
- reduce real setTimeout delays in engine tests
- mock heavy transitive deps in diff-engine test

test execution time: 34s -> 9s (3.9x faster)
environment time: 2.5s -> 0.6s (4x faster)

* docs(testing): update testing best practices with performance rules

- document vi.hoisted + vi.mock + static import as the standard pattern
- explicitly ban vi.resetModules, vi.doMock, vi.importActual, mockAuth, setupCommonApiMocks
- document global mocks from vitest.setup.ts
- add mock pattern reference for auth, hybrid auth, and database chains
- add performance rules section covering heavy deps, jsdom vs node, real timers

* fix(tests): fix 4 failing test files with missing mocks

- socket/middleware/permissions: add vi.mock for @/lib/auth to prevent transitive getBaseUrl() call
- workflow-handler: add vi.mock for @/executor/utils/http matching executor mock pattern
- evaluator-handler: add db.query.account mock structure before vi.spyOn
- router-handler: same db.query.account fix as evaluator

* fix(tests): replace banned Function type with explicit callback signature
* feat(databricks): add Databricks integration with 8 tools

Add complete Databricks integration supporting SQL execution, job management,
run monitoring, and cluster listing via Personal Access Token authentication.

Tools: execute_sql, list_jobs, run_job, get_run, list_runs, cancel_run,
get_run_output, list_clusters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(databricks): throw on invalid JSON params, fix boolean coercion, add expandTasks field

- Throw errors on invalid JSON in jobParameters/notebookParams instead of silently defaulting to {}
- Always set boolean params explicitly to prevent string 'false' being truthy
- Add missing expandTasks dropdown UI field for list_jobs operation

* fix(databricks): align tool inputs/outputs with official API spec

- execute_sql: fix wait_timeout default description (50s, not 10s)
- get_run: add queueDuration field, update lifecycle/result state enums
- get_run_output: fix notebook output size (5 MB not 1 MB), add logsTruncated field
- list_runs: add userCancelledOrTimedout to state, fix limit range (1-24), update state enums
- list_jobs: fix name filter description to "exact case-insensitive"
- list_clusters: add PIPELINE_MAINTENANCE to ClusterSource enum

* fix(databricks): regenerate docs to reflect API spec fixes

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (3)

apps/sim/executor/orchestrators/parallel.ts, line 260
Removed count-type parallel guard may cause regression

The explicit if (config.parallelType === 'count') return [] guard was deleted from resolveDistributionItems. For count-type parallels, distribution is typically undefined (falling into the new early-return), but if a user switches a parallel from collection to count via the UI and the old distribution value is preserved in the serialized workflow, the new code will fall through and attempt to resolve that value as an items list.

The original guard was an intentional safety net for exactly this case — it ensured that regardless of what distribution contains, count-type parallels always produce an empty items array. Consider restoring it:

  private resolveDistributionItems(ctx: ExecutionContext, config: SerializedParallel): any[] {
    if (config.parallelType === 'count') {
      return []
    }

    if (
      config.distribution === undefined ||
      config.distribution === null ||
      config.distribution === ''
    ) {
      return []
    }

apps/sim/executor/utils/parallel-expansion.ts, line 320
Clone configs and nodes accumulate across loop iterations

cloneSubflowGraph registers cloned subflows in dag.parallelConfigs, dag.loopConfigs, and dag.nodes via cloneDAGNode, but none of these entries are ever removed between outer-loop iterations. Each call to expandParallel (one per loop iteration) adds (branchCount - 1) new clone configs and their associated DAG nodes, which persist for the lifetime of the execution.

For a loop with K iterations containing a parallel with N branches, this adds K × (N-1) clone entries to the DAG maps. At the configured maximums (1 000 iterations × 20 branches), that is ~19 000 accumulated entries.

A secondary effect is that collectClonedSubflowNodes (used by clearLoopExecutionState) scans all keys in dag.loopConfigs / dag.parallelConfigs for matching prefixes, so it collects stale clones from every previous iteration on each cleanup pass. state.unmarkExecuted on already-unmarked nodes is harmless, but the linear scan cost grows with each iteration.

Consider tracking which clone IDs were created by each expandParallel call (e.g., return them in ExpansionResult alongside clonedSubflows) and removing them from the DAG maps inside deleteParallelScopeAndClones / resetNestedParallelScopes.


apps/sim/executor/execution/executor.ts, line 275
buildSubflowParentMap parallel scan silently overwrites loop scan entries

The loop scan runs first, then the parallel scan iterates dag.parallelConfigs and calls parentMap.set(nodeId, ...) for any child that is itself a subflow. If the same child subflow ID appears as a direct child of both a loop and a parallel (e.g., a shared subflow referenced from two different containers due to data corruption or a future edge-case workflow shape), the parallel scan will silently overwrite the loop scan's entry with parentType: 'parallel'.

More importantly, even in the expected "clean" case, the ordering means parentType: 'parallel' always wins over parentType: 'loop' when both scans touch the same key. Adding a defensive check to warn when an overwrite would occur helps surface unexpected configurations:

for (const nodeId of config.nodes ?? []) {
  if (dag.parallelConfigs.has(nodeId) || dag.loopConfigs.has(nodeId)) {
    if (parentMap.has(nodeId)) {
      logger.warn('Subflow has multiple parents in buildSubflowParentMap', {
        nodeId,
        existing: parentMap.get(nodeId),
        incoming: { parentId: parallelId, parentType: 'parallel' },
      })
    }
    parentMap.set(nodeId, { parentId: parallelId, parentType: 'parallel' })
  }
}

waleedlatif1 and others added 12 commits March 3, 2026 18:50
…of distributionItems

The distributionItems fallback was never part of SerializedParallel — it
only worked through any typing. Updated the test to use the real
distribution property.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d update test

findParallelBoundaryNodes now skips LOOP_CONTINUE back-edges when
detecting terminal nodes, matching findLoopBoundaryNodes behavior.
Without this, a nested loop's back-edge was incorrectly counted as a
forward edge within the parallel, preventing terminal detection.

Also updated parallel resolver test to use the real distribution
property instead of the non-existent distributionItems fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lones

When a parallel contains a nested loop, cloned loop scopes (__obranch-N)
created by expandParallel were not being deleted, causing stale scopes to
persist across outer loop iterations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion, restore executionOrder

- Remove unreachable `?? candidateIds[0]` fallbacks in loop/parallel resolvers
- Remove arbitrary first-match fallback scan in findEffectiveContainerId
- Fix edgeCrossesLoopBoundary to use innermost loop detection for nested loops
- Add warning log for missing branch outputs in parallel aggregation
- Restore executionOrder on WorkflowNodeMetadata and pipe through child workflow notification
- Remove dead sim-drag-subflow classList.remove call
- Clean up cloned loop subflowParentMap entries in deleteParallelScopeAndClones

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ed blocks

findAncestorLoops only checked direct loop membership, missing blocks nested
inside parallels within loops (and vice versa). Refactored to walk through
both loop and parallel containers recursively, so a block inside a parallel
inside a loop correctly sees the loop's contextual tags (index, currentItem)
instead of the loop's output tags (results).

Also fixed parallel ancestor detection to handle nested parallel-in-loop and
loop-in-parallel scenarios, collecting all ancestor parallels instead of just
the immediate containing one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ghting

Remove !important z-index overrides that prevented nested subflows from
being grabbed/dragged independently. Z-index is now managed by ReactFlow's
elevateNodesOnSelect and per-node zIndex: depth props. Also adds execution
status highlighting for nested subflows in both canvas and snapshot preview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

Prevents infinite recursion if subflowChildrenMap contains circular
references by tracking visited nodes during traversal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@cursor review

Copy link

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (2)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx, line 87
Missing cycle guard in nestingLevel parent-chain traversal

The while (currentParentId) loop lacks a visited set to prevent infinite loops if the parentId chain ever forms a cycle. The PR already adds visited-set guards in several analogous loops (buildParentIterations, getNodeHierarchy, isLoopNestedInside, buildUnifiedParentIterations). The same pattern should be applied here for consistency and defensive safety:

const nestingLevel = useMemo(() => {
  let level = 0
  let currentParentId = data?.parentId
  const visited = new Set<string>()

  while (currentParentId && !visited.has(currentParentId)) {
    visited.add(currentParentId)
    level++
    const parentNode = getNodes().find((n) => n.id === currentParentId)
    if (!parentNode) break
    currentParentId = parentNode.data?.parentId
  }

  return level
}, [id, data?.parentId, getNodes])

apps/sim/executor/execution/executor.ts, line 399
Parallel-config scan can silently overwrite loop-config scan for the same child

buildSubflowParentMap runs the loop scan first, then the parallel scan. Because parentMap.set is unconditional, any nodeId that appears in both a loop config's nodes and a parallel config's nodes will end up with parentType: 'parallel', regardless of which scan found the correct parent.

A valid workflow guarantees each container has exactly one parent, so this normally cannot happen. However, consider an edge case: if the same nested subflow ID is accidentally registered in both a loop config and a parallel config (e.g. during a migration or a copy-paste bug in the serializer), the parallel entry silently wins and buildUnifiedParentIterations will walk the wrong ancestor chain, producing incorrect iteration context without any warning.

A defensive guard would make the conflict visible:

// Scan parallel configs: children can be parallels or loops
for (const [parallelId, config] of dag.parallelConfigs) {
  for (const nodeId of config.nodes ?? []) {
    if (dag.parallelConfigs.has(nodeId) || dag.loopConfigs.has(nodeId)) {
      if (parentMap.has(nodeId)) {
        logger.warn('Subflow registered under multiple parents — keeping loop entry', {
          nodeId,
          loopParent: parentMap.get(nodeId)?.parentId,
          parallelParent: parallelId,
        })
        continue
      }
      parentMap.set(nodeId, { parentId: parallelId, parentType: 'parallel' })
    }
  }
}

@waleedlatif1 waleedlatif1 merged commit 2c79d02 into staging Mar 4, 2026
12 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/loop_nesting branch March 4, 2026 03:21
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.

3 participants