Compare commits

...

1 Commits

Author SHA1 Message Date
priyanshu.solanki
2b8b67179a fix to the response types and formatting 2025-12-18 19:00:17 -07:00
8 changed files with 418 additions and 11 deletions

View File

@@ -15,6 +15,7 @@ export const ResponseBlock: BlockConfig<ResponseBlockOutput> = {
- This is usually used as the last block in the workflow.
`,
category: 'blocks',
hideFromToolbar: true,
bgColor: '#2F55FF',
icon: ResponseIcon,
subBlocks: [

View File

@@ -0,0 +1,121 @@
import { ResponseIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
/**
* WorkflowResponseBlock - A simplified response block with flat output structure.
* Output is directly accessible via <Response.fieldName> instead of nested paths.
*/
export const WorkflowResponseBlock: BlockConfig = {
type: 'workflow_response',
name: 'Response',
description: 'Send structured API response',
longDescription:
'A Response block with direct field access. Use <Response.fieldName> to reference output fields directly.',
docsLink: 'https://docs.sim.ai/blocks/response',
bestPractices: `
- Only use this if the trigger block is the API Trigger.
- Prefer the builder mode over the editor mode.
- This is usually used as the last block in the workflow.
- Output fields are directly accessible: <Response.fieldName>
`,
category: 'blocks',
bgColor: '#2F55FF',
icon: ResponseIcon,
subBlocks: [
{
id: 'dataMode',
title: 'Response Data Mode',
type: 'dropdown',
options: [
{ label: 'Builder', id: 'structured' },
{ label: 'Editor', id: 'json' },
],
value: () => 'structured',
description: 'Choose how to define your response data structure',
},
{
id: 'builderData',
title: 'Response Structure',
type: 'response-format',
condition: { field: 'dataMode', value: 'structured' },
description:
'Define the structure of your response data. Use <variable.name> in field names to reference workflow variables.',
},
{
id: 'data',
title: 'Response Data',
type: 'code',
placeholder: '{\n "message": "Hello world",\n "userId": "<variable.userId>"\n}',
language: 'json',
condition: { field: 'dataMode', value: 'json' },
description:
'Data that will be sent as the response body on API calls. Use <variable.name> to reference workflow variables.',
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert JSON programmer.
Generate ONLY the raw JSON object based on the user's request.
The output MUST be a single, valid JSON object, starting with { and ending with }.
Current response: {context}
Do not include any explanations, markdown formatting, or other text outside the JSON object.
You have access to the following variables you can use to generate the JSON body:
- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., '<paramName>'. Do NOT use 'params.paramName'.
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env.
Example:
{
"name": "<block.agent.response.content>",
"age": <block.function.output.age>,
"success": true
}`,
placeholder: 'Describe the API response structure you need...',
generationType: 'json-object',
},
},
{
id: 'status',
title: 'Status Code',
type: 'short-input',
placeholder: '200',
description: 'HTTP status code (default: 200)',
},
{
id: 'headers',
title: 'Response Headers',
type: 'table',
columns: ['Key', 'Value'],
description: 'Additional HTTP headers to include in the response',
},
],
tools: { access: [] },
inputs: {
dataMode: {
type: 'string',
description: 'Response data definition mode',
},
builderData: {
type: 'json',
description: 'Structured response data',
},
data: {
type: 'json',
description: 'JSON response body',
},
status: {
type: 'number',
description: 'HTTP status code',
},
headers: {
type: 'json',
description: 'Response headers',
},
},
outputs: {
// User's data fields are spread directly at root level
status: { type: 'number', description: 'HTTP status code' },
headers: { type: 'json', description: 'Response headers' },
},
}

View File

@@ -131,6 +131,7 @@ import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
import { WordPressBlock } from '@/blocks/blocks/wordpress'
import { WorkflowBlock } from '@/blocks/blocks/workflow'
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
import { WorkflowResponseBlock } from '@/blocks/blocks/workflow_response'
import { XBlock } from '@/blocks/blocks/x'
import { YouTubeBlock } from '@/blocks/blocks/youtube'
import { ZendeskBlock } from '@/blocks/blocks/zendesk'
@@ -231,6 +232,7 @@ export const registry: Record<string, BlockConfig> = {
reddit: RedditBlock,
resend: ResendBlock,
response: ResponseBlock,
workflow_response: WorkflowResponseBlock,
rss: RssBlock,
router: RouterBlock,
s3: S3Block,

View File

@@ -15,6 +15,7 @@ export enum BlockType {
VARIABLES = 'variables',
RESPONSE = 'response',
WORKFLOW_RESPONSE = 'workflow_response',
HUMAN_IN_THE_LOOP = 'human_in_the_loop',
WORKFLOW = 'workflow',
WORKFLOW_INPUT = 'workflow_input',

View File

@@ -11,6 +11,7 @@ import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler
import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler'
import { WaitBlockHandler } from '@/executor/handlers/wait/wait-handler'
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
import { WorkflowResponseBlockHandler } from '@/executor/handlers/workflow-response/workflow-response-handler'
export {
AgentBlockHandler,
@@ -19,6 +20,7 @@ export {
EvaluatorBlockHandler,
FunctionBlockHandler,
GenericBlockHandler,
WorkflowResponseBlockHandler,
ResponseBlockHandler,
HumanInTheLoopBlockHandler,
RouterBlockHandler,

View File

@@ -18,6 +18,7 @@ import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler
import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler'
import { WaitBlockHandler } from '@/executor/handlers/wait/wait-handler'
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
import { WorkflowResponseBlockHandler } from '@/executor/handlers/workflow-response/workflow-response-handler'
import type { BlockHandler } from '@/executor/types'
/**
@@ -34,6 +35,7 @@ export function createBlockHandlers(): BlockHandler[] {
new ConditionBlockHandler(),
new RouterBlockHandler(),
new ResponseBlockHandler(),
new WorkflowResponseBlockHandler(),
new HumanInTheLoopBlockHandler(),
new AgentBlockHandler(),
new VariablesBlockHandler(),

View File

@@ -0,0 +1,258 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, HTTP } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
const logger = createLogger('WorkflowResponseBlockHandler')
interface JSONProperty {
id: string
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
value: any
collapsed?: boolean
}
/**
* Handler for the WorkflowResponse block.
* Returns a flat output structure where data fields are directly accessible.
* Reference: <Response.fieldName> directly (no wrapper like old Response block)
* API output is exactly what user defines - no metadata, no wrappers.
*/
export class WorkflowResponseBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.WORKFLOW_RESPONSE
}
async execute(
ctx: ExecutionContext,
block: SerializedBlock,
inputs: Record<string, any>
): Promise<BlockOutput> {
logger.info(`Executing workflow response block: ${block.id}`)
try {
const responseData = this.parseResponseData(inputs)
const statusCode = this.parseStatus(inputs.status)
const responseHeaders = this.parseHeaders(inputs.headers)
logger.info('Workflow Response prepared', {
status: statusCode,
dataKeys: typeof responseData === 'object' ? Object.keys(responseData) : [],
headerKeys: Object.keys(responseHeaders),
})
// Flat output structure - no response or data wrapper
// Access as <Response.fieldName>, <Response.status>, <Response.headers>
return {
...responseData,
status: statusCode,
headers: responseHeaders,
}
} catch (error: any) {
logger.error('Workflow Response block execution failed:', error)
return {
error: {
type: 'Workflow Response block execution failed',
message: error.message || 'Unknown error',
},
status: HTTP.STATUS.SERVER_ERROR,
headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON },
}
}
}
private parseResponseData(inputs: Record<string, any>): any {
const dataMode = inputs.dataMode || 'structured'
if (dataMode === 'json' && inputs.data) {
if (typeof inputs.data === 'string') {
try {
return JSON.parse(inputs.data)
} catch (error) {
logger.warn('Failed to parse JSON data, returning as string:', error)
return inputs.data
}
} else if (typeof inputs.data === 'object' && inputs.data !== null) {
return inputs.data
}
return inputs.data
}
if (dataMode === 'structured' && inputs.builderData) {
const convertedData = this.convertBuilderDataToJson(inputs.builderData)
return this.parseObjectStrings(convertedData)
}
return inputs.data || {}
}
private convertBuilderDataToJson(builderData: JSONProperty[]): any {
if (!Array.isArray(builderData)) {
return {}
}
const result: any = {}
for (const prop of builderData) {
if (!prop.name || !prop.name.trim()) {
continue
}
const value = this.convertPropertyValue(prop)
result[prop.name] = value
}
return result
}
private convertPropertyValue(prop: JSONProperty): any {
switch (prop.type) {
case 'object':
return this.convertObjectValue(prop.value)
case 'array':
return this.convertArrayValue(prop.value)
case 'number':
return this.convertNumberValue(prop.value)
case 'boolean':
return this.convertBooleanValue(prop.value)
case 'files':
return prop.value
default:
return prop.value
}
}
private convertObjectValue(value: any): any {
if (Array.isArray(value)) {
return this.convertBuilderDataToJson(value)
}
if (typeof value === 'string' && !this.isVariableReference(value)) {
return this.tryParseJson(value, value)
}
return value
}
private convertArrayValue(value: any): any {
if (Array.isArray(value)) {
return value.map((item: any) => this.convertArrayItem(item))
}
if (typeof value === 'string' && !this.isVariableReference(value)) {
const parsed = this.tryParseJson(value, value)
if (Array.isArray(parsed)) {
return parsed
}
return value
}
return value
}
private convertArrayItem(item: any): any {
if (typeof item !== 'object' || !item.type) {
return item
}
if (item.type === 'object' && Array.isArray(item.value)) {
return this.convertBuilderDataToJson(item.value)
}
if (item.type === 'array' && Array.isArray(item.value)) {
return item.value.map((subItem: any) => {
if (typeof subItem === 'object' && subItem.type) {
return subItem.value
}
return subItem
})
}
return item.value
}
private convertNumberValue(value: any): any {
if (this.isVariableReference(value)) {
return value
}
const numValue = Number(value)
if (Number.isNaN(numValue)) {
return value
}
return numValue
}
private convertBooleanValue(value: any): any {
if (this.isVariableReference(value)) {
return value
}
return value === 'true' || value === true
}
private tryParseJson(jsonString: string, fallback: any): any {
try {
return JSON.parse(jsonString)
} catch {
return fallback
}
}
private isVariableReference(value: any): boolean {
return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>')
}
private parseObjectStrings(data: any): any {
if (typeof data === 'string') {
try {
const parsed = JSON.parse(data)
if (typeof parsed === 'object' && parsed !== null) {
return this.parseObjectStrings(parsed)
}
return parsed
} catch {
return data
}
} else if (Array.isArray(data)) {
return data.map((item) => this.parseObjectStrings(item))
} else if (typeof data === 'object' && data !== null) {
const result: any = {}
for (const [key, value] of Object.entries(data)) {
result[key] = this.parseObjectStrings(value)
}
return result
}
return data
}
private parseStatus(status?: string): number {
if (!status) return HTTP.STATUS.OK
const parsed = Number(status)
if (Number.isNaN(parsed) || parsed < 100 || parsed > 599) {
return HTTP.STATUS.OK
}
return parsed
}
private parseHeaders(
headers: {
id: string
cells: { Key: string; Value: string }
}[]
): Record<string, string> {
const defaultHeaders = { 'Content-Type': HTTP.CONTENT_TYPE.JSON }
if (!headers) return defaultHeaders
const headerObj = headers.reduce((acc: Record<string, string>, header) => {
if (header?.cells?.Key && header?.cells?.Value) {
acc[header.cells.Key] = header.cells.Value
}
return acc
}, {})
return { ...defaultHeaders, ...headerObj }
}
}

View File

@@ -439,26 +439,46 @@ export function stripCustomToolPrefix(name: string) {
}
export const workflowHasResponseBlock = (executionResult: ExecutionResult): boolean => {
if (
!executionResult?.logs ||
!Array.isArray(executionResult.logs) ||
!executionResult.success ||
!executionResult.output.response
) {
if (!executionResult?.logs || !Array.isArray(executionResult.logs) || !executionResult.success) {
return false
}
const responseBlock = executionResult.logs.find(
(log) => log?.blockType === 'response' && log?.success
// Check for old response block format (has output.response)
if (executionResult.output.response) {
const responseBlock = executionResult.logs.find(
(log) => log?.blockType === 'response' && log?.success
)
if (responseBlock) return true
}
// Check for new workflow_response block format (has status/headers at root)
const workflowResponseBlock = executionResult.logs.find(
(log) => log?.blockType === 'workflow_response' && log?.success
)
return responseBlock !== undefined
return workflowResponseBlock !== undefined
}
// Create a HTTP response from response block
export const createHttpResponseFromBlock = (executionResult: ExecutionResult): NextResponse => {
const output = executionResult.output.response
const { data = {}, status = 200, headers = {} } = output
// Check if it's the old response block format
if (executionResult.output.response) {
const output = executionResult.output.response
const { data = {}, status = 200, headers = {} } = output
const responseHeaders = new Headers({
'Content-Type': 'application/json',
...headers,
})
return NextResponse.json(data, {
status: status,
headers: responseHeaders,
})
}
// New workflow_response format - status/headers at root, data spread at root
const { status = 200, headers = {}, ...data } = executionResult.output
const responseHeaders = new Headers({
'Content-Type': 'application/json',