From 55117a4bc14f1f840a2c5ac3f33b3c45c35e7d88 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 6 Jan 2026 23:12:51 -0800 Subject: [PATCH 1/3] fix(agent-tool): fix workflow tool in agent to respect user-provided params, added badge for deployment status --- .../components/tool-input/tool-input.tsx | 90 ++++++- apps/sim/tools/workflow/executor.test.ts | 230 ++++++++++++++++++ apps/sim/tools/workflow/executor.ts | 20 +- apps/sim/tools/workflow/types.ts | 3 +- 4 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 apps/sim/tools/workflow/executor.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index e99a1eb04d..56b28dda0f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -50,6 +50,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { useChildDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment' import { getAllBlocks } from '@/blocks' import { type CustomTool as CustomToolDefinition, @@ -582,6 +583,8 @@ function WorkflowSelectorSyncWrapper({ onChange={onChange} placeholder={uiComponent.placeholder || 'Select workflow'} disabled={disabled || isLoading} + searchable + searchPlaceholder='Search workflows...' /> ) } @@ -752,6 +755,86 @@ function CodeEditorSyncWrapper({ ) } +/** + * Badge component showing deployment status for workflow tools + */ +function WorkflowToolDeployBadge({ + workflowId, + onDeploySuccess, +}: { + workflowId: string + onDeploySuccess?: () => void +}) { + const { isDeployed, needsRedeploy, isLoading, refetch } = useChildDeployment(workflowId) + const [isDeploying, setIsDeploying] = useState(false) + + const deployWorkflow = useCallback(async () => { + if (isDeploying || !workflowId) return + + try { + setIsDeploying(true) + const response = await fetch(`/api/workflows/${workflowId}/deploy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + deployChatEnabled: false, + }), + }) + + if (response.ok) { + refetch() + onDeploySuccess?.() + } else { + logger.error('Failed to deploy workflow') + } + } catch (error) { + logger.error('Error deploying workflow:', error) + } finally { + setIsDeploying(false) + } + }, [isDeploying, workflowId, refetch, onDeploySuccess]) + + // Don't show badge if loading or workflow is deployed and doesn't need redeploy + if (isLoading || (isDeployed && !needsRedeploy)) { + return null + } + + if (typeof isDeployed !== 'boolean') { + return null + } + + if (isDeployed && !needsRedeploy) { + return null + } + + return ( + + + { + e.stopPropagation() + e.preventDefault() + if (!isDeploying) { + deployWorkflow() + } + }} + > + {isDeploying ? 'Deploying...' : !isDeployed ? 'undeployed' : 'redeploy'} + + + + {!isDeployed ? 'Click to deploy' : 'Click to redeploy'} + + + ) +} + /** * Set of built-in tool types that are core platform tools. * @@ -2219,10 +2302,15 @@ export function ToolInput({ {getIssueBadgeLabel(issue)} - {issue.message}: click to open settings + + {issue.message}: click to open settings + ) })()} + {tool.type === 'workflow' && tool.params?.workflowId && ( + + )}
{supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && ( diff --git a/apps/sim/tools/workflow/executor.test.ts b/apps/sim/tools/workflow/executor.test.ts new file mode 100644 index 0000000000..8ad3bca43e --- /dev/null +++ b/apps/sim/tools/workflow/executor.test.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from 'vitest' +import { workflowExecutorTool } from '@/tools/workflow/executor' + +describe('workflowExecutorTool', () => { + describe('request.body', () => { + const buildBody = workflowExecutorTool.request.body! + + it.concurrent('should pass through object inputMapping unchanged (LLM-provided args)', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: { firstName: 'John', lastName: 'Doe', age: 30 }, + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { firstName: 'John', lastName: 'Doe', age: 30 }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should parse JSON string inputMapping (UI-provided via tool-input)', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{"firstName": "John", "lastName": "Doe"}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { firstName: 'John', lastName: 'Doe' }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should handle nested objects in JSON string inputMapping', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{"user": {"name": "John", "email": "john@example.com"}, "count": 5}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { user: { name: 'John', email: 'john@example.com' }, count: 5 }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should handle arrays in JSON string inputMapping', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{"tags": ["a", "b", "c"], "ids": [1, 2, 3]}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { tags: ['a', 'b', 'c'], ids: [1, 2, 3] }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should default to empty object when inputMapping is undefined', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: undefined, + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should default to empty object when inputMapping is null', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: null as any, + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should fallback to empty object for invalid JSON string', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: 'not valid json {', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should fallback to empty object for empty string', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should handle empty object inputMapping', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: {}, + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should handle empty JSON object string', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: {}, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should preserve special characters in string values', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{"message": "Hello\\nWorld", "path": "C:\\\\Users"}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { message: 'Hello\nWorld', path: 'C:\\Users' }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should handle unicode characters in JSON string', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: '{"greeting": "こんにちは", "emoji": "👋"}', + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { greeting: 'こんにちは', emoji: '👋' }, + triggerType: 'api', + useDraftState: false, + }) + }) + + it.concurrent('should not modify object with string values that look like JSON', () => { + const params = { + workflowId: 'test-workflow-id', + inputMapping: { data: '{"nested": "json"}' }, + } + + const result = buildBody(params) + + expect(result).toEqual({ + input: { data: '{"nested": "json"}' }, + triggerType: 'api', + useDraftState: false, + }) + }) + }) + + describe('request.url', () => { + it.concurrent('should build correct URL with workflowId', () => { + const url = workflowExecutorTool.request.url as (params: any) => string + + expect(url({ workflowId: 'abc-123' })).toBe('/api/workflows/abc-123/execute') + expect(url({ workflowId: 'my-workflow' })).toBe('/api/workflows/my-workflow/execute') + }) + }) + + describe('tool metadata', () => { + it.concurrent('should have correct id', () => { + expect(workflowExecutorTool.id).toBe('workflow_executor') + }) + + it.concurrent('should have required workflowId param', () => { + expect(workflowExecutorTool.params.workflowId.required).toBe(true) + }) + + it.concurrent('should have optional inputMapping param', () => { + expect(workflowExecutorTool.params.inputMapping.required).toBe(false) + }) + + it.concurrent('should use POST method', () => { + expect(workflowExecutorTool.request.method).toBe('POST') + }) + }) +}) diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index a5c054dc49..036769eb71 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -33,11 +33,21 @@ export const workflowExecutorTool: ToolConfig< url: (params: WorkflowExecutorParams) => `/api/workflows/${params.workflowId}/execute`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), - body: (params: WorkflowExecutorParams) => ({ - input: params.inputMapping || {}, - triggerType: 'api', - useDraftState: false, - }), + body: (params: WorkflowExecutorParams) => { + let inputData = params.inputMapping || {} + if (typeof inputData === 'string') { + try { + inputData = JSON.parse(inputData) + } catch { + inputData = {} + } + } + return { + input: inputData, + triggerType: 'api', + useDraftState: false, + } + }, }, transformResponse: async (response: Response) => { const data = await response.json() diff --git a/apps/sim/tools/workflow/types.ts b/apps/sim/tools/workflow/types.ts index f86f8961e8..b0f4339d64 100644 --- a/apps/sim/tools/workflow/types.ts +++ b/apps/sim/tools/workflow/types.ts @@ -2,7 +2,8 @@ import type { ToolResponse } from '@/tools/types' export interface WorkflowExecutorParams { workflowId: string - inputMapping?: Record + /** Can be a JSON string (from tool-input UI) or an object (from LLM args) */ + inputMapping?: Record | string } export interface WorkflowExecutorResponse extends ToolResponse { From a04beba30c679d954edf578cc2de06a2ea232902 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 6 Jan 2026 23:19:12 -0800 Subject: [PATCH 2/3] ack PR comment --- .../sub-block/components/tool-input/tool-input.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 56b28dda0f..6823e303b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -796,7 +796,6 @@ function WorkflowToolDeployBadge({ } }, [isDeploying, workflowId, refetch, onDeploySuccess]) - // Don't show badge if loading or workflow is deployed and doesn't need redeploy if (isLoading || (isDeployed && !needsRedeploy)) { return null } @@ -805,10 +804,6 @@ function WorkflowToolDeployBadge({ return null } - if (isDeployed && !needsRedeploy) { - return null - } - return ( From 8e2058329dff3cd4b61d73685ff1464b0ac0565e Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 6 Jan 2026 23:22:44 -0800 Subject: [PATCH 3/3] updated gh stars --- apps/sim/app/(landing)/components/nav/nav.tsx | 2 +- apps/sim/app/chat/[identifier]/chat.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/(landing)/components/nav/nav.tsx b/apps/sim/app/(landing)/components/nav/nav.tsx index d8ae4b9065..0478a69a12 100644 --- a/apps/sim/app/(landing)/components/nav/nav.tsx +++ b/apps/sim/app/(landing)/components/nav/nav.tsx @@ -20,7 +20,7 @@ interface NavProps { } export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) { - const [githubStars, setGithubStars] = useState('24.4k') + const [githubStars, setGithubStars] = useState('25.1k') const [isHovered, setIsHovered] = useState(false) const [isLoginHovered, setIsLoginHovered] = useState(false) const router = useRouter() diff --git a/apps/sim/app/chat/[identifier]/chat.tsx b/apps/sim/app/chat/[identifier]/chat.tsx index 7c6c8f273c..0a39af6657 100644 --- a/apps/sim/app/chat/[identifier]/chat.tsx +++ b/apps/sim/app/chat/[identifier]/chat.tsx @@ -117,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { const [error, setError] = useState(null) const messagesEndRef = useRef(null) const messagesContainerRef = useRef(null) - const [starCount, setStarCount] = useState('24.4k') + const [starCount, setStarCount] = useState('25.1k') const [conversationId, setConversationId] = useState('') const [showScrollButton, setShowScrollButton] = useState(false)