Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/docs/content/docs/integrations/greenhouse.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Harvest v3 returns applications, attachments, education, and employment through

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.

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.

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.

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface ToolSubBlockRendererProps {
/** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */
toolType: string
toolParams: Record<string, string> | undefined
operation?: string
onParamChange: (toolIndex: number, paramId: string, value: string) => void
disabled: boolean
canonicalToggle?: {
Expand All @@ -44,6 +45,7 @@ export function ToolSubBlockRenderer({
effectiveParamId,
toolType,
toolParams,
operation,
onParamChange,
disabled,
canonicalToggle,
Expand Down Expand Up @@ -128,7 +130,7 @@ export function ToolSubBlockRenderer({
isPreview={false}
disabled={disabled}
canonicalToggle={canonicalToggle}
dependencyContext={toolParams}
dependencyContext={{ ...toolParams, operation }}
/>
</DependencyBlockTypeProvider>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getTransitiveSubBlockDependents } from '@/lib/workflows/subblocks/dependencies'
import type { StoredTool } from '@/lib/workflows/tool-input/types'
import { getBlock } from '@/blocks/registry'
import type { ToolMetadata } from '@/tools/metadata'
import { isUserFacingToolParam } from '@/tools/params'

/**
* Clear every TRANSITIVE `dependsOn` descendant of `changedParamId` in a nested tool's params,
Expand All @@ -23,3 +26,43 @@ export function clearDependentToolParams(
}
return next ?? params
}

/** Keeps user-configured inputs that the newly selected operation accepts. */
export function getParamsForToolOperation(
tool: Pick<StoredTool, 'type' | 'params'>,
nextTool: ToolMetadata
): Record<string, string> {
const newParamIds = new Set(
Object.entries(nextTool.params ?? {})
.filter(([, param]) => isUserFacingToolParam(param))
.map(([paramId]) => paramId)
)
const subBlocks = getBlock(tool.type)?.subBlocks ?? []
if (nextTool.oauth) {
const credentialControls = subBlocks.filter(
(subBlock) =>
subBlock.type === 'oauth-input' && subBlock.serviceId === nextTool.oauth?.provider
)
const credentialCanonicalIds = new Set(
credentialControls.flatMap((subBlock) =>
subBlock.canonicalParamId ? [subBlock.canonicalParamId] : []
)
)
for (const subBlock of subBlocks) {
if (
credentialControls.includes(subBlock) ||
(subBlock.canonicalParamId &&
credentialCanonicalIds.has(subBlock.canonicalParamId) &&
(!subBlock.serviceId || subBlock.serviceId === nextTool.oauth.provider))
) {
newParamIds.add(subBlock.id)
if (subBlock.canonicalParamId) newParamIds.add(subBlock.canonicalParamId)
}
}
}
const preservedParams: Record<string, string> = {}
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
if (newParamIds.has(paramId) && value) preservedParams[paramId] = value
})
return clearDependentToolParams(tool.type, preservedParams, 'operation')
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer'
import { ToolUsageControl } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control'
import { clearDependentToolParams } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
import {
clearDependentToolParams,
getParamsForToolOperation,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents'
import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types'
import {
isAgentToolBlock,
Expand Down Expand Up @@ -111,7 +114,6 @@ import {
formatParameterLabel,
getSubBlocksForToolInput,
getToolIdForOperation,
isUserFacingToolParam,
type SubBlocksForToolInput,
} from '@/tools/params'

Expand Down Expand Up @@ -979,18 +981,7 @@ export const ToolInput = memo(function ToolInput({
return
}

const newParamIds = new Set(
Object.entries(newToolConfig.params ?? {})
.filter(([, param]) => isUserFacingToolParam(param))
.map(([paramId]) => paramId)
)

const preservedParams: Record<string, string> = {}
Object.entries(tool.params || {}).forEach(([paramId, value]) => {
if (newParamIds.has(paramId) && value) {
preservedParams[paramId] = value
}
})
const preservedParams = getParamsForToolOperation(tool, newToolConfig)
Comment thread
waleedlatif1 marked this conversation as resolved.

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

const hasOperations =
!isCustomTool && !isMcpFamily && hasMultipleOperations(toolBlock ?? undefined)
const operationChoices = hasOperations
? getOperationChoices(toolBlock ?? undefined)
: null
const selectedOperation =
tool.operation ||
operationChoices?.options.find((option) => !operationChoices.denied.has(option.id))?.id

const currentToolId =
!isCustomTool && !isMcpFamily
? getToolIdForOperation(tool.type, tool.operation, toolBlock ?? undefined) ||
? getToolIdForOperation(tool.type, selectedOperation, toolBlock ?? undefined) ||
tool.toolId ||
''
: tool.toolId || ''
Expand All @@ -1555,8 +1555,8 @@ export const ToolInput = memo(function ToolInput({
currentToolId,
tool.type,
{
operation: tool.operation,
...tool.params,
operation: selectedOperation,
},
toolScopedOverrides,
toolBlock ?? undefined
Expand Down Expand Up @@ -1609,8 +1609,6 @@ export const ToolInput = memo(function ToolInput({
toolCredential?.type === sb.reactiveCondition.requiredType
)

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

Expand Down Expand Up @@ -1869,10 +1867,8 @@ export const ToolInput = memo(function ToolInput({
)}
{/* Operation dropdown for tools with multiple operations */}
{(() => {
if (!hasOperations) return null
const { options: operationOptions, denied } = getOperationChoices(
toolBlock ?? undefined
)
if (!operationChoices) return null
const { options: operationOptions, denied } = operationChoices
if (operationOptions.length === 0) return null

return (
Expand All @@ -1884,10 +1880,7 @@ export const ToolInput = memo(function ToolInput({
value: option.id,
hidden: denied.has(option.id),
}))}
value={
tool.operation ||
operationOptions.find((option) => !denied.has(option.id))?.id
}
value={selectedOperation}
onChange={(value) => handleOperationChange(toolIndex, value)}
placeholder='Select operation'
/* Denied operations only drop out once the config
Expand All @@ -1912,7 +1905,7 @@ export const ToolInput = memo(function ToolInput({
canonicalGroup && hasCanonicalPair
? resolveCanonicalMode(
canonicalGroup,
{ operation: tool.operation, ...tool.params },
{ ...tool.params, operation: selectedOperation },
toolScopedOverrides
)
: undefined
Expand Down Expand Up @@ -1949,6 +1942,7 @@ export const ToolInput = memo(function ToolInput({
effectiveParamId={effectiveParamId}
toolType={tool.type}
toolParams={tool.params}
operation={selectedOperation}
onParamChange={handleParamChange}
disabled={disabled}
canonicalToggle={canonicalToggleProp}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { isEqual } from 'es-toolkit'
import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants'
import type { FallbackModelEntry } from '@/lib/workflows/blocks/fallback-models'
import { buildSubBlockValues } from '@/lib/workflows/subblocks/visibility'
import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider'
import {
CheckboxList,
Expand Down Expand Up @@ -121,10 +122,10 @@ interface SubBlockProps {
* Supports boolean, condition objects, and functions that return conditions.
*
* @param config - The sub-block configuration containing requirement rules
* @param subBlockValues - Current values of all subblocks for condition evaluation
* @param values - Flat sibling values for condition evaluation
* @returns `true` if the field is required based on current context
*/
const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string, any>): boolean => {
const isFieldRequired = (config: SubBlockConfig, values?: Record<string, any>): boolean => {
if (!config.required) return false
if (typeof config.required === 'boolean') return config.required

Expand All @@ -141,7 +142,7 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
},
values: Record<string, any>
): boolean => {
const fieldValue = values[cond.field]?.value
const fieldValue = values[cond.field]
const condValue = cond.value

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

if (cond.and) {
const andFieldValue = values[cond.and.field]?.value
const andFieldValue = values[cond.and.field]
const andCondValue = cond.and.value
let andMatch: boolean
if (Array.isArray(andCondValue)) {
Expand All @@ -169,8 +170,9 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
return match
}

const condition = typeof config.required === 'function' ? config.required() : config.required
return evalCond(condition, subBlockValues || {})
const condition =
typeof config.required === 'function' ? config.required(values) : config.required
return evalCond(condition, values || {})
}

/**
Expand Down Expand Up @@ -199,7 +201,7 @@ const getPreviewValue = (
*
* @param config - The sub-block configuration defining the label content
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @param requiredValues - Flat sibling values for evaluating conditional requirements
* @param wandState - State and handlers for the inline AI generate feature
* @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle
* @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled (includes dependsOn gating)
Expand All @@ -210,7 +212,7 @@ const getPreviewValue = (
const renderLabel = (
config: SubBlockConfig,
isValidJson: boolean,
subBlockValues?: Record<string, any>,
requiredValues?: Record<string, any>,
wandState?: {
isSearchActive: boolean
searchQuery: string
Expand Down Expand Up @@ -246,7 +248,7 @@ const renderLabel = (
if (config.type === 'switch') return null
if (!config.title) return null

const required = isFieldRequired(config, subBlockValues)
const required = isFieldRequired(config, requiredValues)
const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled
const showCanonicalToggle = !!canonicalToggle && !wandState?.isPreview
const showCopy = copyState?.showCopyButton && !wandState?.isPreview
Expand Down Expand Up @@ -617,6 +619,12 @@ function SubBlockComponent({
| null
| undefined

const requiredValues =
!isPreview && dependencyContext
? dependencyContext
: subBlockValues
? buildSubBlockValues(subBlockValues)
: undefined
const contextValues = dependencyContext ?? (isPreview ? subBlockValues : undefined)

const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {
Expand Down Expand Up @@ -956,7 +964,7 @@ function SubBlockComponent({
blockId={blockId}
subBlock={config}
disabled={isDisabled}
required={isFieldRequired(config, subBlockValues)}
required={isFieldRequired(config, requiredValues)}
isPreview={isPreview}
previewValue={previewValue}
/>
Expand Down Expand Up @@ -1248,7 +1256,7 @@ function SubBlockComponent({
{renderLabel(
config,
isValidJson,
subBlockValues,
requiredValues,
{
isSearchActive,
searchQuery,
Expand Down
Loading
Loading