Skip to content

Commit 9f8b03d

Browse files
authored
fix(greenhouse): align configured agent inputs with API parameters (#9213)
Expose configured Greenhouse agent inputs using API parameter names and migrate saved workflow fields without restoring stale values.
1 parent 5187677 commit 9f8b03d

12 files changed

Lines changed: 984 additions & 409 deletions

File tree

‎apps/docs/content/docs/integrations/greenhouse.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Harvest v3 returns applications, attachments, education, and employment through
2727

2828
List operations return one page, its `count`, `nextCursor`, and `hasMore`. Set a page size from 1 to 500 and pass `nextCursor` into the next call until `hasMore` is false. A cursor carries the initial query, so other filters and page size are ignored while it is set. Date filters accept a JSON object such as `{"gte":"2026-01-01T00:00:00Z"}`; use either `created_at` or `updated_at`, not both.
2929

30+
For list operations, Job IDs uses comma-separated IDs. For prospect application creation, Job IDs uses a JSON array of integers. Created At takes a JSON date range on list operations, an ISO 8601 timestamp when creating an application, and a `YYYY-MM-DD` date when updating an application.
31+
3032
Partial reads leave unrequested fields as `null`, including collections. An explicit empty array means the collection is empty. Date-only filters, such as offer `sent_on` and `starts_on`, use `YYYY-MM-DD` bounds.
3133

3234
When updating candidate contact arrays, tags, or linked users, read the current candidate first and submit the complete desired array: supplied arrays replace their existing values. Unset inputs are omitted; custom-field entries may use `null` to clear a value. Creating an email note records an email in Greenhouse and does not send one. Application movement may trigger Greenhouse stage automation, and rejection can optionally send a rejection email. Move, hire, reject, and unreject operations return no provider fields on success.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface ToolSubBlockRendererProps {
2323
/** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */
2424
toolType: string
2525
toolParams: Record<string, string> | undefined
26+
operation?: string
2627
onParamChange: (toolIndex: number, paramId: string, value: string) => void
2728
disabled: boolean
2829
canonicalToggle?: {
@@ -44,6 +45,7 @@ export function ToolSubBlockRenderer({
4445
effectiveParamId,
4546
toolType,
4647
toolParams,
48+
operation,
4749
onParamChange,
4850
disabled,
4951
canonicalToggle,
@@ -128,7 +130,7 @@ export function ToolSubBlockRenderer({
128130
isPreview={false}
129131
disabled={disabled}
130132
canonicalToggle={canonicalToggle}
131-
dependencyContext={toolParams}
133+
dependencyContext={{ ...toolParams, operation }}
132134
/>
133135
</DependencyBlockTypeProvider>
134136
)

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { getTransitiveSubBlockDependents } from '@/lib/workflows/subblocks/dependencies'
2+
import type { StoredTool } from '@/lib/workflows/tool-input/types'
23
import { getBlock } from '@/blocks/registry'
4+
import type { ToolMetadata } from '@/tools/metadata'
5+
import { isUserFacingToolParam } from '@/tools/params'
36

47
/**
58
* Clear every TRANSITIVE `dependsOn` descendant of `changedParamId` in a nested tool's params,
@@ -23,3 +26,43 @@ export function clearDependentToolParams(
2326
}
2427
return next ?? params
2528
}
29+
30+
/** Keeps user-configured inputs that the newly selected operation accepts. */
31+
export function getParamsForToolOperation(
32+
tool: Pick<StoredTool, 'type' | 'params'>,
33+
nextTool: ToolMetadata
34+
): Record<string, string> {
35+
const newParamIds = new Set(
36+
Object.entries(nextTool.params ?? {})
37+
.filter(([, param]) => isUserFacingToolParam(param))
38+
.map(([paramId]) => paramId)
39+
)
40+
const subBlocks = getBlock(tool.type)?.subBlocks ?? []
41+
if (nextTool.oauth) {
42+
const credentialControls = subBlocks.filter(
43+
(subBlock) =>
44+
subBlock.type === 'oauth-input' && subBlock.serviceId === nextTool.oauth?.provider
45+
)
46+
const credentialCanonicalIds = new Set(
47+
credentialControls.flatMap((subBlock) =>
48+
subBlock.canonicalParamId ? [subBlock.canonicalParamId] : []
49+
)
50+
)
51+
for (const subBlock of subBlocks) {
52+
if (
53+
credentialControls.includes(subBlock) ||
54+
(subBlock.canonicalParamId &&
55+
credentialCanonicalIds.has(subBlock.canonicalParamId) &&
56+
(!subBlock.serviceId || subBlock.serviceId === nextTool.oauth.provider))
57+
) {
58+
newParamIds.add(subBlock.id)
59+
if (subBlock.canonicalParamId) newParamIds.add(subBlock.canonicalParamId)
60+
}
61+
}
62+
}
63+
const preservedParams: Record<string, string> = {}
64+
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
65+
if (newParamIds.has(paramId) && value) preservedParams[paramId] = value
66+
})
67+
return clearDependentToolParams(tool.type, preservedParams, 'operation')
68+
}

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

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
6060
import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer'
6161
import { ToolUsageControl } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control'
62-
import { clearDependentToolParams } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
62+
import {
63+
clearDependentToolParams,
64+
getParamsForToolOperation,
65+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
6366
import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types'
6467
import {
6568
isAgentToolBlock,
@@ -111,7 +114,6 @@ import {
111114
formatParameterLabel,
112115
getSubBlocksForToolInput,
113116
getToolIdForOperation,
114-
isUserFacingToolParam,
115117
type SubBlocksForToolInput,
116118
} from '@/tools/params'
117119

@@ -979,18 +981,7 @@ export const ToolInput = memo(function ToolInput({
979981
return
980982
}
981983

982-
const newParamIds = new Set(
983-
Object.entries(newToolConfig.params ?? {})
984-
.filter(([, param]) => isUserFacingToolParam(param))
985-
.map(([paramId]) => paramId)
986-
)
987-
988-
const preservedParams: Record<string, string> = {}
989-
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
990-
if (newParamIds.has(paramId) && value) {
991-
preservedParams[paramId] = value
992-
}
993-
})
984+
const preservedParams = getParamsForToolOperation(tool, newToolConfig)
994985

995986
if (tool.type === 'jira') {
996987
const subBlockStore = useSubBlockStore.getState()
@@ -1529,9 +1520,18 @@ export const ToolInput = memo(function ToolInput({
15291520
? (toolBlocks.find((block) => block.type === tool.type) ?? getBlock(tool.type))
15301521
: null
15311522

1523+
const hasOperations =
1524+
!isCustomTool && !isMcpFamily && hasMultipleOperations(toolBlock ?? undefined)
1525+
const operationChoices = hasOperations
1526+
? getOperationChoices(toolBlock ?? undefined)
1527+
: null
1528+
const selectedOperation =
1529+
tool.operation ||
1530+
operationChoices?.options.find((option) => !operationChoices.denied.has(option.id))?.id
1531+
15321532
const currentToolId =
15331533
!isCustomTool && !isMcpFamily
1534-
? getToolIdForOperation(tool.type, tool.operation, toolBlock ?? undefined) ||
1534+
? getToolIdForOperation(tool.type, selectedOperation, toolBlock ?? undefined) ||
15351535
tool.toolId ||
15361536
''
15371537
: tool.toolId || ''
@@ -1555,8 +1555,8 @@ export const ToolInput = memo(function ToolInput({
15551555
currentToolId,
15561556
tool.type,
15571557
{
1558-
operation: tool.operation,
15591558
...tool.params,
1559+
operation: selectedOperation,
15601560
},
15611561
toolScopedOverrides,
15621562
toolBlock ?? undefined
@@ -1609,8 +1609,6 @@ export const ToolInput = memo(function ToolInput({
16091609
toolCredential?.type === sb.reactiveCondition.requiredType
16101610
)
16111611

1612-
const hasOperations =
1613-
!isCustomTool && !isMcpFamily && hasMultipleOperations(toolBlock ?? undefined)
16141612
const showToolControl = supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool))
16151613
const hasToolBody = showToolControl || hasOperations || displaySubBlocks.length > 0
16161614

@@ -1869,10 +1867,8 @@ export const ToolInput = memo(function ToolInput({
18691867
)}
18701868
{/* Operation dropdown for tools with multiple operations */}
18711869
{(() => {
1872-
if (!hasOperations) return null
1873-
const { options: operationOptions, denied } = getOperationChoices(
1874-
toolBlock ?? undefined
1875-
)
1870+
if (!operationChoices) return null
1871+
const { options: operationOptions, denied } = operationChoices
18761872
if (operationOptions.length === 0) return null
18771873

18781874
return (
@@ -1884,10 +1880,7 @@ export const ToolInput = memo(function ToolInput({
18841880
value: option.id,
18851881
hidden: denied.has(option.id),
18861882
}))}
1887-
value={
1888-
tool.operation ||
1889-
operationOptions.find((option) => !denied.has(option.id))?.id
1890-
}
1883+
value={selectedOperation}
18911884
onChange={(value) => handleOperationChange(toolIndex, value)}
18921885
placeholder='Select operation'
18931886
/* Denied operations only drop out once the config
@@ -1912,7 +1905,7 @@ export const ToolInput = memo(function ToolInput({
19121905
canonicalGroup && hasCanonicalPair
19131906
? resolveCanonicalMode(
19141907
canonicalGroup,
1915-
{ operation: tool.operation, ...tool.params },
1908+
{ ...tool.params, operation: selectedOperation },
19161909
toolScopedOverrides
19171910
)
19181911
: undefined
@@ -1949,6 +1942,7 @@ export const ToolInput = memo(function ToolInput({
19491942
effectiveParamId={effectiveParamId}
19501943
toolType={tool.type}
19511944
toolParams={tool.params}
1945+
operation={selectedOperation}
19521946
onParamChange={handleParamChange}
19531947
disabled={disabled}
19541948
canonicalToggle={canonicalToggleProp}

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

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { isEqual } from 'es-toolkit'
1212
import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants'
1313
import type { FallbackModelEntry } from '@/lib/workflows/blocks/fallback-models'
14+
import { buildSubBlockValues } from '@/lib/workflows/subblocks/visibility'
1415
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
1516
import {
1617
CheckboxList,
@@ -121,10 +122,10 @@ interface SubBlockProps {
121122
* Supports boolean, condition objects, and functions that return conditions.
122123
*
123124
* @param config - The sub-block configuration containing requirement rules
124-
* @param subBlockValues - Current values of all subblocks for condition evaluation
125+
* @param values - Flat sibling values for condition evaluation
125126
* @returns `true` if the field is required based on current context
126127
*/
127-
const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string, any>): boolean => {
128+
const isFieldRequired = (config: SubBlockConfig, values?: Record<string, any>): boolean => {
128129
if (!config.required) return false
129130
if (typeof config.required === 'boolean') return config.required
130131

@@ -141,7 +142,7 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
141142
},
142143
values: Record<string, any>
143144
): boolean => {
144-
const fieldValue = values[cond.field]?.value
145+
const fieldValue = values[cond.field]
145146
const condValue = cond.value
146147

147148
let match: boolean
@@ -154,7 +155,7 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
154155
if (cond.not) match = !match
155156

156157
if (cond.and) {
157-
const andFieldValue = values[cond.and.field]?.value
158+
const andFieldValue = values[cond.and.field]
158159
const andCondValue = cond.and.value
159160
let andMatch: boolean
160161
if (Array.isArray(andCondValue)) {
@@ -169,8 +170,9 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
169170
return match
170171
}
171172

172-
const condition = typeof config.required === 'function' ? config.required() : config.required
173-
return evalCond(condition, subBlockValues || {})
173+
const condition =
174+
typeof config.required === 'function' ? config.required(values) : config.required
175+
return evalCond(condition, values || {})
174176
}
175177

176178
/**
@@ -199,7 +201,7 @@ const getPreviewValue = (
199201
*
200202
* @param config - The sub-block configuration defining the label content
201203
* @param isValidJson - Whether the JSON content is valid (for code blocks)
202-
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
204+
* @param requiredValues - Flat sibling values for evaluating conditional requirements
203205
* @param wandState - State and handlers for the inline AI generate feature
204206
* @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle
205207
* @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled (includes dependsOn gating)
@@ -210,7 +212,7 @@ const getPreviewValue = (
210212
const renderLabel = (
211213
config: SubBlockConfig,
212214
isValidJson: boolean,
213-
subBlockValues?: Record<string, any>,
215+
requiredValues?: Record<string, any>,
214216
wandState?: {
215217
isSearchActive: boolean
216218
searchQuery: string
@@ -246,7 +248,7 @@ const renderLabel = (
246248
if (config.type === 'switch') return null
247249
if (!config.title) return null
248250

249-
const required = isFieldRequired(config, subBlockValues)
251+
const required = isFieldRequired(config, requiredValues)
250252
const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled
251253
const showCanonicalToggle = !!canonicalToggle && !wandState?.isPreview
252254
const showCopy = copyState?.showCopyButton && !wandState?.isPreview
@@ -617,6 +619,12 @@ function SubBlockComponent({
617619
| null
618620
| undefined
619621

622+
const requiredValues =
623+
!isPreview && dependencyContext
624+
? dependencyContext
625+
: subBlockValues
626+
? buildSubBlockValues(subBlockValues)
627+
: undefined
620628
const contextValues = dependencyContext ?? (isPreview ? subBlockValues : undefined)
621629

622630
const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {
@@ -956,7 +964,7 @@ function SubBlockComponent({
956964
blockId={blockId}
957965
subBlock={config}
958966
disabled={isDisabled}
959-
required={isFieldRequired(config, subBlockValues)}
967+
required={isFieldRequired(config, requiredValues)}
960968
isPreview={isPreview}
961969
previewValue={previewValue}
962970
/>
@@ -1248,7 +1256,7 @@ function SubBlockComponent({
12481256
{renderLabel(
12491257
config,
12501258
isValidJson,
1251-
subBlockValues,
1259+
requiredValues,
12521260
{
12531261
isSearchActive,
12541262
searchQuery,

0 commit comments

Comments
 (0)