Skip to content

Commit 02229f0

Browse files
authored
fix(agent-tool): fix workflow tool in agent to respect user-provided params, added badge for deployment status (#2705)
* fix(agent-tool): fix workflow tool in agent to respect user-provided params, added badge for deployment status * ack PR comment * updated gh stars
1 parent a2451ef commit 02229f0

File tree

6 files changed

+333
-9
lines changed

6 files changed

+333
-9
lines changed

apps/sim/app/(landing)/components/nav/nav.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface NavProps {
2020
}
2121

2222
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
23-
const [githubStars, setGithubStars] = useState('24.4k')
23+
const [githubStars, setGithubStars] = useState('25.1k')
2424
const [isHovered, setIsHovered] = useState(false)
2525
const [isLoginHovered, setIsLoginHovered] = useState(false)
2626
const router = useRouter()

apps/sim/app/chat/[identifier]/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
117117
const [error, setError] = useState<string | null>(null)
118118
const messagesEndRef = useRef<HTMLDivElement>(null)
119119
const messagesContainerRef = useRef<HTMLDivElement>(null)
120-
const [starCount, setStarCount] = useState('24.4k')
120+
const [starCount, setStarCount] = useState('25.1k')
121121
const [conversationId, setConversationId] = useState('')
122122

123123
const [showScrollButton, setShowScrollButton] = useState(false)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
5151
import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector'
5252
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
53+
import { useChildDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment'
5354
import { getAllBlocks } from '@/blocks'
5455
import {
5556
type CustomTool as CustomToolDefinition,
@@ -582,6 +583,8 @@ function WorkflowSelectorSyncWrapper({
582583
onChange={onChange}
583584
placeholder={uiComponent.placeholder || 'Select workflow'}
584585
disabled={disabled || isLoading}
586+
searchable
587+
searchPlaceholder='Search workflows...'
585588
/>
586589
)
587590
}
@@ -752,6 +755,81 @@ function CodeEditorSyncWrapper({
752755
)
753756
}
754757

758+
/**
759+
* Badge component showing deployment status for workflow tools
760+
*/
761+
function WorkflowToolDeployBadge({
762+
workflowId,
763+
onDeploySuccess,
764+
}: {
765+
workflowId: string
766+
onDeploySuccess?: () => void
767+
}) {
768+
const { isDeployed, needsRedeploy, isLoading, refetch } = useChildDeployment(workflowId)
769+
const [isDeploying, setIsDeploying] = useState(false)
770+
771+
const deployWorkflow = useCallback(async () => {
772+
if (isDeploying || !workflowId) return
773+
774+
try {
775+
setIsDeploying(true)
776+
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
777+
method: 'POST',
778+
headers: {
779+
'Content-Type': 'application/json',
780+
},
781+
body: JSON.stringify({
782+
deployChatEnabled: false,
783+
}),
784+
})
785+
786+
if (response.ok) {
787+
refetch()
788+
onDeploySuccess?.()
789+
} else {
790+
logger.error('Failed to deploy workflow')
791+
}
792+
} catch (error) {
793+
logger.error('Error deploying workflow:', error)
794+
} finally {
795+
setIsDeploying(false)
796+
}
797+
}, [isDeploying, workflowId, refetch, onDeploySuccess])
798+
799+
if (isLoading || (isDeployed && !needsRedeploy)) {
800+
return null
801+
}
802+
803+
if (typeof isDeployed !== 'boolean') {
804+
return null
805+
}
806+
807+
return (
808+
<Tooltip.Root>
809+
<Tooltip.Trigger asChild>
810+
<Badge
811+
variant={!isDeployed ? 'red' : 'amber'}
812+
className='cursor-pointer'
813+
size='sm'
814+
dot
815+
onClick={(e: React.MouseEvent) => {
816+
e.stopPropagation()
817+
e.preventDefault()
818+
if (!isDeploying) {
819+
deployWorkflow()
820+
}
821+
}}
822+
>
823+
{isDeploying ? 'Deploying...' : !isDeployed ? 'undeployed' : 'redeploy'}
824+
</Badge>
825+
</Tooltip.Trigger>
826+
<Tooltip.Content>
827+
<span className='text-sm'>{!isDeployed ? 'Click to deploy' : 'Click to redeploy'}</span>
828+
</Tooltip.Content>
829+
</Tooltip.Root>
830+
)
831+
}
832+
755833
/**
756834
* Set of built-in tool types that are core platform tools.
757835
*
@@ -2219,10 +2297,15 @@ export function ToolInput({
22192297
{getIssueBadgeLabel(issue)}
22202298
</Badge>
22212299
</Tooltip.Trigger>
2222-
<Tooltip.Content>{issue.message}: click to open settings</Tooltip.Content>
2300+
<Tooltip.Content>
2301+
<span className='text-sm'>{issue.message}: click to open settings</span>
2302+
</Tooltip.Content>
22232303
</Tooltip.Root>
22242304
)
22252305
})()}
2306+
{tool.type === 'workflow' && tool.params?.workflowId && (
2307+
<WorkflowToolDeployBadge workflowId={tool.params.workflowId} />
2308+
)}
22262309
</div>
22272310
<div className='flex flex-shrink-0 items-center gap-[8px]'>
22282311
{supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && (
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { workflowExecutorTool } from '@/tools/workflow/executor'
3+
4+
describe('workflowExecutorTool', () => {
5+
describe('request.body', () => {
6+
const buildBody = workflowExecutorTool.request.body!
7+
8+
it.concurrent('should pass through object inputMapping unchanged (LLM-provided args)', () => {
9+
const params = {
10+
workflowId: 'test-workflow-id',
11+
inputMapping: { firstName: 'John', lastName: 'Doe', age: 30 },
12+
}
13+
14+
const result = buildBody(params)
15+
16+
expect(result).toEqual({
17+
input: { firstName: 'John', lastName: 'Doe', age: 30 },
18+
triggerType: 'api',
19+
useDraftState: false,
20+
})
21+
})
22+
23+
it.concurrent('should parse JSON string inputMapping (UI-provided via tool-input)', () => {
24+
const params = {
25+
workflowId: 'test-workflow-id',
26+
inputMapping: '{"firstName": "John", "lastName": "Doe"}',
27+
}
28+
29+
const result = buildBody(params)
30+
31+
expect(result).toEqual({
32+
input: { firstName: 'John', lastName: 'Doe' },
33+
triggerType: 'api',
34+
useDraftState: false,
35+
})
36+
})
37+
38+
it.concurrent('should handle nested objects in JSON string inputMapping', () => {
39+
const params = {
40+
workflowId: 'test-workflow-id',
41+
inputMapping: '{"user": {"name": "John", "email": "[email protected]"}, "count": 5}',
42+
}
43+
44+
const result = buildBody(params)
45+
46+
expect(result).toEqual({
47+
input: { user: { name: 'John', email: '[email protected]' }, count: 5 },
48+
triggerType: 'api',
49+
useDraftState: false,
50+
})
51+
})
52+
53+
it.concurrent('should handle arrays in JSON string inputMapping', () => {
54+
const params = {
55+
workflowId: 'test-workflow-id',
56+
inputMapping: '{"tags": ["a", "b", "c"], "ids": [1, 2, 3]}',
57+
}
58+
59+
const result = buildBody(params)
60+
61+
expect(result).toEqual({
62+
input: { tags: ['a', 'b', 'c'], ids: [1, 2, 3] },
63+
triggerType: 'api',
64+
useDraftState: false,
65+
})
66+
})
67+
68+
it.concurrent('should default to empty object when inputMapping is undefined', () => {
69+
const params = {
70+
workflowId: 'test-workflow-id',
71+
inputMapping: undefined,
72+
}
73+
74+
const result = buildBody(params)
75+
76+
expect(result).toEqual({
77+
input: {},
78+
triggerType: 'api',
79+
useDraftState: false,
80+
})
81+
})
82+
83+
it.concurrent('should default to empty object when inputMapping is null', () => {
84+
const params = {
85+
workflowId: 'test-workflow-id',
86+
inputMapping: null as any,
87+
}
88+
89+
const result = buildBody(params)
90+
91+
expect(result).toEqual({
92+
input: {},
93+
triggerType: 'api',
94+
useDraftState: false,
95+
})
96+
})
97+
98+
it.concurrent('should fallback to empty object for invalid JSON string', () => {
99+
const params = {
100+
workflowId: 'test-workflow-id',
101+
inputMapping: 'not valid json {',
102+
}
103+
104+
const result = buildBody(params)
105+
106+
expect(result).toEqual({
107+
input: {},
108+
triggerType: 'api',
109+
useDraftState: false,
110+
})
111+
})
112+
113+
it.concurrent('should fallback to empty object for empty string', () => {
114+
const params = {
115+
workflowId: 'test-workflow-id',
116+
inputMapping: '',
117+
}
118+
119+
const result = buildBody(params)
120+
121+
expect(result).toEqual({
122+
input: {},
123+
triggerType: 'api',
124+
useDraftState: false,
125+
})
126+
})
127+
128+
it.concurrent('should handle empty object inputMapping', () => {
129+
const params = {
130+
workflowId: 'test-workflow-id',
131+
inputMapping: {},
132+
}
133+
134+
const result = buildBody(params)
135+
136+
expect(result).toEqual({
137+
input: {},
138+
triggerType: 'api',
139+
useDraftState: false,
140+
})
141+
})
142+
143+
it.concurrent('should handle empty JSON object string', () => {
144+
const params = {
145+
workflowId: 'test-workflow-id',
146+
inputMapping: '{}',
147+
}
148+
149+
const result = buildBody(params)
150+
151+
expect(result).toEqual({
152+
input: {},
153+
triggerType: 'api',
154+
useDraftState: false,
155+
})
156+
})
157+
158+
it.concurrent('should preserve special characters in string values', () => {
159+
const params = {
160+
workflowId: 'test-workflow-id',
161+
inputMapping: '{"message": "Hello\\nWorld", "path": "C:\\\\Users"}',
162+
}
163+
164+
const result = buildBody(params)
165+
166+
expect(result).toEqual({
167+
input: { message: 'Hello\nWorld', path: 'C:\\Users' },
168+
triggerType: 'api',
169+
useDraftState: false,
170+
})
171+
})
172+
173+
it.concurrent('should handle unicode characters in JSON string', () => {
174+
const params = {
175+
workflowId: 'test-workflow-id',
176+
inputMapping: '{"greeting": "こんにちは", "emoji": "👋"}',
177+
}
178+
179+
const result = buildBody(params)
180+
181+
expect(result).toEqual({
182+
input: { greeting: 'こんにちは', emoji: '👋' },
183+
triggerType: 'api',
184+
useDraftState: false,
185+
})
186+
})
187+
188+
it.concurrent('should not modify object with string values that look like JSON', () => {
189+
const params = {
190+
workflowId: 'test-workflow-id',
191+
inputMapping: { data: '{"nested": "json"}' },
192+
}
193+
194+
const result = buildBody(params)
195+
196+
expect(result).toEqual({
197+
input: { data: '{"nested": "json"}' },
198+
triggerType: 'api',
199+
useDraftState: false,
200+
})
201+
})
202+
})
203+
204+
describe('request.url', () => {
205+
it.concurrent('should build correct URL with workflowId', () => {
206+
const url = workflowExecutorTool.request.url as (params: any) => string
207+
208+
expect(url({ workflowId: 'abc-123' })).toBe('/api/workflows/abc-123/execute')
209+
expect(url({ workflowId: 'my-workflow' })).toBe('/api/workflows/my-workflow/execute')
210+
})
211+
})
212+
213+
describe('tool metadata', () => {
214+
it.concurrent('should have correct id', () => {
215+
expect(workflowExecutorTool.id).toBe('workflow_executor')
216+
})
217+
218+
it.concurrent('should have required workflowId param', () => {
219+
expect(workflowExecutorTool.params.workflowId.required).toBe(true)
220+
})
221+
222+
it.concurrent('should have optional inputMapping param', () => {
223+
expect(workflowExecutorTool.params.inputMapping.required).toBe(false)
224+
})
225+
226+
it.concurrent('should use POST method', () => {
227+
expect(workflowExecutorTool.request.method).toBe('POST')
228+
})
229+
})
230+
})

apps/sim/tools/workflow/executor.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,21 @@ export const workflowExecutorTool: ToolConfig<
3333
url: (params: WorkflowExecutorParams) => `/api/workflows/${params.workflowId}/execute`,
3434
method: 'POST',
3535
headers: () => ({ 'Content-Type': 'application/json' }),
36-
body: (params: WorkflowExecutorParams) => ({
37-
input: params.inputMapping || {},
38-
triggerType: 'api',
39-
useDraftState: false,
40-
}),
36+
body: (params: WorkflowExecutorParams) => {
37+
let inputData = params.inputMapping || {}
38+
if (typeof inputData === 'string') {
39+
try {
40+
inputData = JSON.parse(inputData)
41+
} catch {
42+
inputData = {}
43+
}
44+
}
45+
return {
46+
input: inputData,
47+
triggerType: 'api',
48+
useDraftState: false,
49+
}
50+
},
4151
},
4252
transformResponse: async (response: Response) => {
4353
const data = await response.json()

0 commit comments

Comments
 (0)