Skip to content

Commit db8b564

Browse files
authored
feat(starterinput): added option to specify input format for starter block for api triggered executions (#204)
* added docs to landing page * added basic input format for starter block * improvements for input format for api calls, tested with and without input format * restore old db configs for idle timeout and connect timeout * added tests for execution with formatted input * added syncing of subblock values to make input format persist * fixed empty variable name bug * hide api key in deployment notif, fix db issues
1 parent 57d13a2 commit db8b564

File tree

18 files changed

+755
-52
lines changed

18 files changed

+755
-52
lines changed

sim/app/(landing)/components/nav-client.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export default function NavClient({ children }: { children: React.ReactNode }) {
4949

5050
{/* Social media icons */}
5151
<div className={`flex items-center ${isMobile ? 'gap-2' : 'gap-3'}`}>
52+
<a
53+
href={`${process.env.NEXT_PUBLIC_DOCS_URL}/docs`}
54+
className="text-white/80 hover:text-white/100 text-xl p-2 rounded-md hover:scale-[1.04] transition-colors transition-transform duration-200"
55+
rel="noopener noreferrer"
56+
>
57+
docs
58+
</a>
5259
<a
5360
href="https://x.com/simstudioai"
5461
className="text-white/80 hover:text-white/100 p-2 rounded-md group hover:scale-[1.04] transition-colors transition-transform duration-200"

sim/app/api/workflows/[id]/execute/route.test.ts

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,24 @@
33
*
44
* @vitest-environment node
55
*/
6+
import { NextRequest } from 'next/server'
67
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
78
import { createMockRequest } from '@/app/api/__test-utils__/utils'
89

910
describe('Workflow Execution API Route', () => {
11+
let executeMock = vi.fn().mockResolvedValue({
12+
success: true,
13+
output: {
14+
response: 'Test response',
15+
},
16+
logs: [],
17+
metadata: {
18+
duration: 123,
19+
startTime: new Date().toISOString(),
20+
endTime: new Date().toISOString(),
21+
},
22+
})
23+
1024
beforeEach(() => {
1125
vi.resetModules()
1226

@@ -48,21 +62,24 @@ describe('Workflow Execution API Route', () => {
4862
}),
4963
}))
5064

65+
// Reset execute mock to track calls
66+
executeMock = vi.fn().mockResolvedValue({
67+
success: true,
68+
output: {
69+
response: 'Test response',
70+
},
71+
logs: [],
72+
metadata: {
73+
duration: 123,
74+
startTime: new Date().toISOString(),
75+
endTime: new Date().toISOString(),
76+
},
77+
})
78+
5179
// Mock executor
5280
vi.doMock('@/executor', () => ({
5381
Executor: vi.fn().mockImplementation(() => ({
54-
execute: vi.fn().mockResolvedValue({
55-
success: true,
56-
output: {
57-
response: 'Test response',
58-
},
59-
logs: [],
60-
metadata: {
61-
duration: 123,
62-
startTime: new Date().toISOString(),
63-
endTime: new Date().toISOString(),
64-
},
65-
}),
82+
execute: executeMock,
6683
})),
6784
}))
6885

@@ -110,6 +127,11 @@ describe('Workflow Execution API Route', () => {
110127
})),
111128
})),
112129
})),
130+
update: vi.fn().mockImplementation(() => ({
131+
set: vi.fn().mockImplementation(() => ({
132+
where: vi.fn().mockResolvedValue(undefined),
133+
})),
134+
})),
113135
}
114136

115137
return { db: mockDb }
@@ -178,6 +200,9 @@ describe('Workflow Execution API Route', () => {
178200
// Verify executor was initialized
179201
const Executor = (await import('@/executor')).Executor
180202
expect(Executor).toHaveBeenCalled()
203+
204+
// Verify execute was called with undefined input (GET requests don't have body)
205+
expect(executeMock).toHaveBeenCalledWith('workflow-id')
181206
})
182207

183208
/**
@@ -232,6 +257,124 @@ describe('Workflow Execution API Route', () => {
232257
// Verify executor was constructed
233258
const Executor = (await import('@/executor')).Executor
234259
expect(Executor).toHaveBeenCalled()
260+
261+
// Verify execute was called with the input body
262+
expect(executeMock).toHaveBeenCalledWith('workflow-id')
263+
// Verify the body was passed to the executor constructor
264+
expect(Executor).toHaveBeenCalledWith(
265+
expect.anything(),
266+
expect.anything(),
267+
expect.anything(),
268+
requestBody
269+
)
270+
})
271+
272+
/**
273+
* Test POST execution with structured input matching the input format
274+
*/
275+
it('should execute workflow with structured input matching the input format', async () => {
276+
// Create structured input matching the expected input format
277+
const structuredInput = {
278+
firstName: 'John',
279+
age: 30,
280+
isActive: true,
281+
preferences: { theme: 'dark' },
282+
tags: ['test', 'api'],
283+
}
284+
285+
// Create a mock request with the structured input
286+
const req = createMockRequest('POST', structuredInput)
287+
288+
// Create params similar to what Next.js would provide
289+
const params = Promise.resolve({ id: 'workflow-id' })
290+
291+
// Import the handler after mocks are set up
292+
const { POST } = await import('./route')
293+
294+
// Call the handler
295+
const response = await POST(req, { params })
296+
297+
// Ensure response exists and is successful
298+
expect(response).toBeDefined()
299+
expect(response.status).toBe(200)
300+
301+
// Parse the response body
302+
const data = await response.json()
303+
expect(data).toHaveProperty('success', true)
304+
305+
// Verify the executor was constructed with the structured input
306+
const Executor = (await import('@/executor')).Executor
307+
expect(Executor).toHaveBeenCalledWith(
308+
expect.anything(),
309+
expect.anything(),
310+
expect.anything(),
311+
structuredInput
312+
)
313+
})
314+
315+
/**
316+
* Test POST execution with empty request body
317+
*/
318+
it('should execute workflow with empty request body', async () => {
319+
// Create a mock request with empty body
320+
const req = createMockRequest('POST')
321+
322+
// Create params similar to what Next.js would provide
323+
const params = Promise.resolve({ id: 'workflow-id' })
324+
325+
// Import the handler after mocks are set up
326+
const { POST } = await import('./route')
327+
328+
// Call the handler
329+
const response = await POST(req, { params })
330+
331+
// Ensure response exists and is successful
332+
expect(response).toBeDefined()
333+
expect(response.status).toBe(200)
334+
335+
// Parse the response body
336+
const data = await response.json()
337+
expect(data).toHaveProperty('success', true)
338+
339+
// Verify the executor was constructed with an empty object
340+
const Executor = (await import('@/executor')).Executor
341+
expect(Executor).toHaveBeenCalledWith(
342+
expect.anything(),
343+
expect.anything(),
344+
expect.anything(),
345+
{}
346+
)
347+
})
348+
349+
/**
350+
* Test POST execution with invalid JSON body
351+
*/
352+
it('should handle invalid JSON in request body', async () => {
353+
// Create a mock request with invalid JSON text
354+
const req = new NextRequest('https://example.com/api/workflows/workflow-id/execute', {
355+
method: 'POST',
356+
headers: {
357+
'Content-Type': 'application/json',
358+
},
359+
body: 'this is not valid JSON',
360+
})
361+
362+
// Create params similar to what Next.js would provide
363+
const params = Promise.resolve({ id: 'workflow-id' })
364+
365+
// Import the handler after mocks are set up
366+
const { POST } = await import('./route')
367+
368+
// Call the handler - should throw an error when trying to parse the body
369+
const response = await POST(req, { params })
370+
371+
// Expect error response due to JSON parsing failure
372+
expect(response.status).toBe(500)
373+
374+
const data = await response.json()
375+
expect(data).toHaveProperty('error')
376+
// Check for JSON parse error message rather than "Failed to execute workflow"
377+
expect(data.error).toContain('JSON')
235378
})
236379

237380
/**

sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
234234
return createErrorResponse(validation.error.message, validation.error.status)
235235
}
236236

237-
const body = await request.json().catch(() => ({}))
237+
const bodyText = await request.text()
238+
const body = bodyText ? JSON.parse(bodyText) : {}
239+
238240
const result = await executeWorkflow(validation.workflow, requestId, body)
239241
return createSuccessResponse(result)
240242
} catch (error: any) {

sim/app/w/[id]/components/control-bar/control-bar.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { useExecutionStore } from '@/stores/execution/store'
4343
import { useNotificationStore } from '@/stores/notifications/store'
4444
import { useGeneralStore } from '@/stores/settings/general/store'
4545
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
46+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
4647
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
4748
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
4849
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
@@ -205,12 +206,64 @@ export function ControlBar() {
205206
removeWorkflow(activeWorkflowId)
206207
}
207208

209+
/**
210+
* Get an example of the input format for the workflow
211+
*/
212+
const getInputFormatExample = () => {
213+
let inputFormatExample = ''
214+
try {
215+
// Find the starter block in the workflow
216+
const blocks = Object.values(useWorkflowStore.getState().blocks)
217+
const starterBlock = blocks.find((block) => block.type === 'starter')
218+
219+
if (starterBlock) {
220+
const inputFormat = useSubBlockStore.getState().getValue(starterBlock.id, 'inputFormat')
221+
222+
// If input format is defined, create an example
223+
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
224+
const exampleData: Record<string, any> = {}
225+
226+
// Create example values for each field
227+
inputFormat.forEach((field) => {
228+
if (field.name) {
229+
switch (field.type) {
230+
case 'string':
231+
exampleData[field.name] = 'example'
232+
break
233+
case 'number':
234+
exampleData[field.name] = 42
235+
break
236+
case 'boolean':
237+
exampleData[field.name] = true
238+
break
239+
case 'object':
240+
exampleData[field.name] = { key: 'value' }
241+
break
242+
case 'array':
243+
exampleData[field.name] = [1, 2, 3]
244+
break
245+
}
246+
}
247+
})
248+
249+
inputFormatExample = ` -d '${JSON.stringify(exampleData)}'`
250+
}
251+
}
252+
} catch (error) {
253+
console.error('Error generating input format example:', error)
254+
}
255+
256+
return inputFormatExample
257+
}
258+
208259
/**
209260
* Workflow deployment handler
210261
*/
211262
const handleDeploy = async () => {
212263
if (!activeWorkflowId) return
213264

265+
const inputFormatExample = getInputFormatExample()
266+
214267
// If already deployed, show the API info
215268
if (isDeployed) {
216269
// Try to find an existing API notification
@@ -249,7 +302,7 @@ export function ControlBar() {
249302
{
250303
label: 'Example curl command',
251304
content: apiKey
252-
? `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json" ${endpoint}`
305+
? `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json"${inputFormatExample} ${endpoint}`
253306
: `You need an API key to call this endpoint. Visit your account settings to create one.`,
254307
},
255308
],
@@ -292,7 +345,7 @@ export function ControlBar() {
292345
{
293346
label: 'Example curl command',
294347
content: apiKey
295-
? `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json" ${endpoint}`
348+
? `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json"${inputFormatExample} ${endpoint}`
296349
: `You need an API key to call this endpoint. Visit your account settings to create one.`,
297350
},
298351
],

0 commit comments

Comments
 (0)