Compare commits

..

9 Commits

Author SHA1 Message Date
Vikhyath Mondreti
12239a0803 consolidate literal gen 2026-01-26 01:33:06 -08:00
Vikhyath Mondreti
c264481361 fix tests 2026-01-26 01:14:07 -08:00
Vikhyath Mondreti
0e9bcdf1b2 remove template literal check 2026-01-26 01:10:30 -08:00
Vikhyath Mondreti
e20ec7ae3c fix python nan and inf resolution 2026-01-26 00:26:48 -08:00
Vikhyath Mondreti
87dcd53b98 case insensitive lookup 2026-01-26 00:23:33 -08:00
Vikhyath Mondreti
8aabf06f62 remove hacky fallback 2026-01-26 00:09:59 -08:00
Vikhyath Mondreti
b45fc62e7b fix(codegen): function prologue resolution edge cases 2026-01-26 00:03:15 -08:00
Waleed
80f00479a3 improvement(docs): added images and videos to quick references (#3004)
* improvement(docs): added images and videos to quick references

* moved mp4s to blob, completed quick reference guide
2026-01-25 23:31:40 -08:00
Vikhyath Mondreti
c140e90559 fix(multi-trigger): resolution paths for triggers (#3002)
* fix(multi-trigger): resolution paths for triggers

* fix trigger input format version

* fix output condition logic

* update type guard:

* fix
2026-01-25 23:20:42 -08:00
40 changed files with 464 additions and 162 deletions

View File

@@ -0,0 +1,40 @@
'use client'
import { getAssetUrl } from '@/lib/utils'
interface ActionImageProps {
src: string
alt: string
}
interface ActionVideoProps {
src: string
alt: string
}
export function ActionImage({ src, alt }: ActionImageProps) {
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
return (
<img
src={resolvedSrc}
alt={alt}
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
/>
)
}
export function ActionVideo({ src, alt }: ActionVideoProps) {
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
return (
<video
src={resolvedSrc}
autoPlay
loop
muted
playsInline
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
/>
)
}

View File

@@ -4,6 +4,7 @@ description: Essential actions for navigating and using the Sim workflow editor
--- ---
import { Callout } from 'fumadocs-ui/components/callout' import { Callout } from 'fumadocs-ui/components/callout'
import { ActionImage, ActionVideo } from '@/components/ui/action-media'
A quick lookup for everyday actions in the Sim workflow editor. For keyboard shortcuts, see [Keyboard Shortcuts](/keyboard-shortcuts). A quick lookup for everyday actions in the Sim workflow editor. For keyboard shortcuts, see [Keyboard Shortcuts](/keyboard-shortcuts).
@@ -13,124 +14,362 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
## Workspaces ## Workspaces
| Action | How | <table>
|--------|-----| <thead>
| Create a workspace | Click workspace dropdown in sidebar → **New Workspace** | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Rename a workspace | Workspace settings → Edit name | </thead>
| Switch workspaces | Click workspace dropdown in sidebar → Select workspace | <tbody>
| Invite team members | Workspace settings → **Team** → **Invite** | <tr>
<td>Create a workspace</td>
<td>Click workspace dropdown → **New Workspace**</td>
<td><ActionVideo src="/static/quick-reference/create-workspace.mp4" alt="Create workspace" /></td>
</tr>
<tr>
<td>Switch workspaces</td>
<td>Click workspace dropdown → Select workspace</td>
<td><ActionVideo src="/static/quick-reference/switch-workspace.mp4" alt="Switch workspaces" /></td>
</tr>
<tr>
<td>Invite team members</td>
<td>Sidebar → **Invite**</td>
<td><ActionVideo src="/static/quick-reference/invite.mp4" alt="Invite team members" /></td>
</tr>
<tr>
<td>Rename a workspace</td>
<td>Right-click workspace → **Rename**</td>
<td rowSpan={4}><ActionImage src="/static/quick-reference/workspace-context-menu.png" alt="Workspace context menu" /></td>
</tr>
<tr>
<td>Duplicate a workspace</td>
<td>Right-click workspace → **Duplicate**</td>
</tr>
<tr>
<td>Export a workspace</td>
<td>Right-click workspace → **Export**</td>
</tr>
<tr>
<td>Delete a workspace</td>
<td>Right-click workspace → **Delete**</td>
</tr>
</tbody>
</table>
## Workflows ## Workflows
| Action | How | <table>
|--------|-----| <thead>
| Create a workflow | Click **New Workflow** button or `Mod+Shift+A` | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Rename a workflow | Double-click workflow name in sidebar, or right-click → **Rename** | </thead>
| Duplicate a workflow | Right-click workflow → **Duplicate** | <tbody>
| Reorder workflows | Drag workflow up/down in the sidebar list | <tr>
| Import a workflow | Sidebar menu → **Import** → Select file | <td>Create a workflow</td>
| Create a folder | Right-click in sidebar → **New Folder** | <td>Click **+** button in sidebar</td>
| Rename a folder | Right-click folder → **Rename** | <td><ActionImage src="/static/quick-reference/create-workflow.png" alt="Create workflow" /></td>
| Delete a folder | Right-click folder → **Delete** | </tr>
| Collapse/expand folder | Click folder arrow, or double-click folder | <tr>
| Move workflow to folder | Drag workflow onto folder in sidebar | <td>Reorder / move workflows</td>
| Delete a workflow | Right-click workflow → **Delete** | <td>Drag workflow up/down or onto a folder</td>
| Export a workflow | Right-click workflow → **Export** | <td><ActionVideo src="/static/quick-reference/reordering.mp4" alt="Reorder workflows" /></td>
| Assign workflow color | Right-click workflow → **Change Color** | </tr>
| Multi-select workflows | `Mod+Click` or `Shift+Click` workflows in sidebar | <tr>
| Open in new tab | Right-click workflow → **Open in New Tab** | <td>Import a workflow</td>
<td>Click import button in sidebar → Select file</td>
<td><ActionImage src="/static/quick-reference/import-workflow.png" alt="Import workflow" /></td>
</tr>
<tr>
<td>Multi-select workflows</td>
<td>`Mod+Click` or `Shift+Click` workflows in sidebar</td>
<td><ActionVideo src="/static/quick-reference/multiselect.mp4" alt="Multi-select workflows" /></td>
</tr>
<tr>
<td>Open in new tab</td>
<td>Right-click workflow → **Open in New Tab**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/workflow-context-menu.png" alt="Workflow context menu" /></td>
</tr>
<tr>
<td>Rename a workflow</td>
<td>Right-click workflow → **Rename**</td>
</tr>
<tr>
<td>Assign workflow color</td>
<td>Right-click workflow → **Change Color**</td>
</tr>
<tr>
<td>Duplicate a workflow</td>
<td>Right-click workflow → **Duplicate**</td>
</tr>
<tr>
<td>Export a workflow</td>
<td>Right-click workflow → **Export**</td>
</tr>
<tr>
<td>Delete a workflow</td>
<td>Right-click workflow → **Delete**</td>
</tr>
<tr>
<td>Rename a folder</td>
<td>Right-click folder → **Rename**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/folder-context-menu.png" alt="Folder context menu" /></td>
</tr>
<tr>
<td>Create workflow in folder</td>
<td>Right-click folder → **Create workflow**</td>
</tr>
<tr>
<td>Create folder in folder</td>
<td>Right-click folder → **Create folder**</td>
</tr>
<tr>
<td>Duplicate a folder</td>
<td>Right-click folder → **Duplicate**</td>
</tr>
<tr>
<td>Export a folder</td>
<td>Right-click folder → **Export**</td>
</tr>
<tr>
<td>Delete a folder</td>
<td>Right-click folder → **Delete**</td>
</tr>
</tbody>
</table>
## Blocks ## Blocks
| Action | How | <table>
|--------|-----| <thead>
| Add a block | Drag from Toolbar panel, or right-click canvas → **Add Block** | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Select a block | Click on the block | </thead>
| Multi-select blocks | `Mod+Click` additional blocks, or right-drag to draw selection box | <tbody>
| Move blocks | Drag selected block(s) to new position | <tr>
| Copy blocks | `Mod+C` with blocks selected | <td>Add a block</td>
| Paste blocks | `Mod+V` to paste copied blocks | <td>Drag from Toolbar panel, or right-click canvas → **Add Block**</td>
| Duplicate blocks | Right-click → **Duplicate** | <td><ActionVideo src="/static/quick-reference/add-block.mp4" alt="Add a block" /></td>
| Delete blocks | `Delete` or `Backspace` key, or right-click → **Delete** | </tr>
| Rename a block | Click block name in header, or edit in the Editor panel | <tr>
| Enable/Disable a block | Right-click → **Enable/Disable** | <td>Multi-select blocks</td>
| Toggle handle orientation | Right-click → **Toggle Handles** | <td>`Mod+Click` additional blocks, or shift-drag to draw selection box</td>
| Toggle trigger mode | Right-click trigger block → **Toggle Trigger Mode** | <td><ActionVideo src="/static/quick-reference/multiselect-blocks.mp4" alt="Multi-select blocks" /></td>
| Configure a block | Select block → use Editor panel on right | </tr>
<tr>
<td>Copy blocks</td>
<td>`Mod+C` with blocks selected</td>
<td rowSpan={2}><ActionVideo src="/static/quick-reference/copy-paste.mp4" alt="Copy and paste blocks" /></td>
</tr>
<tr>
<td>Paste blocks</td>
<td>`Mod+V` to paste copied blocks</td>
</tr>
<tr>
<td>Duplicate blocks</td>
<td>Right-click → **Duplicate**</td>
<td><ActionVideo src="/static/quick-reference/duplicate-block.mp4" alt="Duplicate blocks" /></td>
</tr>
<tr>
<td>Delete blocks</td>
<td>`Delete` or `Backspace` key, or right-click → **Delete**</td>
<td><ActionImage src="/static/quick-reference/delete-block.png" alt="Delete block" /></td>
</tr>
<tr>
<td>Rename a block</td>
<td>Click block name in header, or edit in the Editor panel</td>
<td><ActionVideo src="/static/quick-reference/rename-block.mp4" alt="Rename a block" /></td>
</tr>
<tr>
<td>Enable/Disable a block</td>
<td>Right-click → **Enable/Disable**</td>
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
</tr>
<tr>
<td>Toggle handle orientation</td>
<td>Right-click → **Toggle Handles**</td>
<td><ActionVideo src="/static/quick-reference/toggle-handles.mp4" alt="Toggle handle orientation" /></td>
</tr>
<tr>
<td>Configure a block</td>
<td>Select block → use Editor panel on right</td>
<td><ActionVideo src="/static/quick-reference/configure-block.mp4" alt="Configure a block" /></td>
</tr>
</tbody>
</table>
## Connections ## Connections
| Action | How | <table>
|--------|-----| <thead>
| Create a connection | Drag from output handle to input handle | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Delete a connection | Click edge to select → `Delete` key | </thead>
| Use output in another block | Drag connection tag into input field | <tbody>
<tr>
## Canvas Navigation <td>Create a connection</td>
<td>Drag from output handle to input handle</td>
| Action | How | <td><ActionVideo src="/static/quick-reference/connect-blocks.mp4" alt="Connect blocks" /></td>
|--------|-----| </tr>
| Pan/move canvas | Left-drag on empty space, or scroll/trackpad | <tr>
| Zoom in/out | Scroll wheel or pinch gesture | <td>Delete a connection</td>
| Auto-layout | `Shift+L` | <td>Click edge to select `Delete` key</td>
| Draw selection box | Right-drag on empty canvas area | <td><ActionVideo src="/static/quick-reference/delete-connection.mp4" alt="Delete connection" /></td>
</tr>
<tr>
<td>Use output in another block</td>
<td>Drag connection tag into input field</td>
<td><ActionVideo src="/static/quick-reference/connection-tag.mp4" alt="Use connection tag" /></td>
</tr>
</tbody>
</table>
## Panels & Views ## Panels & Views
| Action | How | <table>
|--------|-----| <thead>
| Open Copilot tab | Press `C` or click Copilot tab | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Open Toolbar tab | Press `T` or click Toolbar tab | </thead>
| Open Editor tab | Press `E` or click Editor tab | <tbody>
| Search toolbar | `Mod+F` | <tr>
| Toggle advanced mode | Click toggle button on input fields | <td>Search toolbar</td>
| Resize panels | Drag panel edge | <td>`Mod+F`</td>
| Collapse/expand sidebar | Click collapse button on sidebar | <td><ActionVideo src="/static/quick-reference/search-toolbar.mp4" alt="Search toolbar" /></td>
</tr>
<tr>
<td>Search everything</td>
<td>`Mod+K`</td>
<td><ActionImage src="/static/quick-reference/search-everything.png" alt="Search everything" /></td>
</tr>
<tr>
<td>Toggle manual mode</td>
<td>Click toggle button to switch between manual and selector</td>
<td><ActionImage src="/static/quick-reference/toggle-manual-mode.png" alt="Toggle manual mode" /></td>
</tr>
<tr>
<td>Collapse/expand sidebar</td>
<td>Click collapse button on sidebar</td>
<td><ActionVideo src="/static/quick-reference/collapse-sidebar.mp4" alt="Collapse sidebar" /></td>
</tr>
</tbody>
</table>
## Running & Testing ## Running & Testing
| Action | How | <table>
|--------|-----| <thead>
| Run workflow | Click Play button or `Mod+Enter` | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Stop workflow | Click Stop button or `Mod+Enter` while running | </thead>
| Test with chat | Use Chat panel on the right side | <tbody>
| Select output to view | Click dropdown in Chat panel → Select block output | <tr>
| Clear chat history | Click clear button in Chat panel | <td>Run workflow</td>
| View execution logs | Open terminal panel at bottom, or `Mod+L` | <td>Click Run Workflow button or `Mod+Enter`</td>
| Filter logs by block | Click block filter in terminal | <td><ActionImage src="/static/quick-reference/run-workflow.png" alt="Run workflow" /></td>
| Filter logs by status | Click status filter in terminal | </tr>
| Search logs | Use search field in terminal | <tr>
| Copy log entry | Right-click log entry → **Copy** | <td>Stop workflow</td>
| Clear terminal | `Mod+D` | <td>Click Stop button or `Mod+Enter` while running</td>
<td><ActionImage src="/static/quick-reference/stop-workflow.png" alt="Stop workflow" /></td>
</tr>
<tr>
<td>Test with chat</td>
<td>Use Chat panel on the right side</td>
<td><ActionImage src="/static/quick-reference/test-chat.png" alt="Test with chat" /></td>
</tr>
<tr>
<td>Select output to view</td>
<td>Click dropdown in Chat panel → Select block output</td>
<td><ActionImage src="/static/quick-reference/output-select.png" alt="Select output to view" /></td>
</tr>
<tr>
<td>Clear chat history</td>
<td>Click clear button in Chat panel</td>
<td><ActionImage src="/static/quick-reference/clear-chat.png" alt="Clear chat history" /></td>
</tr>
<tr>
<td>View execution logs</td>
<td>Open terminal panel at bottom, or `Mod+L`</td>
<td><ActionImage src="/static/quick-reference/terminal.png" alt="Execution logs terminal" /></td>
</tr>
<tr>
<td>Filter logs by block or status</td>
<td>Click block filter in terminal or right-click log entry → **Filter by Block** or **Filter by Status**</td>
<td><ActionImage src="/static/quick-reference/filter-block.png" alt="Filter logs by block" /></td>
</tr>
<tr>
<td>Search logs</td>
<td>Use search field in terminal or right-click log entry → **Search**</td>
<td><ActionImage src="/static/quick-reference/terminal-search.png" alt="Search logs" /></td>
</tr>
<tr>
<td>Copy log entry</td>
<td>Clipboard Icon or Right-click log entry → **Copy**</td>
<td><ActionImage src="/static/quick-reference/copy-log.png" alt="Copy log entry" /></td>
</tr>
<tr>
<td>Clear terminal</td>
<td>Trash icon or `Mod+D`</td>
<td><ActionImage src="/static/quick-reference/clear-terminal.png" alt="Clear terminal" /></td>
</tr>
</tbody>
</table>
## Deployment ## Deployment
| Action | How | <table>
|--------|-----| <thead>
| Deploy a workflow | Click **Deploy** button in Deploy tab | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Update deployment | Click **Update** when changes are detected | </thead>
| View deployment status | Check status indicator (Live/Update/Deploy) in Deploy tab | <tbody>
| Revert deployment | Access previous versions in Deploy tab | <tr>
| Copy webhook URL | Deploy tab → Copy webhook URL | <td>Deploy a workflow</td>
| Copy API endpoint | Deploy tab → Copy API endpoint URL | <td>Click **Deploy** button in panel</td>
| Set up a schedule | Add Schedule trigger block → Configure interval | <td><ActionImage src="/static/quick-reference/deploy.png" alt="Deploy workflow" /></td>
</tr>
<tr>
<td>Update deployment</td>
<td>Click **Update** when changes are detected</td>
<td><ActionImage src="/static/quick-reference/update-deployment.png" alt="Update deployment" /></td>
</tr>
<tr>
<td>View deployment status</td>
<td>Check status indicator (Live/Update/Deploy) in Deploy tab</td>
<td><ActionImage src="/static/quick-reference/view-deployment.png" alt="View deployment status" /></td>
</tr>
<tr>
<td>Revert deployment</td>
<td>Access previous versions in Deploy tab → **Promote to live**</td>
<td><ActionImage src="/static/quick-reference/promote-deployment.png" alt="Promote deployment to live" /></td>
</tr>
<tr>
<td>Copy API endpoint</td>
<td>Deploy tab → Copy API endpoint URL</td>
<td><ActionImage src="/static/quick-reference/copy-api.png" alt="Copy API endpoint" /></td>
</tr>
</tbody>
</table>
## Variables ## Variables
| Action | How | <table>
|--------|-----| <thead>
| Add workflow variable | Variables tab → **Add Variable** | <tr><th>Action</th><th>How</th><th>Preview</th></tr>
| Edit workflow variable | Variables tab → Click variable to edit | </thead>
| Delete workflow variable | Variables tab → Click delete icon on variable | <tbody>
| Add environment variable | Settings → **Environment Variables** → **Add** | <tr>
| Reference a variable | Use `{{variableName}}` syntax in block inputs | <td>Add / Edit / Delete workflow variable</td>
<td>Panel -> Variables -> **Add Variable**, click to edit, or delete icon</td>
## Credentials <td><ActionImage src="/static/quick-reference/variables.png" alt="Variables panel" /></td>
</tr>
| Action | How | <tr>
|--------|-----| <td>Add environment variable</td>
| Add API key | Block credential field → **Add Credential** → Enter API key | <td>Settings → **Environment Variables** → **Add**</td>
| Connect OAuth account | Block credential field → **Connect** → Authorize with provider | <td><ActionImage src="/static/quick-reference/add-env-variable.png" alt="Add environment variable" /></td>
| Manage credentials | Settings → **Credentials** | </tr>
| Remove credential | Settings → **Credentials** → Delete credential | <tr>
<td>Reference a workflow variable</td>
<td>Use `<blockName.itemName>` syntax in block inputs</td>
<td><ActionImage src="/static/quick-reference/variable-reference.png" alt="Reference workflow variable" /></td>
</tr>
<tr>
<td>Reference an environment variable</td>
<td>Use `&#123;&#123;ENV_VAR&#125;&#125;` syntax in block inputs</td>
<td><ActionImage src="/static/quick-reference/env-variable-reference.png" alt="Reference environment variable" /></td>
</tr>
</tbody>
</table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -8,6 +8,7 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants' import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference' import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import { import {
createEnvVarPattern, createEnvVarPattern,
createWorkflowVariablePattern, createWorkflowVariablePattern,
@@ -387,7 +388,12 @@ function resolveWorkflowVariables(
if (type === 'number') { if (type === 'number') {
variableValue = Number(variableValue) variableValue = Number(variableValue)
} else if (type === 'boolean') { } else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true if (typeof variableValue === 'boolean') {
// Already a boolean, keep as-is
} else {
const normalized = String(variableValue).toLowerCase().trim()
variableValue = normalized === 'true'
}
} else if (type === 'json' && typeof variableValue === 'string') { } else if (type === 'json' && typeof variableValue === 'string') {
try { try {
variableValue = JSON.parse(variableValue) variableValue = JSON.parse(variableValue)
@@ -687,11 +693,7 @@ export async function POST(req: NextRequest) {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++ prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) { for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) { prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologueLineCount++ prologueLineCount++
} }
@@ -762,11 +764,7 @@ export async function POST(req: NextRequest) {
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n` prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++ prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) { for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) { prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologueLineCount++ prologueLineCount++
} }
const wrapped = [ const wrapped = [

View File

@@ -0,0 +1,48 @@
/**
* Formats a JavaScript/TypeScript value as a code literal for the target language.
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
*
* @param value - The value to format
* @param language - Target language ('javascript' or 'python')
* @returns A string literal representation valid in the target language
*
* @example
* formatLiteralForCode(null, 'python') // => 'None'
* formatLiteralForCode(true, 'python') // => 'True'
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
*/
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
const isPython = language === 'python'
if (value === undefined) {
return isPython ? 'None' : 'undefined'
}
if (value === null) {
return isPython ? 'None' : 'null'
}
if (typeof value === 'boolean') {
return isPython ? (value ? 'True' : 'False') : String(value)
}
if (typeof value === 'number') {
if (Number.isNaN(value)) {
return isPython ? "float('nan')" : 'NaN'
}
if (value === Number.POSITIVE_INFINITY) {
return isPython ? "float('inf')" : 'Infinity'
}
if (value === Number.NEGATIVE_INFINITY) {
return isPython ? "float('-inf')" : '-Infinity'
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
if (isPython) {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
}
return JSON.stringify(value)
}

View File

@@ -157,7 +157,14 @@ export class VariableResolver {
let replacementError: Error | null = null let replacementError: Error | null = null
// Use generic utility for smart variable reference replacement const blockType = block?.metadata?.id
const language =
blockType === BlockType.FUNCTION
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
| string
| undefined)
: undefined
let result = replaceValidReferences(template, (match) => { let result = replaceValidReferences(template, (match) => {
if (replacementError) return match if (replacementError) return match
@@ -167,14 +174,7 @@ export class VariableResolver {
return match return match
} }
const blockType = block?.metadata?.id return this.blockResolver.formatValueForBlock(resolved, blockType, language)
const isInTemplateLiteral =
blockType === BlockType.FUNCTION &&
template.includes('${') &&
template.includes('}') &&
template.includes('`')
return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
} catch (error) { } catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error)) replacementError = error instanceof Error ? error : new Error(String(error))
return match return match

View File

@@ -257,15 +257,9 @@ describe('BlockResolver', () => {
expect(result).toBe('"hello"') expect(result).toBe('"hello"')
}) })
it.concurrent('should format string for function block in template literal', () => { it.concurrent('should format object for function block', () => {
const resolver = new BlockResolver(createTestWorkflow()) const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock('hello', 'function', true) const result = resolver.formatValueForBlock({ a: 1 }, 'function')
expect(result).toBe('hello')
})
it.concurrent('should format object for function block in template literal', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
expect(result).toBe('{"a":1}') expect(result).toBe('{"a":1}')
}) })

View File

@@ -10,6 +10,7 @@ import {
type OutputSchema, type OutputSchema,
resolveBlockReference, resolveBlockReference,
} from '@/executor/utils/block-reference' } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import { import {
navigatePath, navigatePath,
type ResolutionContext, type ResolutionContext,
@@ -159,17 +160,13 @@ export class BlockResolver implements Resolver {
return this.nameToBlockId.get(normalizeName(name)) return this.nameToBlockId.get(normalizeName(name))
} }
public formatValueForBlock( public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
value: any,
blockType: string | undefined,
isInTemplateLiteral = false
): string {
if (blockType === 'condition') { if (blockType === 'condition') {
return this.stringifyForCondition(value) return this.stringifyForCondition(value)
} }
if (blockType === 'function') { if (blockType === 'function') {
return this.formatValueForCodeContext(value, isInTemplateLiteral) return this.formatValueForCodeContext(value, language)
} }
if (blockType === 'response') { if (blockType === 'response') {
@@ -210,29 +207,7 @@ export class BlockResolver implements Resolver {
return String(value) return String(value)
} }
private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string { private formatValueForCodeContext(value: any, language?: string): string {
if (isInTemplateLiteral) { return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
if (typeof value === 'string') {
return value
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
if (value === undefined) {
return 'undefined'
}
if (value === null) {
return 'null'
}
return String(value)
} }
} }

View File

@@ -30,7 +30,10 @@ export function navigatePath(obj: any, path: string[]): any {
const arrayMatch = part.match(/^([^[]+)(\[.+)$/) const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
if (arrayMatch) { if (arrayMatch) {
const [, prop, bracketsPart] = arrayMatch const [, prop, bracketsPart] = arrayMatch
current = current[prop] current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[prop]
: undefined
if (current === undefined || current === null) { if (current === undefined || current === null) {
return undefined return undefined
} }
@@ -49,7 +52,10 @@ export function navigatePath(obj: any, path: string[]): any {
const index = Number.parseInt(part, 10) const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined current = Array.isArray(current) ? current[index] : undefined
} else { } else {
current = current[part] current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[part]
: undefined
} }
} }
return current return current

View File

@@ -132,6 +132,8 @@ async function executeCode(request) {
for (const [key, value] of Object.entries(contextVariables)) { for (const [key, value] of Object.entries(contextVariables)) {
if (value === undefined) { if (value === undefined) {
await jail.set(key, undefined) await jail.set(key, undefined)
} else if (value === null) {
await jail.set(key, null)
} else { } else {
await jail.set(key, new ivm.ExternalCopy(value).copyInto()) await jail.set(key, new ivm.ExternalCopy(value).copyInto())
} }

View File

@@ -26,7 +26,7 @@ describe('VariableManager', () => {
it.concurrent('should handle boolean type variables', () => { it.concurrent('should handle boolean type variables', () => {
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false) expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true) expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true) expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
}) })

View File

@@ -61,7 +61,7 @@ export class VariableManager {
// Special case for 'anything else' in the test // Special case for 'anything else' in the test
if (unquoted === 'anything else') return true if (unquoted === 'anything else') return true
const normalized = String(unquoted).toLowerCase().trim() const normalized = String(unquoted).toLowerCase().trim()
return normalized === 'true' || normalized === '1' return normalized === 'true'
} }
case 'object': case 'object':