Skip to content

Commit 8577b1f

Browse files
committed
Hitl v1
1 parent dbde59b commit 8577b1f

File tree

11 files changed

+206
-78
lines changed

11 files changed

+206
-78
lines changed

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,28 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
754754
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
755755
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
756756
}
757+
} else if (sourceBlock.type === 'human_in_the_loop') {
758+
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
759+
760+
const isSelfReference = activeSourceBlockId === blockId
761+
762+
if (dynamicOutputs.length > 0) {
763+
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
764+
// For self-reference, only show url and resumeEndpoint (not response format fields)
765+
blockTags = isSelfReference
766+
? allTags.filter(
767+
(tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')
768+
)
769+
: allTags
770+
} else {
771+
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
772+
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
773+
blockTags = isSelfReference
774+
? allTags.filter(
775+
(tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')
776+
)
777+
: allTags
778+
}
757779
} else {
758780
const operationValue =
759781
mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation')
@@ -1073,7 +1095,24 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
10731095
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
10741096
}
10751097
} else if (accessibleBlock.type === 'human_in_the_loop') {
1076-
blockTags = [`${normalizedBlockName}.url`]
1098+
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
1099+
1100+
const isSelfReference = accessibleBlockId === blockId
1101+
1102+
if (dynamicOutputs.length > 0) {
1103+
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
1104+
// For self-reference, only show url and resumeEndpoint (not response format fields)
1105+
blockTags = isSelfReference
1106+
? allTags.filter(
1107+
(tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')
1108+
)
1109+
: allTags
1110+
} else {
1111+
blockTags = [
1112+
`${normalizedBlockName}.url`,
1113+
`${normalizedBlockName}.resumeEndpoint`,
1114+
]
1115+
}
10771116
} else {
10781117
const operationValue =
10791118
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')

apps/sim/blocks/blocks/human_in_the_loop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,6 @@ export const HumanInTheLoopBlock: BlockConfig<ResponseBlockOutput> = {
157157
},
158158
outputs: {
159159
url: { type: 'string', description: 'Resume UI URL' },
160-
// apiUrl: { type: 'string', description: 'Resume API URL' }, // Commented out - not accessible as output
160+
resumeEndpoint: { type: 'string', description: 'Resume API endpoint URL for direct curl requests' },
161161
},
162162
}

apps/sim/blocks/blocks/webhook_request.ts

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
1-
import { createHmac } from 'crypto'
2-
import { createLogger } from '@sim/logger'
3-
import { v4 as uuidv4 } from 'uuid'
41
import { WebhookIcon } from '@/components/icons'
52
import type { BlockConfig } from '@/blocks/types'
63
import type { RequestResponse } from '@/tools/http/types'
74

8-
const logger = createLogger('WebhookRequestBlock')
9-
10-
/**
11-
* Generates HMAC-SHA256 signature for webhook payload
12-
*/
13-
function generateSignature(secret: string, timestamp: number, body: string): string {
14-
const signatureBase = `${timestamp}.${body}`
15-
return createHmac('sha256', secret).update(signatureBase).digest('hex')
16-
}
17-
185
export const WebhookRequestBlock: BlockConfig<RequestResponse> = {
196
type: 'webhook_request',
207
name: 'Webhook',
@@ -83,57 +70,7 @@ Example:
8370
},
8471
],
8572
tools: {
86-
access: ['http_request'],
87-
config: {
88-
tool: () => 'http_request',
89-
params: (params: Record<string, any>) => {
90-
const timestamp = Date.now()
91-
const deliveryId = uuidv4()
92-
93-
// Start with webhook-specific headers
94-
const webhookHeaders: Record<string, string> = {
95-
'Content-Type': 'application/json',
96-
'X-Webhook-Timestamp': timestamp.toString(),
97-
'X-Delivery-ID': deliveryId,
98-
'Idempotency-Key': deliveryId,
99-
}
100-
101-
// Add signature if secret is provided
102-
if (params.secret) {
103-
const bodyString =
104-
typeof params.body === 'string' ? params.body : JSON.stringify(params.body || {})
105-
const signature = generateSignature(params.secret, timestamp, bodyString)
106-
webhookHeaders['X-Webhook-Signature'] = `t=${timestamp},v1=${signature}`
107-
}
108-
109-
// Merge with user-provided headers (user headers take precedence)
110-
// Headers must be in TableRow format: { cells: { Key: string, Value: string } }
111-
const userHeaders = params.headers || []
112-
const mergedHeaders = [
113-
...Object.entries(webhookHeaders).map(([key, value]) => ({
114-
cells: { Key: key, Value: value },
115-
})),
116-
...userHeaders,
117-
]
118-
119-
const payload = {
120-
url: params.url,
121-
method: 'POST',
122-
headers: mergedHeaders,
123-
body: params.body,
124-
}
125-
126-
logger.info('Sending webhook request', {
127-
url: payload.url,
128-
method: payload.method,
129-
headers: mergedHeaders,
130-
body: payload.body,
131-
hasSignature: !!params.secret,
132-
})
133-
134-
return payload
135-
},
136-
},
73+
access: ['webhook_request'],
13774
},
13875
inputs: {
13976
url: { type: 'string', description: 'Webhook URL to send the request to' },
@@ -147,4 +84,3 @@ Example:
14784
headers: { type: 'json', description: 'Response headers' },
14885
},
14986
}
150-

apps/sim/executor/execution/block-executor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ export class BlockExecutor {
510510
const placeholderState: BlockState = {
511511
output: {
512512
url: resumeLinks.uiUrl,
513-
// apiUrl: resumeLinks.apiUrl, // Hidden from output
513+
resumeEndpoint: resumeLinks.apiUrl,
514514
},
515515
executed: false,
516516
executionTime: existingState?.executionTime ?? 0,

apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
227227

228228
if (resumeLinks) {
229229
output.url = resumeLinks.uiUrl
230-
// output.apiUrl = resumeLinks.apiUrl // Hidden from output
230+
output.resumeEndpoint = resumeLinks.apiUrl
231231
}
232232

233233
return output
@@ -576,9 +576,9 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
576576
if (context.resumeLinks.uiUrl) {
577577
pauseOutput.url = context.resumeLinks.uiUrl
578578
}
579-
// if (context.resumeLinks.apiUrl) {
580-
// pauseOutput.apiUrl = context.resumeLinks.apiUrl
581-
// } // Hidden from output
579+
if (context.resumeLinks.apiUrl) {
580+
pauseOutput.resumeEndpoint = context.resumeLinks.apiUrl
581+
}
582582
}
583583

584584
if (Array.isArray(context.inputFormat)) {

apps/sim/lib/workflows/blocks/block-outputs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,10 @@ export function getBlockOutputs(
226226
}
227227

228228
if (blockType === 'human_in_the_loop') {
229-
// For human_in_the_loop, only expose url (inputFormat fields are only available after resume)
229+
// For human_in_the_loop, only expose url and resumeEndpoint (inputFormat fields are only available after resume)
230230
return {
231231
url: { type: 'string', description: 'Resume UI URL' },
232+
resumeEndpoint: { type: 'string', description: 'Resume API endpoint URL for direct curl requests' },
232233
}
233234
}
234235

apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -527,15 +527,15 @@ export class PauseResumeManager {
527527

528528
mergedOutput.resume = mergedOutput.resume ?? mergedResponse.resume
529529

530-
// Preserve url from resume links (apiUrl hidden from output)
530+
// Preserve url and resumeEndpoint from resume links
531531
const resumeLinks = mergedOutput.resume ?? mergedResponse.resume
532532
if (resumeLinks && typeof resumeLinks === 'object') {
533533
if (resumeLinks.uiUrl) {
534534
mergedOutput.url = resumeLinks.uiUrl
535535
}
536-
// if (resumeLinks.apiUrl) {
537-
// mergedOutput.apiUrl = resumeLinks.apiUrl
538-
// } // Hidden from output
536+
if (resumeLinks.apiUrl) {
537+
mergedOutput.resumeEndpoint = resumeLinks.apiUrl
538+
}
539539
}
540540

541541
for (const [key, value] of Object.entries(submissionPayload)) {

apps/sim/tools/http/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import { requestTool } from './request'
2+
import { webhookRequestTool } from './webhook_request'
23

34
export const httpRequestTool = requestTool
5+
export { webhookRequestTool }

apps/sim/tools/http/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ export interface RequestResponse extends ToolResponse {
1717
headers: Record<string, string>
1818
}
1919
}
20+
21+
export interface WebhookRequestParams {
22+
url: string
23+
body?: any
24+
secret?: string
25+
headers?: TableRow[]
26+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { createHmac } from 'crypto'
2+
import { v4 as uuidv4 } from 'uuid'
3+
import type { ToolConfig } from '@/tools/types'
4+
import type { RequestResponse, WebhookRequestParams } from './types'
5+
import { transformTable } from './utils'
6+
7+
/**
8+
* Generates HMAC-SHA256 signature for webhook payload
9+
*/
10+
function generateSignature(secret: string, timestamp: number, body: string): string {
11+
const signatureBase = `${timestamp}.${body}`
12+
return createHmac('sha256', secret).update(signatureBase).digest('hex')
13+
}
14+
15+
export const webhookRequestTool: ToolConfig<WebhookRequestParams, RequestResponse> = {
16+
id: 'webhook_request',
17+
name: 'Webhook Request',
18+
description: 'Send a webhook request with automatic headers and optional HMAC signing',
19+
version: '1.0.0',
20+
21+
params: {
22+
url: {
23+
type: 'string',
24+
required: true,
25+
description: 'The webhook URL to send the request to',
26+
},
27+
body: {
28+
type: 'object',
29+
description: 'JSON payload to send',
30+
},
31+
secret: {
32+
type: 'string',
33+
description: 'Optional secret for HMAC-SHA256 signature',
34+
},
35+
headers: {
36+
type: 'object',
37+
description: 'Additional headers to include',
38+
},
39+
},
40+
41+
request: {
42+
url: (params: WebhookRequestParams) => params.url,
43+
44+
method: () => 'POST',
45+
46+
headers: (params: WebhookRequestParams) => {
47+
const timestamp = Date.now()
48+
const deliveryId = uuidv4()
49+
50+
// Start with webhook-specific headers
51+
const webhookHeaders: Record<string, string> = {
52+
'Content-Type': 'application/json',
53+
'X-Webhook-Timestamp': timestamp.toString(),
54+
'X-Delivery-ID': deliveryId,
55+
'Idempotency-Key': deliveryId,
56+
}
57+
58+
// Add signature if secret is provided
59+
if (params.secret) {
60+
const bodyString =
61+
typeof params.body === 'string' ? params.body : JSON.stringify(params.body || {})
62+
const signature = generateSignature(params.secret, timestamp, bodyString)
63+
webhookHeaders['X-Webhook-Signature'] = `t=${timestamp},v1=${signature}`
64+
}
65+
66+
// Merge with user-provided headers (user headers take precedence)
67+
// Handle different header formats:
68+
// - Array of TableRow objects (from block usage): [{ cells: { Key, Value } }]
69+
// - Plain object (from direct tool usage): { key: value }
70+
// - undefined/null
71+
let userHeaders: Record<string, string> = {}
72+
if (params.headers) {
73+
if (Array.isArray(params.headers)) {
74+
userHeaders = transformTable(params.headers)
75+
} else if (typeof params.headers === 'object') {
76+
userHeaders = params.headers as Record<string, string>
77+
}
78+
}
79+
80+
return { ...webhookHeaders, ...userHeaders }
81+
},
82+
83+
body: (params: WebhookRequestParams) => params.body,
84+
},
85+
86+
transformResponse: async (response: Response) => {
87+
const contentType = response.headers.get('content-type') || ''
88+
89+
const headers: Record<string, string> = {}
90+
response.headers.forEach((value, key) => {
91+
headers[key] = value
92+
})
93+
94+
const data = await (contentType.includes('application/json')
95+
? response.json()
96+
: response.text())
97+
98+
// Check if this is a proxy response
99+
if (
100+
contentType.includes('application/json') &&
101+
typeof data === 'object' &&
102+
data !== null &&
103+
data.data !== undefined &&
104+
data.status !== undefined
105+
) {
106+
return {
107+
success: data.success,
108+
output: {
109+
data: data.data,
110+
status: data.status,
111+
headers: data.headers || {},
112+
},
113+
error: data.success ? undefined : data.error,
114+
}
115+
}
116+
117+
return {
118+
success: response.ok,
119+
output: {
120+
data,
121+
status: response.status,
122+
headers,
123+
},
124+
error: undefined,
125+
}
126+
},
127+
128+
outputs: {
129+
data: {
130+
type: 'json',
131+
description: 'Response data from the webhook endpoint',
132+
},
133+
status: {
134+
type: 'number',
135+
description: 'HTTP status code',
136+
},
137+
headers: {
138+
type: 'object',
139+
description: 'Response headers',
140+
},
141+
},
142+
}

0 commit comments

Comments
 (0)