mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-24 22:38:00 -05:00
Compare commits
5 Commits
fix/blog-0
...
fix/edge-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1735bc8a28 | ||
|
|
bff4476cbe | ||
|
|
3bbf7f5d1d | ||
|
|
68683258c3 | ||
|
|
fc7f56e21b |
@@ -124,11 +124,44 @@ Choose between four types of loops:
|
||||
3. Drag other blocks inside the loop container
|
||||
4. Connect the blocks as needed
|
||||
|
||||
### Accessing Results
|
||||
### Referencing Loop Data
|
||||
|
||||
After a loop completes, you can access aggregated results:
|
||||
There's an important distinction between referencing loop data from **inside** vs **outside** the loop:
|
||||
|
||||
- **`<loop.results>`**: Array of results from all loop iterations
|
||||
<Tabs items={['Inside the Loop', 'Outside the Loop']}>
|
||||
<Tab>
|
||||
**Inside the loop**, use `<loop.>` references to access the current iteration context:
|
||||
|
||||
- **`<loop.index>`**: Current iteration number (0-based)
|
||||
- **`<loop.currentItem>`**: Current item being processed (forEach only)
|
||||
- **`<loop.items>`**: Full collection being iterated (forEach only)
|
||||
|
||||
```
|
||||
// Inside a Function block within the loop
|
||||
const idx = <loop.index>; // 0, 1, 2, ...
|
||||
const item = <loop.currentItem>; // Current item
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
These references are only available for blocks **inside** the loop container. They give you access to the current iteration's context.
|
||||
</Callout>
|
||||
</Tab>
|
||||
<Tab>
|
||||
**Outside the loop** (after it completes), reference the loop block by its name to access aggregated results:
|
||||
|
||||
- **`<LoopBlockName.results>`**: Array of results from all iterations
|
||||
|
||||
```
|
||||
// If your loop block is named "Process Items"
|
||||
const allResults = <processitems.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
After the loop completes, use the loop's block name (not `loop.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||
</Callout>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -184,28 +217,29 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
Available **inside** the loop only:
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.currentItem</strong>: Current item being processed
|
||||
<strong>{"<loop.index>"}</strong>: Current iteration number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.index</strong>: Current iteration number (0-based)
|
||||
<strong>{"<loop.currentItem>"}</strong>: Current item being processed (forEach only)
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.items</strong>: Full collection (forEach loops)
|
||||
<strong>{"<loop.items>"}</strong>: Full collection (forEach only)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.results</strong>: Array of all iteration results
|
||||
<strong>{"<blockname.results>"}</strong>: Array of all iteration results (accessed via block name)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Structure</strong>: Results maintain iteration order
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the loop
|
||||
<strong>Access</strong>: Available in blocks after the loop completes
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
|
||||
@@ -76,11 +76,44 @@ Choose between two types of parallel execution:
|
||||
3. Drag a single block inside the parallel container
|
||||
4. Connect the block as needed
|
||||
|
||||
### Accessing Results
|
||||
### Referencing Parallel Data
|
||||
|
||||
After a parallel block completes, you can access aggregated results:
|
||||
There's an important distinction between referencing parallel data from **inside** vs **outside** the parallel block:
|
||||
|
||||
- **`<parallel.results>`**: Array of results from all parallel instances
|
||||
<Tabs items={['Inside the Parallel', 'Outside the Parallel']}>
|
||||
<Tab>
|
||||
**Inside the parallel**, use `<parallel.>` references to access the current instance context:
|
||||
|
||||
- **`<parallel.index>`**: Current instance number (0-based)
|
||||
- **`<parallel.currentItem>`**: Item for this instance (collection-based only)
|
||||
- **`<parallel.items>`**: Full collection being distributed (collection-based only)
|
||||
|
||||
```
|
||||
// Inside a Function block within the parallel
|
||||
const idx = <parallel.index>; // 0, 1, 2, ...
|
||||
const item = <parallel.currentItem>; // This instance's item
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
These references are only available for blocks **inside** the parallel container. They give you access to the current instance's context.
|
||||
</Callout>
|
||||
</Tab>
|
||||
<Tab>
|
||||
**Outside the parallel** (after it completes), reference the parallel block by its name to access aggregated results:
|
||||
|
||||
- **`<ParallelBlockName.results>`**: Array of results from all instances
|
||||
|
||||
```
|
||||
// If your parallel block is named "Process Tasks"
|
||||
const allResults = <processtasks.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
After the parallel completes, use the parallel's block name (not `parallel.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||
</Callout>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -98,11 +131,11 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
|
||||
|
||||
### Result Aggregation
|
||||
|
||||
Results from all parallel instances are automatically collected:
|
||||
Results from all parallel instances are automatically collected and accessible via the block name:
|
||||
|
||||
```javascript
|
||||
// In a Function block after the parallel
|
||||
const allResults = input.parallel.results;
|
||||
// In a Function block after a parallel named "Process Tasks"
|
||||
const allResults = <processtasks.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
@@ -158,25 +191,26 @@ Understanding when to use each:
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
Available **inside** the parallel only:
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.currentItem</strong>: Item for this instance
|
||||
<strong>{"<parallel.index>"}</strong>: Instance number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.index</strong>: Instance number (0-based)
|
||||
<strong>{"<parallel.currentItem>"}</strong>: Item for this instance (collection-based only)
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.items</strong>: Full collection (collection-based)
|
||||
<strong>{"<parallel.items>"}</strong>: Full collection (collection-based only)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.results</strong>: Array of all instance results
|
||||
<strong>{"<blockname.results>"}</strong>: Array of all instance results (accessed via block name)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the parallel
|
||||
<strong>Access</strong>: Available in blocks after the parallel completes
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
|
||||
@@ -1260,7 +1260,6 @@ async function handleRun(
|
||||
const instance = getClientTool(toolCall.id)
|
||||
|
||||
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||
setToolCallState(toolCall, 'executing')
|
||||
onStateChange?.('executing')
|
||||
try {
|
||||
await useCopilotStore.getState().executeIntegrationTool(toolCall.id)
|
||||
|
||||
@@ -496,7 +496,7 @@ export function DeployModal({
|
||||
</div>
|
||||
)}
|
||||
{apiDeployWarnings.length > 0 && (
|
||||
<div className='mb-3 rounded-[4px] border border-amber-500/30 bg-amber-500/10 p-3 text-amber-700 dark:text-amber-400 text-sm'>
|
||||
<div className='mb-3 rounded-[4px] border border-amber-500/30 bg-amber-500/10 p-3 text-amber-700 text-sm dark:text-amber-400'>
|
||||
<div className='font-semibold'>Deployment Warning</div>
|
||||
{apiDeployWarnings.map((warning, index) => (
|
||||
<div key={index}>{warning}</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ Copilot supports slash commands that trigger specialized capabilities:
|
||||
|
||||
- `/fast` — uses a faster model for quick responses when you need speed over depth
|
||||
- `/research` — performs multi-step web research on a topic, synthesizing results from multiple sources
|
||||
- `/actions` — enables agentic mode where Copilot can take actions on your behalf, like modifying blocks or creating workflows
|
||||
- `/actions` — lets Copilot directly use your connected integrations as tools, like reading your Gmail, sending Slack messages, or querying your database—all outside the context of a workflow
|
||||
- `/search` — searches the web for relevant information
|
||||
- `/read` — reads and extracts content from a URL
|
||||
- `/scrape` — scrapes structured data from web pages
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { filterNewEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { filterNewEdges, filterValidEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -226,9 +226,12 @@ export function useCollaborativeWorkflow() {
|
||||
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
||||
const { edges } = payload
|
||||
if (Array.isArray(edges) && edges.length > 0) {
|
||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||
const blocks = useWorkflowStore.getState().blocks
|
||||
const currentEdges = useWorkflowStore.getState().edges
|
||||
const validEdges = filterValidEdges(edges, blocks)
|
||||
const newEdges = filterNewEdges(validEdges, currentEdges)
|
||||
if (newEdges.length > 0) {
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -1004,7 +1007,11 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
if (edges.length === 0) return false
|
||||
|
||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||
// Filter out invalid edges (e.g., edges targeting trigger blocks) and duplicates
|
||||
const blocks = useWorkflowStore.getState().blocks
|
||||
const currentEdges = useWorkflowStore.getState().edges
|
||||
const validEdges = filterValidEdges(edges, blocks)
|
||||
const newEdges = filterNewEdges(validEdges, currentEdges)
|
||||
if (newEdges.length === 0) return false
|
||||
|
||||
const operationId = crypto.randomUUID()
|
||||
@@ -1020,7 +1027,7 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
||||
|
||||
if (!options?.skipUndoRedo) {
|
||||
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||
@@ -1484,9 +1491,23 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
if (blocks.length === 0) return false
|
||||
|
||||
// Filter out invalid edges (e.g., edges targeting trigger blocks)
|
||||
// Combine existing blocks with new blocks for validation
|
||||
const existingBlocks = useWorkflowStore.getState().blocks
|
||||
const newBlocksMap = blocks.reduce(
|
||||
(acc, block) => {
|
||||
acc[block.id] = block
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, BlockState>
|
||||
)
|
||||
const allBlocks = { ...existingBlocks, ...newBlocksMap }
|
||||
const validEdges = filterValidEdges(edges, allBlocks)
|
||||
|
||||
logger.info('Batch adding blocks collaboratively', {
|
||||
blockCount: blocks.length,
|
||||
edgeCount: edges.length,
|
||||
edgeCount: validEdges.length,
|
||||
filteredEdges: edges.length - validEdges.length,
|
||||
})
|
||||
|
||||
const operationId = crypto.randomUUID()
|
||||
@@ -1496,16 +1517,18 @@ export function useCollaborativeWorkflow() {
|
||||
operation: {
|
||||
operation: BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS,
|
||||
target: OPERATION_TARGETS.BLOCKS,
|
||||
payload: { blocks, edges, loops, parallels, subBlockValues },
|
||||
payload: { blocks, edges: validEdges, loops, parallels, subBlockValues },
|
||||
},
|
||||
workflowId: activeWorkflowId || '',
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
useWorkflowStore.getState().batchAddBlocks(blocks, edges, subBlockValues)
|
||||
useWorkflowStore.getState().batchAddBlocks(blocks, validEdges, subBlockValues, {
|
||||
skipEdgeValidation: true,
|
||||
})
|
||||
|
||||
if (!options?.skipUndoRedo) {
|
||||
undoRedo.recordBatchAddBlocks(blocks, edges, subBlockValues)
|
||||
undoRedo.recordBatchAddBlocks(blocks, validEdges, subBlockValues)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { Edge } from 'reactflow'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import type {
|
||||
BlockState,
|
||||
@@ -17,6 +18,32 @@ import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
|
||||
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||
|
||||
/**
|
||||
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
||||
*/
|
||||
function isValidEdge(
|
||||
edge: Edge,
|
||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||
): boolean {
|
||||
const sourceBlock = blocks[edge.source]
|
||||
const targetBlock = blocks[edge.target]
|
||||
if (!sourceBlock || !targetBlock) return false
|
||||
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
||||
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
||||
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters edges to only include valid ones (target exists and is not a trigger block)
|
||||
*/
|
||||
export function filterValidEdges(
|
||||
edges: Edge[],
|
||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||
): Edge[] {
|
||||
return edges.filter((edge) => isValidEdge(edge, blocks))
|
||||
}
|
||||
|
||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||
return edgesToAdd.filter((edge) => {
|
||||
if (edge.source === edge.target) return false
|
||||
|
||||
@@ -4,13 +4,17 @@ import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import {
|
||||
filterNewEdges,
|
||||
filterValidEdges,
|
||||
getUniqueBlockName,
|
||||
mergeSubblockState,
|
||||
} from '@/stores/workflows/utils'
|
||||
import type {
|
||||
Position,
|
||||
SubBlockState,
|
||||
@@ -91,26 +95,6 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown {
|
||||
return null
|
||||
}
|
||||
|
||||
function isValidEdge(
|
||||
edge: Edge,
|
||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||
): boolean {
|
||||
const sourceBlock = blocks[edge.source]
|
||||
const targetBlock = blocks[edge.target]
|
||||
if (!sourceBlock || !targetBlock) return false
|
||||
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
||||
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
||||
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function filterValidEdges(
|
||||
edges: Edge[],
|
||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||
): Edge[] {
|
||||
return edges.filter((edge) => isValidEdge(edge, blocks))
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
@@ -356,7 +340,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
data?: Record<string, any>
|
||||
}>,
|
||||
edges?: Edge[],
|
||||
subBlockValues?: Record<string, Record<string, unknown>>
|
||||
subBlockValues?: Record<string, Record<string, unknown>>,
|
||||
options?: { skipEdgeValidation?: boolean }
|
||||
) => {
|
||||
const currentBlocks = get().blocks
|
||||
const currentEdges = get().edges
|
||||
@@ -381,7 +366,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
}
|
||||
|
||||
if (edges && edges.length > 0) {
|
||||
const validEdges = filterValidEdges(edges, newBlocks)
|
||||
// Skip validation if already validated by caller (e.g., collaborative layer)
|
||||
const validEdges = options?.skipEdgeValidation
|
||||
? edges
|
||||
: filterValidEdges(edges, newBlocks)
|
||||
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
||||
for (const edge of validEdges) {
|
||||
if (!existingEdgeIds.has(edge.id)) {
|
||||
@@ -516,11 +504,12 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
get().updateLastSaved()
|
||||
},
|
||||
|
||||
batchAddEdges: (edges: Edge[]) => {
|
||||
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => {
|
||||
const blocks = get().blocks
|
||||
const currentEdges = get().edges
|
||||
|
||||
const validEdges = filterValidEdges(edges, blocks)
|
||||
// Skip validation if already validated by caller (e.g., collaborative layer)
|
||||
const validEdges = options?.skipValidation ? edges : filterValidEdges(edges, blocks)
|
||||
const filtered = filterNewEdges(validEdges, currentEdges)
|
||||
const newEdges = [...currentEdges]
|
||||
|
||||
|
||||
@@ -203,12 +203,13 @@ export interface WorkflowActions {
|
||||
batchAddBlocks: (
|
||||
blocks: BlockState[],
|
||||
edges?: Edge[],
|
||||
subBlockValues?: Record<string, Record<string, unknown>>
|
||||
subBlockValues?: Record<string, Record<string, unknown>>,
|
||||
options?: { skipEdgeValidation?: boolean }
|
||||
) => void
|
||||
batchRemoveBlocks: (ids: string[]) => void
|
||||
batchToggleEnabled: (ids: string[]) => void
|
||||
batchToggleHandles: (ids: string[]) => void
|
||||
batchAddEdges: (edges: Edge[]) => void
|
||||
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => void
|
||||
batchRemoveEdges: (ids: string[]) => void
|
||||
clear: () => Partial<WorkflowState>
|
||||
updateLastSaved: () => void
|
||||
|
||||
Reference in New Issue
Block a user