Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions apps/docs/content/docs/en/blocks/router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
title: Router
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'

Expand Down Expand Up @@ -102,11 +101,18 @@ Input (Lead) → Router
└── [Self-serve] → Workflow (Automated Onboarding)
```

## Error Handling

When the Router cannot determine an appropriate route for the given context, it will route to the **error path** instead of arbitrarily selecting a route. This happens when:

- The context doesn't clearly match any of the defined route descriptions
- The AI determines that none of the available routes are appropriate

## Best Practices

- **Write clear route descriptions**: Each route description should clearly explain when that route should be selected. Be specific about the criteria.
- **Make routes mutually exclusive**: When possible, ensure route descriptions don't overlap to prevent ambiguous routing decisions.
- **Include an error/fallback route**: Add a catch-all route for unexpected inputs that don't match other routes.
- **Connect an error path**: Handle cases where no route matches by connecting an error handler for graceful fallback behavior.
- **Use descriptive route titles**: Route titles appear in the workflow canvas, so make them meaningful for readability.
- **Test with diverse inputs**: Ensure the Router handles various input types, edge cases, and unexpected content.
- **Monitor routing performance**: Review routing decisions regularly and refine route descriptions based on actual usage patterns.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,17 +654,20 @@ export function ConditionInput({
}

const removeBlock = (id: string) => {
if (isPreview || disabled || conditionalBlocks.length <= 2) return
if (isPreview || disabled) return
// Condition mode requires at least 2 blocks (if/else), router mode requires at least 1
const minBlocks = isRouterMode ? 1 : 2
if (conditionalBlocks.length <= minBlocks) return

// Remove any associated edges before removing the block
const handlePrefix = isRouterMode ? `router-${id}` : `condition-${id}`
const edgeIdsToRemove = edges
.filter((edge) => edge.sourceHandle?.startsWith(`condition-${id}`))
.filter((edge) => edge.sourceHandle?.startsWith(handlePrefix))
.map((edge) => edge.id)
if (edgeIdsToRemove.length > 0) {
batchRemoveEdges(edgeIdsToRemove)
}

if (conditionalBlocks.length === 1) return
shouldPersistRef.current = true
setConditionalBlocks((blocks) => updateBlockTitles(blocks.filter((block) => block.id !== id)))

Expand Down Expand Up @@ -816,7 +819,9 @@ export function ConditionInput({
<Button
variant='ghost'
onClick={() => removeBlock(block.id)}
disabled={isPreview || disabled || conditionalBlocks.length === 1}
disabled={
isPreview || disabled || conditionalBlocks.length <= (isRouterMode ? 1 : 2)
}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
>
<Trash className='h-[14px] w-[14px]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
return parsed.map((item: unknown, index: number) => {
const routeItem = item as { id?: string; value?: string }
return {
id: routeItem?.id ?? `${id}-route-${index}`,
// Use stable ID format that matches ConditionInput's generateStableId
id: routeItem?.id ?? `${id}-route${index + 1}`,
value: routeItem?.value ?? '',
}
})
Expand All @@ -873,7 +874,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
logger.warn('Failed to parse router routes value', { error, blockId: id })
}

return [{ id: `${id}-route-route1`, value: '' }]
// Fallback must match ConditionInput's default: generateStableId(blockId, 'route1') = `${blockId}-route1`
return [{ id: `${id}-route1`, value: '' }]
}, [type, subBlockState, id])

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,14 @@ const WorkflowContent = React.memo(() => {
const handleId = conditionHandles[0].getAttribute('data-handleid')
if (handleId) return handleId
}
} else if (block.type === 'router_v2') {
const routerHandles = document.querySelectorAll(
`[data-nodeid^="${block.id}"][data-handleid^="router-"]`
)
if (routerHandles.length > 0) {
const handleId = routerHandles[0].getAttribute('data-handleid')
if (handleId) return handleId
}
} else if (block.type === 'loop') {
return 'loop-end-source'
} else if (block.type === 'parallel') {
Expand Down
23 changes: 12 additions & 11 deletions apps/sim/blocks/blocks/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,25 +115,26 @@ Description: ${route.value || 'No description provided'}
)
.join('\n')

return `You are an intelligent routing agent. Your task is to analyze the provided context and select the most appropriate route from the available options.
return `You are a DETERMINISTIC routing agent. You MUST select exactly ONE option.

Available Routes:
${routesInfo}

Context to analyze:
Context to route:
${context}

Instructions:
1. Carefully analyze the context against each route's description
2. Select the route that best matches the context's intent and requirements
3. Consider the semantic meaning, not just keyword matching
4. If multiple routes could match, choose the most specific one
ROUTING RULES:
1. ALWAYS prefer selecting a route over NO_MATCH
2. Pick the route whose description BEST matches the context, even if it's not a perfect match
3. If the context is even partially related to a route's description, select that route
4. ONLY output NO_MATCH if the context is completely unrelated to ALL route descriptions

Response Format:
Return ONLY the route ID as a single string, no punctuation, no explanation.
Example: "route-abc123"
OUTPUT FORMAT:
- Output EXACTLY one route ID (copied exactly as shown above) OR "NO_MATCH"
- No explanation, no punctuation, no additional text
- Just the route ID or NO_MATCH

Remember: Your response must be ONLY the route ID - no additional text, formatting, or explanation.`
Your response:`
}

/**
Expand Down
16 changes: 13 additions & 3 deletions apps/sim/executor/handlers/router/router-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,24 @@ export class RouterBlockHandler implements BlockHandler {
const result = await response.json()

const chosenRouteId = result.content.trim()

if (chosenRouteId === 'NO_MATCH' || chosenRouteId.toUpperCase() === 'NO_MATCH') {
logger.info('Router determined no route matches the context, routing to error path')
throw new Error('Router could not determine a matching route for the given context')
}

const chosenRoute = routes.find((r) => r.id === chosenRouteId)

// Throw error if LLM returns invalid route ID - this routes through error path
if (!chosenRoute) {
const availableRoutes = routes.map((r) => ({ id: r.id, title: r.title }))
logger.error(
`Invalid routing decision. Response content: "${result.content}", available routes:`,
routes.map((r) => ({ id: r.id, title: r.title }))
`Invalid routing decision. Response content: "${result.content}". Available routes:`,
availableRoutes
)
throw new Error(
`Router could not determine a valid route. LLM response: "${result.content}". Available route IDs: ${routes.map((r) => r.id).join(', ')}`
)
throw new Error(`Invalid routing decision: ${chosenRouteId}`)
}

// Find the target block connected to this route's handle
Expand Down
1 change: 0 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
Expand Down