Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b7e377e
v0.5.91: docs i18n, turborepo upgrade
waleedlatif1 Feb 16, 2026
da46a38
v0.5.92: shortlinks, copilot scrolling stickiness, pagination
waleedlatif1 Feb 17, 2026
fdca736
v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot …
waleedlatif1 Feb 18, 2026
15ace5e
v0.5.94: vercel integration, folder insertion, migrated tracking redi…
waleedlatif1 Feb 19, 2026
67aa4bb
v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, up…
waleedlatif1 Feb 20, 2026
34d92fa
v0.5.96: sim oauth provider, slack ephemeral message tool and blockki…
waleedlatif1 Feb 21, 2026
115f04e
v0.5.97: oidc discovery for copilot mcp
waleedlatif1 Feb 21, 2026
0d86ea0
v0.5.98: change detection improvements, rate limit and code execution…
waleedlatif1 Feb 22, 2026
af59234
v0.5.99: local dev improvements, live workflow logs in terminal
waleedlatif1 Feb 23, 2026
67f8a68
v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log i…
waleedlatif1 Feb 25, 2026
4fd0989
v0.5.101: circular dependency mitigation, confluence enhancements, go…
waleedlatif1 Feb 26, 2026
198e2c2
feat(executor): support nested loop DAG construction and edge wiring
abram0v1ch Mar 1, 2026
7ecbe5d
feat(executor): add nested loop iteration context and named loop vari…
abram0v1ch Mar 1, 2026
acfd6d6
feat(terminal): propagate parent iteration context through SSE events…
abram0v1ch Mar 1, 2026
c140523
feat(canvas): allow nesting subflow containers and prevent cycles
abram0v1ch Mar 1, 2026
968ed58
feat(agent): add MCP server discovery mode for agent tool input (#3353)
waleedlatif1 Feb 26, 2026
86736d0
improvement(tests): speed up unit tests by eliminating vi.resetModule…
waleedlatif1 Feb 26, 2026
b87563b
feat(databricks): add Databricks integration with 8 tools (#3361)
waleedlatif1 Feb 27, 2026
a94e634
feat(luma): add Luma integration for event and guest management (#3364)
waleedlatif1 Feb 27, 2026
1f3110f
feat(gamma): add gamma integration for AI-powered content generation …
waleedlatif1 Feb 27, 2026
740aee6
feat(greenhouse): add greenhouse integration for managing candidates,…
waleedlatif1 Feb 27, 2026
9f677f3
feat(ashby): add ashby integration for candidate, job, and applicatio…
waleedlatif1 Feb 27, 2026
a90eb31
improvement(oauth): reordered oauth modal (#3368)
waleedlatif1 Feb 27, 2026
665e7cc
feat(loops): add Loops email platform integration (#3359)
waleedlatif1 Feb 27, 2026
39c9c80
feat(resend): expand integration with contacts, domains, and enhanced…
waleedlatif1 Feb 27, 2026
6e17cc4
improvement(blocks): update luma styling and linkup field modes (#3370)
waleedlatif1 Feb 27, 2026
c00c5f1
feat(x): add 28 new X API v2 tool integrations and expand OAuth scope…
waleedlatif1 Feb 27, 2026
c3c3649
improvement(docs): audit and standardize tool description sections, u…
waleedlatif1 Feb 27, 2026
6647241
improvement(x): align OAuth scopes, add scope descriptions, and set o…
waleedlatif1 Feb 27, 2026
9265b9c
improvement(ci): add sticky disk caches and bump runner for faster bu…
waleedlatif1 Feb 27, 2026
a1cb4c0
improvement(selectors): make selectorKeys declarative (#3374)
icecrasher321 Feb 27, 2026
d9cc375
improvement(selectors): consolidate selector input logic (#3375)
icecrasher321 Feb 27, 2026
a2a7d1a
feat(google-contacts): add google contacts integration (#3340)
waleedlatif1 Feb 27, 2026
828475c
improvement(mcp): add all MCP server tools individually instead of as…
waleedlatif1 Feb 27, 2026
eac3c5b
fix(sse): fix memory leaks in SSE stream cleanup and add memory telem…
waleedlatif1 Feb 28, 2026
80adb61
improvement(ashby): validate ashby integration and update skill files…
waleedlatif1 Feb 28, 2026
ce29e60
improvement(luma): expand host response fields and harden event ID in…
waleedlatif1 Feb 28, 2026
43a86dd
improvement(resend): add error handling, authMode, and naming consist…
waleedlatif1 Feb 28, 2026
1a51664
fix(chat-deploy): fix launch chat popup and auth persistence, clean u…
waleedlatif1 Feb 28, 2026
8d2ae12
improvement(loops): validate loops integration and update skill files…
waleedlatif1 Feb 28, 2026
9fa8d3c
fix(monitoring): set MemoryTelemetry logger to INFO level for product…
waleedlatif1 Feb 28, 2026
0eb3f73
feat(integrations): add amplitude, google pagespeed insights, and pag…
waleedlatif1 Mar 1, 2026
d16784e
feat(docs): add API reference with OpenAPI spec and auto-generated en…
waleedlatif1 Mar 2, 2026
367ccf6
fix(icons): fix pagerduty icon (#3392)
waleedlatif1 Mar 2, 2026
7ff85a4
improvement(executor): audit and harden nested loop/parallel implemen…
waleedlatif1 Mar 2, 2026
45ec8b2
improvement(executor): audit and harden nested loop/parallel implemen…
waleedlatif1 Mar 3, 2026
55d8099
improvement(executor): audit fixes for nested subflow implementation
waleedlatif1 Mar 3, 2026
ecfb48f
finished
waleedlatif1 Mar 3, 2026
bfbad9c
improvement(airtable): added more tools (#3396)
waleedlatif1 Mar 2, 2026
ea51d8c
fix(layout): polyfill crypto.randomUUID for non-secure HTTP contexts …
waleedlatif1 Mar 2, 2026
35634a4
feat(integrations): add dub.co integration (#3400)
waleedlatif1 Mar 2, 2026
310687a
fix(memory): fix O(n²) string concatenation and unconsumed fetch resp…
waleedlatif1 Mar 2, 2026
fd2e15b
chore(careers): remove careers page, redirect to Ashby jobs portal (#…
waleedlatif1 Mar 2, 2026
7b89b20
feat(integrations): add google meet integration (#3403)
waleedlatif1 Mar 3, 2026
a54324e
ack comments
waleedlatif1 Mar 3, 2026
2e61711
fix(terminal): deduplicate nested container entries in buildEntryTree
waleedlatif1 Mar 3, 2026
15f3d9c
improvement(executor): clean up nested subflow implementation
waleedlatif1 Mar 3, 2026
2b97791
fix(test): update parallel resolver test to use distribution instead …
waleedlatif1 Mar 3, 2026
9205786
fix(executor): skip loop back-edges in parallel boundary detection an…
waleedlatif1 Mar 3, 2026
f972409
fix(executor): clean up cloned loop scopes in deleteParallelScopeAndC…
waleedlatif1 Mar 3, 2026
837e7c8
fix(executor): remove dead fallbacks, fix nested loop boundary detect…
waleedlatif1 Mar 3, 2026
0386055
leftover
waleedlatif1 Mar 3, 2026
6af3d53
upgrade turborepo
waleedlatif1 Mar 3, 2026
ab07a72
update stagehand icon
waleedlatif1 Mar 3, 2026
3bac0dd
fix(tag-dropdown): show contextual loop/parallel tags for deeply nest…
waleedlatif1 Mar 3, 2026
8093739
testing
waleedlatif1 Mar 3, 2026
f8de34b
fixed dedicated logs
waleedlatif1 Mar 3, 2026
ea4fcdc
fix
waleedlatif1 Mar 4, 2026
27ed713
fix(subflows): enable nested subflow interaction and execution highli…
waleedlatif1 Mar 4, 2026
d714149
Merge branch 'staging' into feat/loop_nesting
waleedlatif1 Mar 4, 2026
2a6b631
fix(preview): add cycle guard to recursive subflow status derivation
waleedlatif1 Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 14 additions & 150 deletions apps/docs/components/icons.tsx

Large diffs are not rendered by default.

10 changes: 1 addition & 9 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -833,15 +833,7 @@ input[type="search"]::-ms-clear {
animation: growShrink 1.5s infinite ease-in-out;
}

/* Subflow node z-index and drag-over styles */
.workflow-container .react-flow__node-subflowNode {
z-index: -1 !important;
}

.workflow-container .react-flow__node-subflowNode:has([data-subflow-selected="true"]) {
z-index: 10 !important;
}

/* Subflow node drag-over styles */
.loop-node-drag-over,
.parallel-node-drag-over {
box-shadow: 0 0 0 1.75px var(--brand-secondary) !important;
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationTotal: iterationContext.iterationTotal,
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
...(iterationContext.parentIterations?.length && {
parentIterations: iterationContext.parentIterations,
}),
}),
...(childWorkflowContext && {
childWorkflowBlockId: childWorkflowContext.parentBlockId,
Expand Down Expand Up @@ -884,6 +887,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationTotal: iterationContext.iterationTotal,
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
...(iterationContext.parentIterations?.length && {
parentIterations: iterationContext.parentIterations,
}),
}),
...childWorkflowData,
...instanceData,
Expand Down Expand Up @@ -915,6 +921,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationTotal: iterationContext.iterationTotal,
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
...(iterationContext.parentIterations?.length && {
parentIterations: iterationContext.parentIterations,
}),
}),
...childWorkflowData,
...instanceData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,93 +1167,122 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
{} as Record<string, { type: string; id: string }>
)

let loopBlockGroup: BlockTagGroup | null = null
const loopBlockGroups: BlockTagGroup[] = []
const ancestorLoopIds = new Set<string>()
const visitedContainerIds = new Set<string>()

const findAncestorContainers = (targetId: string) => {
if (visitedContainerIds.has(targetId)) return
visitedContainerIds.add(targetId)

// Check if targetId is directly inside any loop
for (const [loopId, loop] of Object.entries(loops)) {
if (loop.nodes.includes(targetId) && !ancestorLoopIds.has(loopId)) {
ancestorLoopIds.add(loopId)
const loopBlock = blocks[loopId]
if (loopBlock) {
const loopType = loop.loopType || 'for'
const loopBlockName = loopBlock.name || loopBlock.type
const normalizedLoopName = normalizeName(loopBlockName)
const contextualTags: string[] = [`${normalizedLoopName}.index`]
if (loopType === 'forEach') {
contextualTags.push(`${normalizedLoopName}.currentItem`)
contextualTags.push(`${normalizedLoopName}.items`)
}
loopBlockGroups.push({
blockName: loopBlockName,
blockId: loopId,
blockType: 'loop',
tags: contextualTags,
distance: 0,
isContextual: true,
})
}
findAncestorContainers(loopId)
}
}
// Also walk through containing parallels so we find loops that contain
// the parallel (e.g. block inside parallel inside loop)
for (const [parallelId, parallel] of Object.entries(parallels || {})) {
if (parallel.nodes.includes(targetId)) {
findAncestorContainers(parallelId)
}
}
}

const isLoopBlock = blocks[blockId]?.type === 'loop'
const currentLoop = isLoopBlock ? loops[blockId] : null

const containingLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(blockId))

let containingLoopBlockId: string | null = null

if (currentLoop && isLoopBlock) {
containingLoopBlockId = blockId
const loopType = currentLoop.loopType || 'for'

if (isLoopBlock && loops[blockId]) {
const loop = loops[blockId]
ancestorLoopIds.add(blockId)
const loopBlock = blocks[blockId]
if (loopBlock) {
const loopType = loop.loopType || 'for'
const loopBlockName = loopBlock.name || loopBlock.type
const normalizedLoopName = normalizeName(loopBlockName)
const contextualTags: string[] = [`${normalizedLoopName}.index`]
if (loopType === 'forEach') {
contextualTags.push(`${normalizedLoopName}.currentItem`)
contextualTags.push(`${normalizedLoopName}.items`)
}

loopBlockGroup = {
loopBlockGroups.push({
blockName: loopBlockName,
blockId: blockId,
blockType: 'loop',
tags: contextualTags,
distance: 0,
isContextual: true,
}
})
}
} else if (containingLoop) {
const [loopId, loop] = containingLoop
containingLoopBlockId = loopId
const loopType = loop.loopType || 'for'

const containingLoopBlock = blocks[loopId]
if (containingLoopBlock) {
const loopBlockName = containingLoopBlock.name || containingLoopBlock.type
const normalizedLoopName = normalizeName(loopBlockName)
const contextualTags: string[] = [`${normalizedLoopName}.index`]
if (loopType === 'forEach') {
contextualTags.push(`${normalizedLoopName}.currentItem`)
contextualTags.push(`${normalizedLoopName}.items`)
}
findAncestorContainers(blockId)
} else {
findAncestorContainers(blockId)
}

loopBlockGroup = {
blockName: loopBlockName,
blockId: loopId,
blockType: 'loop',
tags: contextualTags,
distance: 0,
isContextual: true,
const parallelBlockGroups: BlockTagGroup[] = []
const ancestorParallelIds = new Set<string>()
const visitedParallelTargets = new Set<string>()

const findAncestorParallels = (targetId: string) => {
if (visitedParallelTargets.has(targetId)) return
visitedParallelTargets.add(targetId)

for (const [parallelId, parallel] of Object.entries(parallels || {})) {
if (parallel.nodes.includes(targetId) && !ancestorParallelIds.has(parallelId)) {
ancestorParallelIds.add(parallelId)
const parallelBlock = blocks[parallelId]
if (parallelBlock) {
const parallelType = parallel.parallelType || 'count'
const parallelBlockName = parallelBlock.name || parallelBlock.type
const normalizedParallelName = normalizeName(parallelBlockName)
const contextualTags: string[] = [`${normalizedParallelName}.index`]
if (parallelType === 'collection') {
contextualTags.push(`${normalizedParallelName}.currentItem`)
contextualTags.push(`${normalizedParallelName}.items`)
}
parallelBlockGroups.push({
blockName: parallelBlockName,
blockId: parallelId,
blockType: 'parallel',
tags: contextualTags,
distance: 0,
isContextual: true,
})
}
// Walk up through containing loops and parallels
for (const [loopId, loop] of Object.entries(loops)) {
if (loop.nodes.includes(parallelId)) {
findAncestorParallels(loopId)
}
}
findAncestorParallels(parallelId)
}
}
}

let parallelBlockGroup: BlockTagGroup | null = null
const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) =>
parallel.nodes.includes(blockId)
)
let containingParallelBlockId: string | null = null
if (containingParallel) {
const [parallelId, parallel] = containingParallel
containingParallelBlockId = parallelId
const parallelType = parallel.parallelType || 'count'

const containingParallelBlock = blocks[parallelId]
if (containingParallelBlock) {
const parallelBlockName = containingParallelBlock.name || containingParallelBlock.type
const normalizedParallelName = normalizeName(parallelBlockName)
const contextualTags: string[] = [`${normalizedParallelName}.index`]
if (parallelType === 'collection') {
contextualTags.push(`${normalizedParallelName}.currentItem`)
contextualTags.push(`${normalizedParallelName}.items`)
}

parallelBlockGroup = {
blockName: parallelBlockName,
blockId: parallelId,
blockType: 'parallel',
tags: contextualTags,
distance: 0,
isContextual: true,
}
}
findAncestorParallels(blockId)
// Also check through ancestor loops (a block in a loop that's in a parallel)
for (const loopId of ancestorLoopIds) {
findAncestorParallels(loopId)
}

const blockTagGroups: BlockTagGroup[] = []
Expand All @@ -1275,8 +1304,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (!blockConfig) {
if (accessibleBlock.type === 'loop' || accessibleBlock.type === 'parallel') {
if (
accessibleBlockId === containingLoopBlockId ||
accessibleBlockId === containingParallelBlockId
ancestorLoopIds.has(accessibleBlockId) ||
ancestorParallelIds.has(accessibleBlockId)
) {
continue
}
Expand Down Expand Up @@ -1366,12 +1395,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}

const finalBlockTagGroups: BlockTagGroup[] = []
if (loopBlockGroup) {
finalBlockTagGroups.push(loopBlockGroup)
}
if (parallelBlockGroup) {
finalBlockTagGroups.push(parallelBlockGroup)
}
finalBlockTagGroups.push(...loopBlockGroups)
finalBlockTagGroups.push(...parallelBlockGroups)

blockTagGroups.sort((a, b) => a.distance - b.distance)
finalBlockTagGroups.push(...blockTagGroups)
Expand Down Expand Up @@ -1570,21 +1595,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (variableObj) {
processedTag = tag
}
} else if (
blockGroup?.isContextual &&
(blockGroup.blockType === 'loop' || blockGroup.blockType === 'parallel')
) {
const tagParts = tag.split('.')
if (tagParts.length === 1) {
processedTag = blockGroup.blockType
} else {
const lastPart = tagParts[tagParts.length - 1]
if (['index', 'currentItem', 'items'].includes(lastPart)) {
processedTag = `${blockGroup.blockType}.${lastPart}`
} else {
processedTag = tag
}
}
}

let newValue: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ const ToolbarItem = memo(function ToolbarItem({

const handleDragStart = useCallback(
(e: React.DragEvent<HTMLElement>) => {
if (!isTrigger && (item.type === 'loop' || item.type === 'parallel')) {
document.body.classList.add('sim-drag-subflow')
}
const iconElement = e.currentTarget.querySelector('.toolbar-item-icon')
onDragStart(e, item.type, isTriggerCapable, {
name: item.name,
Expand All @@ -80,12 +77,6 @@ const ToolbarItem = memo(function ToolbarItem({
[item.type, item.name, item.bgColor, isTriggerCapable, onDragStart, isTrigger]
)

const handleDragEnd = useCallback(() => {
if (!isTrigger) {
document.body.classList.remove('sim-drag-subflow')
}
}, [isTrigger])

const handleClick = useCallback(() => {
onClick(item.type, isTriggerCapable)
}, [item.type, isTriggerCapable, onClick])
Expand Down Expand Up @@ -114,7 +105,6 @@ const ToolbarItem = memo(function ToolbarItem({
tabIndex={-1}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onClick={handleClick}
onContextMenu={handleContextMenu}
className={clsx(
Expand Down
Loading