- {/* Execution Status & Duration Header */}
- {(executionData.status || executionData.durationMs !== undefined) && (
-
+ {/* Execution Status & Duration Header */}
+ {executionData && (executionData.status || executionData.durationMs !== undefined) && (
+
- ) : null}
+ )}
+
+ {/* Input Section - Collapsible */}
+ {executionData?.input !== undefined && (
+
+ )}
+
+ {/* Output Section - Collapsible, expanded by default */}
+ {executionData?.output !== undefined && (
+
+ )}
+
+ {/* Workflow Preview - only for workflow blocks with a selected child workflow */}
+ {isWorkflowBlock && childWorkflowId && (
+
+ )}
{/* Subblock Values - Using SubBlock components in preview mode */}
-
- {/* CSS override to show full opacity and prevent interaction instead of dimmed disabled state */}
-
+
+
{visibleSubBlocks.length > 0 ? (
{visibleSubBlocks.map((subBlockConfig, index) => (
@@ -1349,7 +1429,7 @@ function PreviewEditorContent({
)}
{/* Context Menu */}
-
+ /** Skips expensive subblock computations for thumbnails/template previews */
+ lightweight?: boolean
}
/**
@@ -157,21 +180,17 @@ function resolveToolsDisplay(
if (!tool || typeof tool !== 'object') return null
const t = tool as Record
- // Priority 1: Use tool.title if already populated
if (t.title && typeof t.title === 'string') return t.title
- // Priority 2: Extract from inline schema (legacy format)
const schema = t.schema as Record | undefined
if (schema?.function && typeof schema.function === 'object') {
const fn = schema.function as Record
if (fn.name && typeof fn.name === 'string') return fn.name
}
- // Priority 3: Extract from OpenAI function format
const fn = t.function as Record | undefined
if (fn?.name && typeof fn.name === 'string') return fn.name
- // Priority 4: Resolve built-in tool blocks from registry
if (
typeof t.type === 'string' &&
t.type !== 'custom-tool' &&
@@ -204,21 +223,16 @@ function resolveToolsDisplay(
* - Shows '-' for other selector types that need hydration
*/
function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) {
- // Mask password fields
const isPasswordField = subBlock?.password === true
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
- // Resolve various display names (synchronous access, matching WorkflowBlock priority)
const dropdownLabel = resolveDropdownLabel(subBlock, rawValue)
const variablesDisplay = resolveVariablesDisplay(subBlock, rawValue)
const toolsDisplay = resolveToolsDisplay(subBlock, rawValue)
const workflowName = resolveWorkflowName(subBlock, rawValue)
- // Check if this is a selector type that needs hydration (show '-' for raw IDs)
const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type)
- // Compute final display value matching WorkflowBlock logic
- // Priority order matches WorkflowBlock: masked > hydrated names > selector fallback > raw value
const hydratedName = dropdownLabel || variablesDisplay || toolsDisplay || workflowName
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)
@@ -258,6 +272,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
isPreviewSelected = false,
executionStatus,
subBlockValues,
+ lightweight = false,
} = data
const blockConfig = getBlock(type)
@@ -268,44 +283,68 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
)
const rawValues = useMemo(() => {
- if (!subBlockValues) return {}
+ if (lightweight || !subBlockValues) return {}
return Object.entries(subBlockValues).reduce>((acc, [key, entry]) => {
acc[key] = extractValue(entry)
return acc
}, {})
- }, [subBlockValues])
+ }, [subBlockValues, lightweight])
const visibleSubBlocks = useMemo(() => {
if (!blockConfig?.subBlocks) return []
- const isStarterOrTrigger =
- blockConfig.category === 'triggers' || type === 'starter' || isTrigger
+ const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers'
+ const effectiveTrigger = isTrigger || type === 'starter'
return blockConfig.subBlocks.filter((subBlock) => {
if (subBlock.hidden) return false
if (subBlock.hideFromPreview) return false
if (!isSubBlockFeatureEnabled(subBlock)) return false
- // Handle trigger mode visibility
- if (subBlock.mode === 'trigger' && !isStarterOrTrigger) return false
+ if (effectiveTrigger) {
+ const isValidTriggerSubblock = isPureTriggerBlock
+ ? subBlock.mode === 'trigger' || !subBlock.mode
+ : subBlock.mode === 'trigger'
+ if (!isValidTriggerSubblock) return false
+ } else {
+ if (subBlock.mode === 'trigger') return false
+ }
+
+ /** Skip value-dependent visibility checks in lightweight mode */
+ if (lightweight) return !subBlock.condition
- // Check advanced mode visibility
if (!isSubBlockVisibleForMode(subBlock, false, canonicalIndex, rawValues, undefined)) {
return false
}
-
- // Check condition visibility
if (!subBlock.condition) return true
return evaluateSubBlockCondition(subBlock.condition, rawValues)
})
- }, [blockConfig?.subBlocks, blockConfig?.category, type, isTrigger, canonicalIndex, rawValues])
+ }, [
+ lightweight,
+ blockConfig?.subBlocks,
+ blockConfig?.triggers?.enabled,
+ blockConfig?.category,
+ type,
+ isTrigger,
+ canonicalIndex,
+ rawValues,
+ ])
/**
- * Compute condition rows for condition blocks
+ * Compute condition rows for condition blocks.
+ * In lightweight mode, returns default structure without parsing values.
*/
const conditionRows = useMemo(() => {
if (type !== 'condition') return []
+ /** Default structure for lightweight mode or when no values */
+ const defaultRows = [
+ { id: 'if', title: 'if', value: '' },
+ { id: 'else', title: 'else', value: '' },
+ ]
+
+ if (lightweight) return defaultRows
+
const conditionsValue = rawValues.conditions
const raw = typeof conditionsValue === 'string' ? conditionsValue : undefined
@@ -325,21 +364,24 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
}
}
} catch {
- // Failed to parse, use fallback
+ /* empty */
}
- return [
- { id: 'if', title: 'if', value: '' },
- { id: 'else', title: 'else', value: '' },
- ]
- }, [type, rawValues])
+ return defaultRows
+ }, [type, rawValues, lightweight])
/**
- * Compute router rows for router_v2 blocks
+ * Compute router rows for router_v2 blocks.
+ * In lightweight mode, returns default structure without parsing values.
*/
const routerRows = useMemo(() => {
if (type !== 'router_v2') return []
+ /** Default structure for lightweight mode or when no values */
+ const defaultRows = [{ id: 'route1', value: '' }]
+
+ if (lightweight) return defaultRows
+
const routesValue = rawValues.routes
const raw = typeof routesValue === 'string' ? routesValue : undefined
@@ -357,11 +399,11 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
}
}
} catch {
- // Failed to parse, use fallback
+ /* empty */
}
- return [{ id: 'route1', value: '' }]
- }, [type, rawValues])
+ return defaultRows
+ }, [type, rawValues, lightweight])
if (!blockConfig) {
return null
@@ -379,9 +421,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
? routerRows.length > 0 || shouldShowDefaultHandles
: hasSubBlocks || shouldShowDefaultHandles
- const horizontalHandleClass = '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]'
- const verticalHandleClass = '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]'
-
const hasError = executionStatus === 'error'
const hasSuccess = executionStatus === 'success'
@@ -406,7 +445,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
type='target'
position={horizontalHandles ? Position.Left : Position.Top}
id='target'
- className={horizontalHandles ? horizontalHandleClass : verticalHandleClass}
+ className={horizontalHandles ? HANDLE_STYLES.horizontal : HANDLE_STYLES.vertical}
style={
horizontalHandles
? { left: '-7px', top: `${HANDLE_POSITIONS.DEFAULT_Y_OFFSET}px` }
@@ -439,36 +478,37 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
{hasContentBelowHeader && (
{type === 'condition' ? (
- // Condition block: render condition rows
conditionRows.map((cond) => (
-
+
))
) : type === 'router_v2' ? (
- // Router block: render context + route rows
<>
{routerRows.map((route, index) => (
))}
>
) : (
- // Standard blocks: render visible subblocks
visibleSubBlocks.map((subBlock) => {
- const rawValue = rawValues[subBlock.id]
+ const rawValue = lightweight ? undefined : rawValues[subBlock.id]
return (
)
@@ -479,27 +519,101 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
)}
- {/* Source handle */}
-
+ {/* Condition block handles */}
+ {type === 'condition' && (
+ <>
+ {conditionRows.map((cond, condIndex) => {
+ const topOffset =
+ HANDLE_POSITIONS.CONDITION_START_Y + condIndex * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT
+ return (
+
+ )
+ })}
+
+ >
+ )}
+
+ {/* Router block handles */}
+ {type === 'router_v2' && (
+ <>
+ {routerRows.map((route, routeIndex) => {
+ const topOffset =
+ HANDLE_POSITIONS.CONDITION_START_Y +
+ (routeIndex + 1) * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT
+ return (
+
+ )
+ })}
+
+ >
+ )}
+
+ {/* Source and error handles for non-condition/router blocks */}
+ {type !== 'condition' && type !== 'router_v2' && type !== 'response' && (
+ <>
+
+ {shouldShowDefaultHandles && (
+
+ )}
+ >
+ )}
)
}
+/**
+ * Custom comparison function for React.memo optimization.
+ * Uses fast-path primitive comparison before shallow comparing subBlockValues.
+ * @param prevProps - Previous render props
+ * @param nextProps - Next render props
+ * @returns True if render should be skipped (props are equal)
+ */
function shouldSkipPreviewBlockRender(
prevProps: NodeProps
,
nextProps: NodeProps
): boolean {
- // Check primitive props first (fast path)
if (
prevProps.id !== nextProps.id ||
prevProps.data.type !== nextProps.data.type ||
@@ -508,38 +622,41 @@ function shouldSkipPreviewBlockRender(
prevProps.data.horizontalHandles !== nextProps.data.horizontalHandles ||
prevProps.data.enabled !== nextProps.data.enabled ||
prevProps.data.isPreviewSelected !== nextProps.data.isPreviewSelected ||
- prevProps.data.executionStatus !== nextProps.data.executionStatus
+ prevProps.data.executionStatus !== nextProps.data.executionStatus ||
+ prevProps.data.lightweight !== nextProps.data.lightweight
) {
return false
}
- // Compare subBlockValues by reference first
+ /** Skip subBlockValues comparison in lightweight mode */
+ if (nextProps.data.lightweight) return true
+
const prevValues = prevProps.data.subBlockValues
const nextValues = nextProps.data.subBlockValues
- if (prevValues === nextValues) {
- return true
- }
+ if (prevValues === nextValues) return true
+ if (!prevValues || !nextValues) return false
- if (!prevValues || !nextValues) {
- return false
- }
-
- // Shallow compare keys and values
const prevKeys = Object.keys(prevValues)
const nextKeys = Object.keys(nextValues)
- if (prevKeys.length !== nextKeys.length) {
- return false
- }
+ if (prevKeys.length !== nextKeys.length) return false
for (const key of prevKeys) {
- if (prevValues[key] !== nextValues[key]) {
- return false
- }
+ if (prevValues[key] !== nextValues[key]) return false
}
return true
}
-export const WorkflowPreviewBlock = memo(WorkflowPreviewBlockInner, shouldSkipPreviewBlockRender)
+/**
+ * Preview block component for workflow visualization in readonly contexts.
+ * Optimized for rendering without hooks or store subscriptions.
+ *
+ * @remarks
+ * - Renders block header, subblock values, and connection handles
+ * - Supports condition, router, and standard block types
+ * - Shows error handles for non-trigger blocks
+ * - Displays execution status via colored ring overlays
+ */
+export const PreviewBlock = memo(WorkflowPreviewBlockInner, shouldSkipPreviewBlockRender)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/index.ts
new file mode 100644
index 000000000..bd6475e11
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/index.ts
@@ -0,0 +1 @@
+export { PreviewBlock } from './block'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/index.ts
new file mode 100644
index 000000000..ee99f14c6
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/index.ts
@@ -0,0 +1 @@
+export { PreviewSubflow } from './subflow'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx
similarity index 78%
rename from apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx
index a9aa913a2..848e93c7f 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx
@@ -5,6 +5,9 @@ import { RepeatIcon, SplitIcon } from 'lucide-react'
import { Handle, type NodeProps, Position } from 'reactflow'
import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
+/** Execution status for subflows in preview mode */
+type ExecutionStatus = 'success' | 'error' | 'not-executed'
+
interface WorkflowPreviewSubflowData {
name: string
width?: number
@@ -12,6 +15,10 @@ interface WorkflowPreviewSubflowData {
kind: 'loop' | 'parallel'
/** Whether this subflow is selected in preview mode */
isPreviewSelected?: boolean
+ /** Execution status for highlighting the subflow container */
+ executionStatus?: ExecutionStatus
+ /** Skips expensive computations for thumbnails/template previews (unused in subflow, for consistency) */
+ lightweight?: boolean
}
/**
@@ -20,7 +27,7 @@ interface WorkflowPreviewSubflowData {
* or interactive features.
*/
function WorkflowPreviewSubflowInner({ data }: NodeProps) {
- const { name, width = 500, height = 300, kind, isPreviewSelected = false } = data
+ const { name, width = 500, height = 300, kind, isPreviewSelected = false, executionStatus } = data
const isLoop = kind === 'loop'
const BlockIcon = isLoop ? RepeatIcon : SplitIcon
@@ -35,6 +42,9 @@ function WorkflowPreviewSubflowInner({ data }: NodeProps
- {/* Selection ring overlay */}
+ {/* Selection ring overlay (takes priority over execution rings) */}
{isPreviewSelected && (
)}
+ {/* Success ring overlay (only shown if not selected) */}
+ {!isPreviewSelected && hasSuccess && (
+
+ )}
+ {/* Error ring overlay (only shown if not selected) */}
+ {!isPreviewSelected && hasError && (
+
+ )}
{/* Target handle on left (input to the subflow) */}
+): { width: number; height: number } {
+ const childBlocks = Object.values(blocks).filter((block) => block?.data?.parentId === containerId)
+
+ if (childBlocks.length === 0) {
+ return {
+ width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
+ height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
+ }
+ }
+
+ let maxRight = 0
+ let maxBottom = 0
+
+ for (const child of childBlocks) {
+ if (!child?.position) continue
+
+ const { width: childWidth, height: childHeight } = getPreviewBlockDimensions(child)
+
+ maxRight = Math.max(maxRight, child.position.x + childWidth)
+ maxBottom = Math.max(maxBottom, child.position.y + childHeight)
+ }
+
+ const width = Math.max(
+ CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
+ maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
+ )
+ const height = Math.max(
+ CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
+ maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING
+ )
+
+ return { width, height }
+}
+
+/**
+ * Finds the leftmost block ID from a workflow state.
+ * Excludes subflow containers (loop/parallel) from consideration.
+ * @param workflowState - The workflow state to search
+ * @returns The ID of the leftmost block, or null if no blocks exist
+ */
+export function getLeftmostBlockId(workflowState: WorkflowState | null | undefined): string | null {
+ if (!workflowState?.blocks) return null
+
+ let leftmostId: string | null = null
+ let minX = Number.POSITIVE_INFINITY
+
+ for (const [blockId, block] of Object.entries(workflowState.blocks)) {
+ if (!block || block.type === 'loop' || block.type === 'parallel') continue
+ const x = block.position?.x ?? Number.POSITIVE_INFINITY
+ if (x < minX) {
+ minX = x
+ leftmostId = blockId
+ }
+ }
+
+ return leftmostId
+}
+
+/** Execution status for edges/nodes in the preview */
+type ExecutionStatus = 'success' | 'error' | 'not-executed'
+
+/** Calculates absolute position for blocks, handling nested subflows */
+function calculateAbsolutePosition(
+ block: BlockState,
+ blocks: Record
+): { x: number; y: number } {
+ if (!block.data?.parentId) {
+ return block.position
+ }
+
+ const parentBlock = blocks[block.data.parentId]
+ if (!parentBlock) {
+ logger.warn(`Parent block not found for child block`)
+ return block.position
+ }
+
+ const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks)
+ return {
+ x: parentAbsolutePosition.x + block.position.x,
+ y: parentAbsolutePosition.y + block.position.y,
+ }
+}
+
+interface PreviewWorkflowProps {
+ workflowState: WorkflowState
+ className?: string
+ height?: string | number
+ width?: string | number
+ isPannable?: boolean
+ defaultPosition?: { x: number; y: number }
+ defaultZoom?: number
+ fitPadding?: number
+ onNodeClick?: (blockId: string, mousePosition: { x: number; y: number }) => void
+ /** Callback when a node is right-clicked */
+ onNodeContextMenu?: (blockId: string, mousePosition: { x: number; y: number }) => void
+ /** Callback when the canvas (empty area) is clicked */
+ onPaneClick?: () => void
+ /** Cursor style to show when hovering the canvas */
+ cursorStyle?: 'default' | 'pointer' | 'grab'
+ /** Map of executed block IDs to their status for highlighting the execution path */
+ executedBlocks?: Record
+ /** Currently selected block ID for highlighting */
+ selectedBlockId?: string | null
+ /** Skips expensive subblock computations for thumbnails/template previews */
+ lightweight?: boolean
+}
+
+/**
+ * Preview node types using minimal components without hooks or store subscriptions.
+ * This prevents interaction issues while allowing canvas panning and node clicking.
+ */
+const previewNodeTypes: NodeTypes = {
+ workflowBlock: PreviewBlock,
+ noteBlock: PreviewBlock,
+ subflowNode: PreviewSubflow,
+}
+
+const edgeTypes: EdgeTypes = {
+ default: WorkflowEdge,
+ workflowEdge: WorkflowEdge,
+}
+
+interface FitViewOnChangeProps {
+ nodeIds: string
+ fitPadding: number
+ containerRef: React.RefObject
+}
+
+/**
+ * Helper component that calls fitView when the set of nodes changes or when the container resizes.
+ * Only triggers on actual node additions/removals, not on selection changes.
+ * Must be rendered inside ReactFlowProvider.
+ */
+function FitViewOnChange({ nodeIds, fitPadding, containerRef }: FitViewOnChangeProps) {
+ const { fitView } = useReactFlow()
+ const lastNodeIdsRef = useRef(null)
+
+ useEffect(() => {
+ if (!nodeIds.length) return
+ const shouldFit = lastNodeIdsRef.current !== nodeIds
+ if (!shouldFit) return
+ lastNodeIdsRef.current = nodeIds
+
+ const timeoutId = setTimeout(() => {
+ fitView({ padding: fitPadding, duration: 200 })
+ }, 50)
+ return () => clearTimeout(timeoutId)
+ }, [nodeIds, fitPadding, fitView])
+
+ useEffect(() => {
+ const container = containerRef.current
+ if (!container) return
+
+ let timeoutId: ReturnType | null = null
+
+ const resizeObserver = new ResizeObserver(() => {
+ if (timeoutId) clearTimeout(timeoutId)
+ timeoutId = setTimeout(() => {
+ fitView({ padding: fitPadding, duration: 150 })
+ }, 100)
+ })
+
+ resizeObserver.observe(container)
+ return () => {
+ if (timeoutId) clearTimeout(timeoutId)
+ resizeObserver.disconnect()
+ }
+ }, [containerRef, fitPadding, fitView])
+
+ return null
+}
+
+/**
+ * Readonly workflow component for visualizing workflow state.
+ * Renders blocks, subflows, and edges with execution status highlighting.
+ *
+ * @remarks
+ * - Supports panning and node click interactions
+ * - Shows execution path via green edges for successful paths
+ * - Error edges display red by default, green when error path was taken
+ * - Fits view automatically when nodes change or container resizes
+ */
+export function PreviewWorkflow({
+ workflowState,
+ className,
+ height = '100%',
+ width = '100%',
+ isPannable = true,
+ defaultPosition,
+ defaultZoom = 0.8,
+ fitPadding = 0.25,
+ onNodeClick,
+ onNodeContextMenu,
+ onPaneClick,
+ cursorStyle = 'grab',
+ executedBlocks,
+ selectedBlockId,
+ lightweight = false,
+}: PreviewWorkflowProps) {
+ const containerRef = useRef(null)
+ const nodeTypes = previewNodeTypes
+ const isValidWorkflowState = workflowState?.blocks && workflowState.edges
+
+ const blocksStructure = useMemo(() => {
+ if (!isValidWorkflowState) return { count: 0, ids: '' }
+ return {
+ count: Object.keys(workflowState.blocks || {}).length,
+ ids: Object.keys(workflowState.blocks || {}).join(','),
+ }
+ }, [workflowState.blocks, isValidWorkflowState])
+
+ const loopsStructure = useMemo(() => {
+ if (!isValidWorkflowState) return { count: 0, ids: '' }
+ return {
+ count: Object.keys(workflowState.loops || {}).length,
+ ids: Object.keys(workflowState.loops || {}).join(','),
+ }
+ }, [workflowState.loops, isValidWorkflowState])
+
+ const parallelsStructure = useMemo(() => {
+ if (!isValidWorkflowState) return { count: 0, ids: '' }
+ return {
+ count: Object.keys(workflowState.parallels || {}).length,
+ ids: Object.keys(workflowState.parallels || {}).join(','),
+ }
+ }, [workflowState.parallels, isValidWorkflowState])
+
+ /** Map of subflow ID to child block IDs */
+ const subflowChildrenMap = useMemo(() => {
+ if (!isValidWorkflowState) return new Map()
+
+ const map = new Map()
+ for (const [blockId, block] of Object.entries(workflowState.blocks || {})) {
+ const parentId = block?.data?.parentId
+ if (parentId) {
+ const children = map.get(parentId) || []
+ children.push(blockId)
+ map.set(parentId, children)
+ }
+ }
+ return map
+ }, [workflowState.blocks, isValidWorkflowState])
+
+ /** Derives subflow execution status from child blocks */
+ const getSubflowExecutionStatus = useMemo(() => {
+ return (subflowId: string): ExecutionStatus | undefined => {
+ if (!executedBlocks) return undefined
+
+ const childIds = subflowChildrenMap.get(subflowId)
+ if (!childIds?.length) return undefined
+
+ const childStatuses = childIds.map((id) => executedBlocks[id]).filter(Boolean)
+ if (childStatuses.length === 0) return undefined
+
+ if (childStatuses.some((s) => s.status === 'error')) return 'error'
+ if (childStatuses.some((s) => s.status === 'success')) return 'success'
+ return 'not-executed'
+ }
+ }, [executedBlocks, subflowChildrenMap])
+
+ /** Gets execution status for any block, deriving subflow status from children */
+ const getBlockExecutionStatus = useMemo(() => {
+ return (blockId: string): { status: string; executed: boolean } | undefined => {
+ if (!executedBlocks) return undefined
+
+ const directStatus = executedBlocks[blockId]
+ if (directStatus) {
+ return { status: directStatus.status, executed: true }
+ }
+
+ const block = workflowState.blocks?.[blockId]
+ if (block && (block.type === 'loop' || block.type === 'parallel')) {
+ const subflowStatus = getSubflowExecutionStatus(blockId)
+ if (subflowStatus) {
+ return { status: subflowStatus, executed: true }
+ }
+
+ const incomingEdge = workflowState.edges?.find((e) => e.target === blockId)
+ if (incomingEdge && executedBlocks[incomingEdge.source]?.status === 'success') {
+ return { status: 'not-executed', executed: true }
+ }
+ }
+
+ return undefined
+ }
+ }, [executedBlocks, workflowState.blocks, workflowState.edges, getSubflowExecutionStatus])
+
+ const edgesStructure = useMemo(() => {
+ if (!isValidWorkflowState) return { count: 0, ids: '' }
+ return {
+ count: workflowState.edges?.length || 0,
+ ids: workflowState.edges?.map((e) => e.id).join(',') || '',
+ }
+ }, [workflowState.edges, isValidWorkflowState])
+
+ const nodes: Node[] = useMemo(() => {
+ if (!isValidWorkflowState) return []
+
+ const nodeArray: Node[] = []
+
+ Object.entries(workflowState.blocks || {}).forEach(([blockId, block]) => {
+ if (!block || !block.type) {
+ logger.warn(`Skipping invalid block: ${blockId}`)
+ return
+ }
+
+ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks)
+
+ if (block.type === 'loop' || block.type === 'parallel') {
+ const isSelected = selectedBlockId === blockId
+ const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
+ const subflowExecutionStatus = getSubflowExecutionStatus(blockId)
+
+ nodeArray.push({
+ id: blockId,
+ type: 'subflowNode',
+ position: absolutePosition,
+ draggable: false,
+ data: {
+ name: block.name,
+ width: dimensions.width,
+ height: dimensions.height,
+ kind: block.type as 'loop' | 'parallel',
+ isPreviewSelected: isSelected,
+ executionStatus: subflowExecutionStatus,
+ lightweight,
+ },
+ })
+ return
+ }
+
+ const isSelected = selectedBlockId === blockId
+
+ let executionStatus: ExecutionStatus | undefined
+ if (executedBlocks) {
+ const blockExecution = executedBlocks[blockId]
+ if (blockExecution) {
+ if (blockExecution.status === 'error') {
+ executionStatus = 'error'
+ } else if (blockExecution.status === 'success') {
+ executionStatus = 'success'
+ } else {
+ executionStatus = 'not-executed'
+ }
+ } else {
+ executionStatus = 'not-executed'
+ }
+ }
+
+ nodeArray.push({
+ id: blockId,
+ type: 'workflowBlock',
+ position: absolutePosition,
+ draggable: false,
+ zIndex: block.data?.parentId ? 10 : undefined,
+ data: {
+ type: block.type,
+ name: block.name,
+ isTrigger: block.triggerMode === true,
+ horizontalHandles: block.horizontalHandles ?? false,
+ enabled: block.enabled ?? true,
+ isPreviewSelected: isSelected,
+ executionStatus,
+ subBlockValues: block.subBlocks,
+ lightweight,
+ },
+ })
+ })
+
+ return nodeArray
+ }, [
+ blocksStructure,
+ loopsStructure,
+ parallelsStructure,
+ workflowState.blocks,
+ isValidWorkflowState,
+ executedBlocks,
+ selectedBlockId,
+ getSubflowExecutionStatus,
+ lightweight,
+ ])
+
+ const edges: Edge[] = useMemo(() => {
+ if (!isValidWorkflowState) return []
+
+ /**
+ * Determines edge execution status for visualization.
+ * Error edges turn green when taken (source errored, target executed).
+ * Normal edges turn green when both source succeeded and target executed.
+ */
+ const getEdgeExecutionStatus = (edge: {
+ source: string
+ target: string
+ sourceHandle?: string | null
+ }): ExecutionStatus | undefined => {
+ if (!executedBlocks) return undefined
+
+ const sourceStatus = getBlockExecutionStatus(edge.source)
+ const targetStatus = getBlockExecutionStatus(edge.target)
+ const isErrorEdge = edge.sourceHandle === 'error'
+
+ if (isErrorEdge) {
+ return sourceStatus?.status === 'error' && targetStatus?.executed
+ ? 'success'
+ : 'not-executed'
+ }
+
+ const isSubflowStartEdge =
+ edge.sourceHandle === 'loop-start-source' || edge.sourceHandle === 'parallel-start-source'
+
+ if (isSubflowStartEdge) {
+ const incomingEdge = workflowState.edges?.find((e) => e.target === edge.source)
+ const incomingSucceeded = incomingEdge
+ ? executedBlocks[incomingEdge.source]?.status === 'success'
+ : false
+ return incomingSucceeded ? 'success' : 'not-executed'
+ }
+
+ const targetBlock = workflowState.blocks?.[edge.target]
+ const targetIsSubflow =
+ targetBlock && (targetBlock.type === 'loop' || targetBlock.type === 'parallel')
+
+ if (sourceStatus?.status === 'success' && (targetStatus?.executed || targetIsSubflow)) {
+ return 'success'
+ }
+
+ return 'not-executed'
+ }
+
+ return (workflowState.edges || []).map((edge) => {
+ const status = getEdgeExecutionStatus(edge)
+ const isErrorEdge = edge.sourceHandle === 'error'
+ return {
+ id: edge.id,
+ source: edge.source,
+ target: edge.target,
+ sourceHandle: edge.sourceHandle,
+ targetHandle: edge.targetHandle,
+ data: {
+ ...(status ? { executionStatus: status } : {}),
+ sourceHandle: edge.sourceHandle,
+ },
+ zIndex: status === 'success' ? 10 : isErrorEdge ? 5 : 0,
+ }
+ })
+ }, [
+ edgesStructure,
+ workflowState.edges,
+ workflowState.blocks,
+ isValidWorkflowState,
+ executedBlocks,
+ getBlockExecutionStatus,
+ ])
+
+ if (!isValidWorkflowState) {
+ return (
+
+
+
⚠️ Logged State Not Found
+
+ This log was migrated from the old system and doesn't contain workflow state data.
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ {
+ logger.debug('Node clicked:', { nodeId: node.id, event })
+ onNodeClick(node.id, { x: event.clientX, y: event.clientY })
+ }
+ : undefined
+ }
+ onNodeContextMenu={
+ onNodeContextMenu
+ ? (event, node) => {
+ event.preventDefault()
+ event.stopPropagation()
+ onNodeContextMenu(node.id, { x: event.clientX, y: event.clientY })
+ }
+ : undefined
+ }
+ onPaneClick={onPaneClick}
+ />
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts
index d64f8979c..2abd6a9be 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts
@@ -1,2 +1,6 @@
+export { PreviewContextMenu } from './components/preview-context-menu'
export { PreviewEditor } from './components/preview-editor'
-export { getLeftmostBlockId, WorkflowPreview } from './preview'
+export { getLeftmostBlockId, PreviewWorkflow } from './components/preview-workflow'
+export { PreviewBlock } from './components/preview-workflow/components/block'
+export { PreviewSubflow } from './components/preview-workflow/components/subflow'
+export { buildBlockExecutions, Preview } from './preview'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
index a9d56c794..8780c6317 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
@@ -1,499 +1,292 @@
'use client'
-import { useEffect, useMemo, useRef } from 'react'
-import ReactFlow, {
- ConnectionLineType,
- type Edge,
- type EdgeTypes,
- type Node,
- type NodeTypes,
- ReactFlowProvider,
- useReactFlow,
-} from 'reactflow'
-import 'reactflow/dist/style.css'
-
-import { createLogger } from '@sim/logger'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { ArrowLeft } from 'lucide-react'
+import { Button, Tooltip } from '@/components/emcn'
+import { redactApiKeys } from '@/lib/core/security/redaction'
import { cn } from '@/lib/core/utils/cn'
-import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
-import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
-import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
-import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/block'
-import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/subflow'
-import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
+import { PreviewEditor } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor'
+import {
+ getLeftmostBlockId,
+ PreviewWorkflow,
+} from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow'
+import type { WorkflowState } from '@/stores/workflows/workflow/types'
-const logger = createLogger('WorkflowPreview')
-
-/**
- * Gets block dimensions for preview purposes.
- * For containers, uses stored dimensions or defaults.
- * For regular blocks, uses stored height or estimates based on type.
- */
-function getPreviewBlockDimensions(block: BlockState): { width: number; height: number } {
- if (block.type === 'loop' || block.type === 'parallel') {
- return {
- width: block.data?.width
- ? Math.max(block.data.width, CONTAINER_DIMENSIONS.MIN_WIDTH)
- : CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
- height: block.data?.height
- ? Math.max(block.data.height, CONTAINER_DIMENSIONS.MIN_HEIGHT)
- : CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
- }
- }
-
- if (block.height) {
- return {
- width: BLOCK_DIMENSIONS.FIXED_WIDTH,
- height: Math.max(block.height, BLOCK_DIMENSIONS.MIN_HEIGHT),
- }
- }
-
- return estimateBlockDimensions(block.type)
+interface TraceSpan {
+ blockId?: string
+ input?: unknown
+ output?: unknown
+ status?: string
+ duration?: number
+ children?: TraceSpan[]
}
-/**
- * Calculates container dimensions based on child block positions and sizes.
- * Mirrors the logic from useNodeUtilities.calculateLoopDimensions.
- */
-function calculateContainerDimensions(
- containerId: string,
- blocks: Record
-): { width: number; height: number } {
- const childBlocks = Object.values(blocks).filter((block) => block?.data?.parentId === containerId)
-
- if (childBlocks.length === 0) {
- return {
- width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
- height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
- }
- }
-
- let maxRight = 0
- let maxBottom = 0
-
- for (const child of childBlocks) {
- if (!child?.position) continue
-
- const { width: childWidth, height: childHeight } = getPreviewBlockDimensions(child)
-
- maxRight = Math.max(maxRight, child.position.x + childWidth)
- maxBottom = Math.max(maxBottom, child.position.y + childHeight)
- }
-
- const width = Math.max(
- CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
- maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
- )
- const height = Math.max(
- CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
- maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING
- )
-
- return { width, height }
+interface BlockExecutionData {
+ input: unknown
+ output: unknown
+ status: string
+ durationMs: number
+ /** Child trace spans for nested workflow blocks */
+ children?: TraceSpan[]
}
-/**
- * Finds the leftmost block ID from a workflow state.
- * Returns the block with the smallest x position, excluding subflow containers (loop/parallel).
- */
-export function getLeftmostBlockId(workflowState: WorkflowState | null | undefined): string | null {
- if (!workflowState?.blocks) return null
-
- let leftmostId: string | null = null
- let minX = Number.POSITIVE_INFINITY
-
- for (const [blockId, block] of Object.entries(workflowState.blocks)) {
- if (!block || block.type === 'loop' || block.type === 'parallel') continue
- const x = block.position?.x ?? Number.POSITIVE_INFINITY
- if (x < minX) {
- minX = x
- leftmostId = blockId
- }
- }
-
- return leftmostId
-}
-
-/** Execution status for edges/nodes in the preview */
-type ExecutionStatus = 'success' | 'error' | 'not-executed'
-
-interface WorkflowPreviewProps {
+/** Represents a level in the workflow navigation stack */
+interface WorkflowStackEntry {
workflowState: WorkflowState
- className?: string
- height?: string | number
- width?: string | number
- isPannable?: boolean
- defaultPosition?: { x: number; y: number }
- defaultZoom?: number
- fitPadding?: number
- onNodeClick?: (blockId: string, mousePosition: { x: number; y: number }) => void
- /** Callback when a node is right-clicked */
- onNodeContextMenu?: (blockId: string, mousePosition: { x: number; y: number }) => void
- /** Callback when the canvas (empty area) is clicked */
- onPaneClick?: () => void
- /** Cursor style to show when hovering the canvas */
- cursorStyle?: 'default' | 'pointer' | 'grab'
- /** Map of executed block IDs to their status for highlighting the execution path */
- executedBlocks?: Record
- /** Currently selected block ID for highlighting */
- selectedBlockId?: string | null
+ traceSpans: TraceSpan[]
+ blockExecutions: Record
}
/**
- * Preview node types using minimal components without hooks or store subscriptions.
- * This prevents interaction issues while allowing canvas panning and node clicking.
+ * Extracts child trace spans from a workflow block's execution data.
+ * Checks both the `children` property (where trace span processing moves them)
+ * and the legacy `output.childTraceSpans` for compatibility.
*/
-const previewNodeTypes: NodeTypes = {
- workflowBlock: WorkflowPreviewBlock,
- noteBlock: WorkflowPreviewBlock,
- subflowNode: WorkflowPreviewSubflow,
-}
+function extractChildTraceSpans(blockExecution: BlockExecutionData | undefined): TraceSpan[] {
+ if (!blockExecution) return []
-// Define edge types
-const edgeTypes: EdgeTypes = {
- default: WorkflowEdge,
- workflowEdge: WorkflowEdge, // Keep for backward compatibility
-}
+ if (Array.isArray(blockExecution.children) && blockExecution.children.length > 0) {
+ return blockExecution.children
+ }
-interface FitViewOnChangeProps {
- nodeIds: string
- fitPadding: number
- containerRef: React.RefObject
-}
-
-/**
- * Helper component that calls fitView when the set of nodes changes or when the container resizes.
- * Only triggers on actual node additions/removals, not on selection changes.
- * Must be rendered inside ReactFlowProvider.
- */
-function FitViewOnChange({ nodeIds, fitPadding, containerRef }: FitViewOnChangeProps) {
- const { fitView } = useReactFlow()
- const lastNodeIdsRef = useRef(null)
-
- // Fit view when nodes change
- useEffect(() => {
- if (!nodeIds.length) return
- const shouldFit = lastNodeIdsRef.current !== nodeIds
- if (!shouldFit) return
- lastNodeIdsRef.current = nodeIds
-
- const timeoutId = setTimeout(() => {
- fitView({ padding: fitPadding, duration: 200 })
- }, 50)
- return () => clearTimeout(timeoutId)
- }, [nodeIds, fitPadding, fitView])
-
- // Fit view when container resizes (debounced to avoid excessive calls during drag)
- useEffect(() => {
- const container = containerRef.current
- if (!container) return
-
- let timeoutId: ReturnType | null = null
-
- const resizeObserver = new ResizeObserver(() => {
- if (timeoutId) clearTimeout(timeoutId)
- timeoutId = setTimeout(() => {
- fitView({ padding: fitPadding, duration: 150 })
- }, 100)
- })
-
- resizeObserver.observe(container)
- return () => {
- if (timeoutId) clearTimeout(timeoutId)
- resizeObserver.disconnect()
+ if (blockExecution.output && typeof blockExecution.output === 'object') {
+ const output = blockExecution.output as Record
+ if (Array.isArray(output.childTraceSpans)) {
+ return output.childTraceSpans as TraceSpan[]
}
- }, [containerRef, fitPadding, fitView])
+ }
- return null
+ return []
}
-export function WorkflowPreview({
- workflowState,
+/**
+ * Builds block execution data from trace spans
+ */
+export function buildBlockExecutions(spans: TraceSpan[]): Record {
+ const blockExecutionMap: Record = {}
+
+ const collectBlockSpans = (traceSpans: TraceSpan[]): TraceSpan[] => {
+ const blockSpans: TraceSpan[] = []
+ for (const span of traceSpans) {
+ if (span.blockId) {
+ blockSpans.push(span)
+ }
+ if (span.children && Array.isArray(span.children)) {
+ blockSpans.push(...collectBlockSpans(span.children))
+ }
+ }
+ return blockSpans
+ }
+
+ const allBlockSpans = collectBlockSpans(spans)
+
+ for (const span of allBlockSpans) {
+ if (span.blockId && !blockExecutionMap[span.blockId]) {
+ blockExecutionMap[span.blockId] = {
+ input: redactApiKeys(span.input || {}),
+ output: redactApiKeys(span.output || {}),
+ status: span.status || 'unknown',
+ durationMs: span.duration || 0,
+ children: span.children,
+ }
+ }
+ }
+
+ return blockExecutionMap
+}
+
+interface PreviewProps {
+ /** The workflow state to display */
+ workflowState: WorkflowState
+ /** Trace spans for the execution (optional - enables execution mode features) */
+ traceSpans?: TraceSpan[]
+ /** Pre-computed block executions (optional - will be built from traceSpans if not provided) */
+ blockExecutions?: Record
+ /** Additional CSS class names */
+ className?: string
+ /** Height of the component */
+ height?: string | number
+ /** Width of the component */
+ width?: string | number
+ /** Callback when canvas context menu is opened */
+ onCanvasContextMenu?: (e: React.MouseEvent) => void
+ /** Callback when a node context menu is opened */
+ onNodeContextMenu?: (blockId: string, mousePosition: { x: number; y: number }) => void
+ /** Whether to show border around the component */
+ showBorder?: boolean
+ /** Initial block to select (defaults to leftmost block) */
+ initialSelectedBlockId?: string | null
+ /** Whether to auto-select the leftmost block on mount */
+ autoSelectLeftmost?: boolean
+}
+
+/**
+ * Main preview component that combines PreviewCanvas with PreviewEditor
+ * and handles nested workflow navigation via a stack.
+ *
+ * @remarks
+ * - Manages navigation stack for drilling into nested workflow blocks
+ * - Displays back button when viewing nested workflows
+ * - Properly passes execution data through to nested levels
+ * - Can be used anywhere a workflow preview with editor is needed
+ */
+export function Preview({
+ workflowState: rootWorkflowState,
+ traceSpans: rootTraceSpans,
+ blockExecutions: providedBlockExecutions,
className,
height = '100%',
width = '100%',
- isPannable = true,
- defaultPosition,
- defaultZoom = 0.8,
- fitPadding = 0.25,
- onNodeClick,
+ onCanvasContextMenu,
onNodeContextMenu,
- onPaneClick,
- cursorStyle = 'grab',
- executedBlocks,
- selectedBlockId,
-}: WorkflowPreviewProps) {
- const containerRef = useRef(null)
- const nodeTypes = previewNodeTypes
- const isValidWorkflowState = workflowState?.blocks && workflowState.edges
-
- const blocksStructure = useMemo(() => {
- if (!isValidWorkflowState) return { count: 0, ids: '' }
- return {
- count: Object.keys(workflowState.blocks || {}).length,
- ids: Object.keys(workflowState.blocks || {}).join(','),
+ showBorder = false,
+ initialSelectedBlockId,
+ autoSelectLeftmost = true,
+}: PreviewProps) {
+ /** Initialize pinnedBlockId synchronously to ensure sidebar is present from first render */
+ const [pinnedBlockId, setPinnedBlockId] = useState(() => {
+ if (initialSelectedBlockId) return initialSelectedBlockId
+ if (autoSelectLeftmost) {
+ return getLeftmostBlockId(rootWorkflowState)
}
- }, [workflowState.blocks, isValidWorkflowState])
+ return null
+ })
- const loopsStructure = useMemo(() => {
- if (!isValidWorkflowState) return { count: 0, ids: '' }
- return {
- count: Object.keys(workflowState.loops || {}).length,
- ids: Object.keys(workflowState.loops || {}).join(','),
+ /** Stack for nested workflow navigation. Empty means we're at the root level. */
+ const [workflowStack, setWorkflowStack] = useState([])
+
+ /** Block executions for the root level */
+ const rootBlockExecutions = useMemo(() => {
+ if (providedBlockExecutions) return providedBlockExecutions
+ if (!rootTraceSpans || !Array.isArray(rootTraceSpans)) return {}
+ return buildBlockExecutions(rootTraceSpans)
+ }, [providedBlockExecutions, rootTraceSpans])
+
+ /** Current block executions - either from stack or root */
+ const blockExecutions = useMemo(() => {
+ if (workflowStack.length > 0) {
+ return workflowStack[workflowStack.length - 1].blockExecutions
}
- }, [workflowState.loops, isValidWorkflowState])
+ return rootBlockExecutions
+ }, [workflowStack, rootBlockExecutions])
- const parallelsStructure = useMemo(() => {
- if (!isValidWorkflowState) return { count: 0, ids: '' }
- return {
- count: Object.keys(workflowState.parallels || {}).length,
- ids: Object.keys(workflowState.parallels || {}).join(','),
+ /** Current workflow state - either from stack or root */
+ const workflowState = useMemo(() => {
+ if (workflowStack.length > 0) {
+ return workflowStack[workflowStack.length - 1].workflowState
}
- }, [workflowState.parallels, isValidWorkflowState])
+ return rootWorkflowState
+ }, [workflowStack, rootWorkflowState])
- const edgesStructure = useMemo(() => {
- if (!isValidWorkflowState) return { count: 0, ids: '' }
- return {
- count: workflowState.edges?.length || 0,
- ids: workflowState.edges?.map((e) => e.id).join(',') || '',
- }
- }, [workflowState.edges, isValidWorkflowState])
+ /** Whether we're in execution mode (have trace spans/block executions) */
+ const isExecutionMode = useMemo(() => {
+ return Object.keys(blockExecutions).length > 0
+ }, [blockExecutions])
- const calculateAbsolutePosition = (
- block: any,
- blocks: Record
- ): { x: number; y: number } => {
- if (!block.data?.parentId) {
- return block.position
- }
+ /** Handler to drill down into a nested workflow block */
+ const handleDrillDown = useCallback(
+ (blockId: string, childWorkflowState: WorkflowState) => {
+ const blockExecution = blockExecutions[blockId]
+ const childTraceSpans = extractChildTraceSpans(blockExecution)
+ const childBlockExecutions = buildBlockExecutions(childTraceSpans)
- const parentBlock = blocks[block.data.parentId]
- if (!parentBlock) {
- logger.warn(`Parent block not found for child block: ${block.id}`)
- return block.position
- }
-
- const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks)
-
- return {
- x: parentAbsolutePosition.x + block.position.x,
- y: parentAbsolutePosition.y + block.position.y,
- }
- }
-
- const nodes: Node[] = useMemo(() => {
- if (!isValidWorkflowState) return []
-
- const nodeArray: Node[] = []
-
- Object.entries(workflowState.blocks || {}).forEach(([blockId, block]) => {
- if (!block || !block.type) {
- logger.warn(`Skipping invalid block: ${blockId}`)
- return
- }
-
- const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks)
-
- // Handle loop/parallel containers
- if (block.type === 'loop' || block.type === 'parallel') {
- const isSelected = selectedBlockId === blockId
- const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
- nodeArray.push({
- id: blockId,
- type: 'subflowNode',
- position: absolutePosition,
- draggable: false,
- data: {
- name: block.name,
- width: dimensions.width,
- height: dimensions.height,
- kind: block.type as 'loop' | 'parallel',
- isPreviewSelected: isSelected,
- },
- })
- return
- }
-
- // Handle regular blocks
- const isSelected = selectedBlockId === blockId
-
- let executionStatus: ExecutionStatus | undefined
- if (executedBlocks) {
- const blockExecution = executedBlocks[blockId]
- if (blockExecution) {
- if (blockExecution.status === 'error') {
- executionStatus = 'error'
- } else if (blockExecution.status === 'success') {
- executionStatus = 'success'
- } else {
- executionStatus = 'not-executed'
- }
- } else {
- executionStatus = 'not-executed'
- }
- }
-
- nodeArray.push({
- id: blockId,
- type: 'workflowBlock',
- position: absolutePosition,
- draggable: false,
- // Blocks inside subflows need higher z-index to appear above the container
- zIndex: block.data?.parentId ? 10 : undefined,
- data: {
- type: block.type,
- name: block.name,
- isTrigger: block.triggerMode === true,
- horizontalHandles: block.horizontalHandles ?? false,
- enabled: block.enabled ?? true,
- isPreviewSelected: isSelected,
- executionStatus,
- subBlockValues: block.subBlocks,
+ setWorkflowStack((prev) => [
+ ...prev,
+ {
+ workflowState: childWorkflowState,
+ traceSpans: childTraceSpans,
+ blockExecutions: childBlockExecutions,
},
- })
- })
+ ])
- return nodeArray
- }, [
- blocksStructure,
- loopsStructure,
- parallelsStructure,
- workflowState.blocks,
- isValidWorkflowState,
- executedBlocks,
- selectedBlockId,
- ])
+ /** Set pinned block synchronously to avoid double fitView from sidebar resize */
+ const leftmostId = getLeftmostBlockId(childWorkflowState)
+ setPinnedBlockId(leftmostId)
+ },
+ [blockExecutions]
+ )
- const edges: Edge[] = useMemo(() => {
- if (!isValidWorkflowState) return []
+ /** Handler to go back up the stack */
+ const handleGoBack = useCallback(() => {
+ setWorkflowStack((prev) => prev.slice(0, -1))
+ setPinnedBlockId(null)
+ }, [])
- return (workflowState.edges || []).map((edge) => {
- let executionStatus: ExecutionStatus | undefined
- if (executedBlocks) {
- const sourceExecuted = executedBlocks[edge.source]
- const targetExecuted = executedBlocks[edge.target]
+ /** Handlers for node interactions - memoized to prevent unnecessary re-renders */
+ const handleNodeClick = useCallback((blockId: string) => {
+ setPinnedBlockId(blockId)
+ }, [])
- if (sourceExecuted && targetExecuted) {
- // Edge is success if source succeeded and target was executed (even if target errored)
- if (sourceExecuted.status === 'success') {
- executionStatus = 'success'
- } else {
- executionStatus = 'not-executed'
- }
- } else {
- executionStatus = 'not-executed'
- }
- }
+ const handlePaneClick = useCallback(() => {
+ setPinnedBlockId(null)
+ }, [])
- return {
- id: edge.id,
- source: edge.source,
- target: edge.target,
- sourceHandle: edge.sourceHandle,
- targetHandle: edge.targetHandle,
- data: executionStatus ? { executionStatus } : undefined,
- // Raise executed edges above default edges
- zIndex: executionStatus === 'success' ? 10 : 0,
- }
- })
- }, [edgesStructure, workflowState.edges, isValidWorkflowState, executedBlocks])
+ const handleEditorClose = useCallback(() => {
+ setPinnedBlockId(null)
+ }, [])
- if (!isValidWorkflowState) {
- return (
-
-
-
⚠️ Logged State Not Found
-
- This log was migrated from the old system and doesn't contain workflow state data.
-
-
-
- )
- }
+ useEffect(() => {
+ setWorkflowStack([])
+ }, [rootWorkflowState])
+
+ const isNested = workflowStack.length > 0
return (
-
-
-
-
{
- logger.debug('Node clicked:', { nodeId: node.id, event })
- onNodeClick(node.id, { x: event.clientX, y: event.clientY })
- }
- : undefined
- }
- onNodeContextMenu={
- onNodeContextMenu
- ? (event, node) => {
- event.preventDefault()
- event.stopPropagation()
- onNodeContextMenu(node.id, { x: event.clientX, y: event.clientY })
- }
- : undefined
- }
- onPaneClick={onPaneClick}
- />
-
+
-
+
+ {pinnedBlockId && workflowState.blocks[pinnedBlockId] && (
+
+ )}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx
index 53fdf533d..bf52b1c0b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx
@@ -185,6 +185,16 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
return `claude mcp add "${safeName}" --url "${mcpServerUrl}" --header "X-API-Key:$SIM_API_KEY"`
}
+ // Cursor supports direct URL configuration (no mcp-remote needed)
+ if (client === 'cursor') {
+ const cursorConfig = isPublic
+ ? { url: mcpServerUrl }
+ : { url: mcpServerUrl, headers: { 'X-API-Key': '$SIM_API_KEY' } }
+
+ return JSON.stringify({ mcpServers: { [safeName]: cursorConfig } }, null, 2)
+ }
+
+ // Claude Desktop and VS Code still use mcp-remote (stdio transport)
const mcpRemoteArgs = isPublic
? ['-y', 'mcp-remote', mcpServerUrl]
: ['-y', 'mcp-remote', mcpServerUrl, '--header', 'X-API-Key:$SIM_API_KEY']
@@ -265,14 +275,8 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
.replace(/[^a-z0-9-]/g, '')
const config = isPublic
- ? {
- command: 'npx',
- args: ['-y', 'mcp-remote', mcpServerUrl],
- }
- : {
- command: 'npx',
- args: ['-y', 'mcp-remote', mcpServerUrl, '--header', 'X-API-Key:$SIM_API_KEY'],
- }
+ ? { url: mcpServerUrl }
+ : { url: mcpServerUrl, headers: { 'X-API-Key': '$SIM_API_KEY' } }
const base64Config = btoa(JSON.stringify(config))
return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(safeName)}&config=${encodeURIComponent(base64Config)}`
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
index f862a1290..d2a72a998 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
@@ -158,7 +158,7 @@ const allNavigationItems: NavigationItem[] = [
{ id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' },
{ id: 'environment', label: 'Environment', icon: FolderCode, section: 'system' },
{ id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' },
- { id: 'workflow-mcp-servers', label: 'Deployed MCPs', icon: Server, section: 'system' },
+ { id: 'workflow-mcp-servers', label: 'MCP Servers', icon: Server, section: 'system' },
{
id: 'byok',
label: 'BYOK',
diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts
index 9af1c7fcc..6b4fd0efb 100644
--- a/apps/sim/blocks/blocks/agent.ts
+++ b/apps/sim/blocks/blocks/agent.ts
@@ -513,6 +513,12 @@ Return ONLY the JSON array.`,
})(),
}),
},
+ {
+ id: 'maxTokens',
+ title: 'Max Output Tokens',
+ type: 'short-input',
+ placeholder: 'Enter max tokens (e.g., 4096)...',
+ },
{
id: 'responseFormat',
title: 'Response Format',
@@ -754,6 +760,7 @@ Example 3 (Array Input):
},
},
temperature: { type: 'number', description: 'Response randomness level' },
+ maxTokens: { type: 'number', description: 'Maximum number of tokens in the response' },
reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' },
verbosity: { type: 'string', description: 'Verbosity level for GPT-5 models' },
thinkingLevel: { type: 'string', description: 'Thinking level for Gemini 3 models' },
diff --git a/apps/sim/blocks/blocks/intercom.ts b/apps/sim/blocks/blocks/intercom.ts
index efbc8e876..6919679b6 100644
--- a/apps/sim/blocks/blocks/intercom.ts
+++ b/apps/sim/blocks/blocks/intercom.ts
@@ -36,7 +36,22 @@ export const IntercomBlock: BlockConfig = {
{ label: 'Search Conversations', id: 'search_conversations' },
{ label: 'Create Ticket', id: 'create_ticket' },
{ label: 'Get Ticket', id: 'get_ticket' },
+ { label: 'Update Ticket', id: 'update_ticket' },
{ label: 'Create Message', id: 'create_message' },
+ { label: 'List Admins', id: 'list_admins' },
+ { label: 'Close Conversation', id: 'close_conversation' },
+ { label: 'Open Conversation', id: 'open_conversation' },
+ { label: 'Snooze Conversation', id: 'snooze_conversation' },
+ { label: 'Assign Conversation', id: 'assign_conversation' },
+ { label: 'List Tags', id: 'list_tags' },
+ { label: 'Create Tag', id: 'create_tag' },
+ { label: 'Tag Contact', id: 'tag_contact' },
+ { label: 'Untag Contact', id: 'untag_contact' },
+ { label: 'Tag Conversation', id: 'tag_conversation' },
+ { label: 'Create Note', id: 'create_note' },
+ { label: 'Create Event', id: 'create_event' },
+ { label: 'Attach Contact to Company', id: 'attach_contact_to_company' },
+ { label: 'Detach Contact from Company', id: 'detach_contact_from_company' },
],
value: () => 'create_contact',
},
@@ -384,7 +399,15 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
required: true,
condition: {
field: 'operation',
- value: ['get_conversation', 'reply_conversation'],
+ value: [
+ 'get_conversation',
+ 'reply_conversation',
+ 'close_conversation',
+ 'open_conversation',
+ 'snooze_conversation',
+ 'assign_conversation',
+ 'tag_conversation',
+ ],
},
},
{
@@ -477,11 +500,20 @@ Return ONLY the message text - no explanations.`,
id: 'admin_id',
title: 'Admin ID',
type: 'short-input',
- placeholder: 'ID of the admin sending the message',
+ placeholder: 'ID of the admin performing the action',
required: true,
condition: {
field: 'operation',
- value: ['reply_conversation'],
+ value: [
+ 'reply_conversation',
+ 'close_conversation',
+ 'open_conversation',
+ 'snooze_conversation',
+ 'assign_conversation',
+ 'tag_conversation',
+ 'create_note',
+ 'update_ticket',
+ ],
},
},
{
@@ -526,7 +558,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
required: true,
condition: {
field: 'operation',
- value: ['get_ticket'],
+ value: ['get_ticket', 'update_ticket'],
},
},
{
@@ -799,6 +831,307 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
value: ['list_companies'],
},
},
+ // Close/Open conversation body
+ {
+ id: 'close_body',
+ title: 'Closing Message',
+ type: 'long-input',
+ placeholder: 'Optional message to add when closing',
+ condition: {
+ field: 'operation',
+ value: ['close_conversation'],
+ },
+ },
+ // Snooze conversation
+ {
+ id: 'snoozed_until',
+ title: 'Snooze Until',
+ type: 'short-input',
+ placeholder: 'Unix timestamp when conversation should reopen',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['snooze_conversation'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a Unix timestamp in seconds based on the user's description.
+The timestamp should be a Unix epoch time in seconds (10 digits).
+Examples:
+- "tomorrow" -> Tomorrow at 09:00:00 as Unix timestamp
+- "in 2 hours" -> Current time plus 7200 seconds
+- "next Monday" -> Next Monday at 09:00:00 as Unix timestamp
+
+Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
+ placeholder: 'Describe when to unsnooze (e.g., "tomorrow", "in 2 hours")...',
+ generationType: 'timestamp',
+ },
+ },
+ // Assign conversation
+ {
+ id: 'assignee_id',
+ title: 'Assignee ID',
+ type: 'short-input',
+ placeholder: 'Admin or team ID to assign to (0 to unassign)',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['assign_conversation'],
+ },
+ },
+ {
+ id: 'assign_body',
+ title: 'Assignment Message',
+ type: 'long-input',
+ placeholder: 'Optional message when assigning',
+ condition: {
+ field: 'operation',
+ value: ['assign_conversation'],
+ },
+ },
+ // Update ticket fields
+ {
+ id: 'update_ticket_attributes',
+ title: 'Ticket Attributes',
+ type: 'long-input',
+ placeholder: 'JSON object with ticket attributes to update',
+ condition: {
+ field: 'operation',
+ value: ['update_ticket'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a JSON object for Intercom ticket attributes based on the user's description.
+Example: {"_default_title_": "Updated title", "_default_description_": "Updated description"}
+
+Return ONLY the JSON object - no explanations or markdown formatting.`,
+ placeholder: 'Describe the ticket updates (e.g., "change title to Bug Fixed")...',
+ generationType: 'json-object',
+ },
+ },
+ {
+ id: 'ticket_open',
+ title: 'Ticket Open',
+ type: 'dropdown',
+ options: [
+ { label: 'Keep Open', id: 'true' },
+ { label: 'Close Ticket', id: 'false' },
+ ],
+ condition: {
+ field: 'operation',
+ value: ['update_ticket'],
+ },
+ },
+ {
+ id: 'ticket_is_shared',
+ title: 'Ticket Visible to Users',
+ type: 'dropdown',
+ options: [
+ { label: 'Yes', id: 'true' },
+ { label: 'No', id: 'false' },
+ ],
+ condition: {
+ field: 'operation',
+ value: ['update_ticket'],
+ },
+ },
+ {
+ id: 'ticket_snoozed_until',
+ title: 'Snooze Ticket Until',
+ type: 'short-input',
+ placeholder: 'Unix timestamp when ticket should reopen',
+ condition: {
+ field: 'operation',
+ value: ['update_ticket'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a Unix timestamp in seconds based on the user's description.
+Examples:
+- "tomorrow" -> Tomorrow at 09:00:00 as Unix timestamp
+- "next week" -> 7 days from now
+
+Return ONLY the numeric timestamp.`,
+ placeholder: 'Describe when to unsnooze (e.g., "tomorrow")...',
+ generationType: 'timestamp',
+ },
+ },
+ {
+ id: 'ticket_assignee_id',
+ title: 'Ticket Assignee ID',
+ type: 'short-input',
+ placeholder: 'Admin or team ID to assign to (0 to unassign)',
+ condition: {
+ field: 'operation',
+ value: ['update_ticket'],
+ },
+ },
+ // Tag fields
+ {
+ id: 'tagId',
+ title: 'Tag ID',
+ type: 'short-input',
+ placeholder: 'ID of the tag',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['tag_contact', 'untag_contact', 'tag_conversation'],
+ },
+ },
+ {
+ id: 'tag_name',
+ title: 'Tag Name',
+ type: 'short-input',
+ placeholder: 'Name of the tag to create',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['create_tag'],
+ },
+ },
+ {
+ id: 'tag_id_update',
+ title: 'Tag ID (for update)',
+ type: 'short-input',
+ placeholder: 'ID of existing tag to update (leave empty to create new)',
+ condition: {
+ field: 'operation',
+ value: ['create_tag'],
+ },
+ },
+ // Contact ID for tag/untag/note operations
+ {
+ id: 'tag_contact_id',
+ title: 'Contact ID',
+ type: 'short-input',
+ placeholder: 'ID of the contact',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: [
+ 'tag_contact',
+ 'untag_contact',
+ 'create_note',
+ 'attach_contact_to_company',
+ 'detach_contact_from_company',
+ ],
+ },
+ },
+ // Note fields
+ {
+ id: 'note_body',
+ title: 'Note Content',
+ type: 'long-input',
+ placeholder: 'Text content of the note',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['create_note'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a note for Intercom based on the user's description.
+The note should be clear, professional, and capture the key information.
+
+Return ONLY the note text - no explanations.`,
+ placeholder: 'Describe the note content (e.g., "customer requested callback")...',
+ },
+ },
+ // Event fields
+ {
+ id: 'event_name',
+ title: 'Event Name',
+ type: 'short-input',
+ placeholder: 'Event name (e.g., order-completed)',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ },
+ {
+ id: 'event_user_id',
+ title: 'User ID',
+ type: 'short-input',
+ placeholder: 'Your identifier for the user',
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ },
+ {
+ id: 'event_email',
+ title: 'User Email',
+ type: 'short-input',
+ placeholder: 'Email address of the user',
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ },
+ {
+ id: 'event_contact_id',
+ title: 'Contact ID',
+ type: 'short-input',
+ placeholder: 'Intercom contact ID',
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ },
+ {
+ id: 'event_metadata',
+ title: 'Event Metadata',
+ type: 'long-input',
+ placeholder: 'JSON object with event metadata (max 10 keys)',
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a JSON object for Intercom event metadata based on the user's description.
+The object should contain key-value pairs (max 10 keys).
+Example: {"order_value": 99.99, "items": 3, "coupon_used": true}
+
+Return ONLY the JSON object - no explanations or markdown formatting.`,
+ placeholder: 'Describe the event data (e.g., "order value $50, 2 items")...',
+ generationType: 'json-object',
+ },
+ },
+ {
+ id: 'event_created_at',
+ title: 'Event Time',
+ type: 'short-input',
+ placeholder: 'Unix timestamp when event occurred',
+ condition: {
+ field: 'operation',
+ value: ['create_event'],
+ },
+ wandConfig: {
+ enabled: true,
+ prompt: `Generate a Unix timestamp in seconds based on the user's description.
+Examples:
+- "now" -> Current Unix timestamp
+- "5 minutes ago" -> Current time minus 300 seconds
+
+Return ONLY the numeric timestamp.`,
+ placeholder: 'Describe when the event occurred (e.g., "now")...',
+ generationType: 'timestamp',
+ },
+ },
+ // Company attachment fields
+ {
+ id: 'attach_company_id',
+ title: 'Company ID',
+ type: 'short-input',
+ placeholder: 'ID of the company to attach/detach',
+ required: true,
+ condition: {
+ field: 'operation',
+ value: ['attach_contact_to_company', 'detach_contact_from_company'],
+ },
+ },
],
tools: {
access: [
@@ -818,6 +1151,21 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
'intercom_create_ticket',
'intercom_get_ticket',
'intercom_create_message',
+ 'intercom_update_ticket_v2',
+ 'intercom_list_admins_v2',
+ 'intercom_close_conversation_v2',
+ 'intercom_open_conversation_v2',
+ 'intercom_snooze_conversation_v2',
+ 'intercom_assign_conversation_v2',
+ 'intercom_list_tags_v2',
+ 'intercom_create_tag_v2',
+ 'intercom_tag_contact_v2',
+ 'intercom_untag_contact_v2',
+ 'intercom_tag_conversation_v2',
+ 'intercom_create_note_v2',
+ 'intercom_create_event_v2',
+ 'intercom_attach_contact_to_company_v2',
+ 'intercom_detach_contact_from_company_v2',
],
config: {
tool: (params) => {
@@ -854,6 +1202,36 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
return 'intercom_get_ticket'
case 'create_message':
return 'intercom_create_message'
+ case 'update_ticket':
+ return 'intercom_update_ticket_v2'
+ case 'list_admins':
+ return 'intercom_list_admins_v2'
+ case 'close_conversation':
+ return 'intercom_close_conversation_v2'
+ case 'open_conversation':
+ return 'intercom_open_conversation_v2'
+ case 'snooze_conversation':
+ return 'intercom_snooze_conversation_v2'
+ case 'assign_conversation':
+ return 'intercom_assign_conversation_v2'
+ case 'list_tags':
+ return 'intercom_list_tags_v2'
+ case 'create_tag':
+ return 'intercom_create_tag_v2'
+ case 'tag_contact':
+ return 'intercom_tag_contact_v2'
+ case 'untag_contact':
+ return 'intercom_untag_contact_v2'
+ case 'tag_conversation':
+ return 'intercom_tag_conversation_v2'
+ case 'create_note':
+ return 'intercom_create_note_v2'
+ case 'create_event':
+ return 'intercom_create_event_v2'
+ case 'attach_contact_to_company':
+ return 'intercom_attach_contact_to_company_v2'
+ case 'detach_contact_from_company':
+ return 'intercom_detach_contact_from_company_v2'
default:
throw new Error(`Unknown operation: ${params.operation}`)
}
@@ -870,6 +1248,23 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
message_created_at,
include_translations,
disable_notifications,
+ close_body,
+ assign_body,
+ tag_contact_id,
+ attach_company_id,
+ update_ticket_attributes,
+ ticket_open,
+ ticket_is_shared,
+ ticket_snoozed_until,
+ ticket_assignee_id,
+ tag_name,
+ tag_id_update,
+ note_body,
+ event_user_id,
+ event_email,
+ event_contact_id,
+ event_metadata,
+ event_created_at,
...rest
} = params
const cleanParams: Record
= {}
@@ -897,7 +1292,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
cleanParams.created_at = Number(reply_created_at)
}
- // Map ticket fields
+ // Map ticket fields for create_ticket
if (operation === 'create_ticket') {
if (ticket_company_id) cleanParams.company_id = ticket_company_id
if (ticket_created_at) cleanParams.created_at = Number(ticket_created_at)
@@ -920,6 +1315,71 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
cleanParams.include_translations = include_translations === 'true'
}
+ // Map close_body to body for close_conversation
+ if (operation === 'close_conversation' && close_body) {
+ cleanParams.body = close_body
+ }
+
+ // Map assign_body to body for assign_conversation
+ if (operation === 'assign_conversation' && assign_body) {
+ cleanParams.body = assign_body
+ }
+
+ // Map tag_contact_id to contactId for tag/note/company attachment operations
+ if (
+ [
+ 'tag_contact',
+ 'untag_contact',
+ 'create_note',
+ 'attach_contact_to_company',
+ 'detach_contact_from_company',
+ ].includes(operation) &&
+ tag_contact_id
+ ) {
+ cleanParams.contactId = tag_contact_id
+ }
+
+ // Map attach_company_id to companyId for company attachment operations
+ if (
+ ['attach_contact_to_company', 'detach_contact_from_company'].includes(operation) &&
+ attach_company_id
+ ) {
+ cleanParams.companyId = attach_company_id
+ }
+
+ // Map update_ticket fields
+ if (operation === 'update_ticket') {
+ if (update_ticket_attributes) cleanParams.ticket_attributes = update_ticket_attributes
+ if (ticket_open !== undefined && ticket_open !== '') {
+ cleanParams.open = ticket_open === 'true'
+ }
+ if (ticket_is_shared !== undefined && ticket_is_shared !== '') {
+ cleanParams.is_shared = ticket_is_shared === 'true'
+ }
+ if (ticket_snoozed_until) cleanParams.snoozed_until = Number(ticket_snoozed_until)
+ if (ticket_assignee_id) cleanParams.assignee_id = ticket_assignee_id
+ }
+
+ // Map tag fields for create_tag
+ if (operation === 'create_tag') {
+ if (tag_name) cleanParams.name = tag_name
+ if (tag_id_update) cleanParams.id = tag_id_update
+ }
+
+ // Map note_body to body for create_note
+ if (operation === 'create_note' && note_body) {
+ cleanParams.body = note_body
+ }
+
+ // Map event fields for create_event
+ if (operation === 'create_event') {
+ if (event_user_id) cleanParams.user_id = event_user_id
+ if (event_email) cleanParams.email = event_email
+ if (event_contact_id) cleanParams.id = event_contact_id
+ if (event_metadata) cleanParams.metadata = event_metadata
+ if (event_created_at) cleanParams.created_at = Number(event_created_at)
+ }
+
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
cleanParams[key] = value
@@ -963,7 +1423,22 @@ export const IntercomV2Block: BlockConfig = {
'intercom_search_conversations_v2',
'intercom_create_ticket_v2',
'intercom_get_ticket_v2',
+ 'intercom_update_ticket_v2',
'intercom_create_message_v2',
+ 'intercom_list_admins_v2',
+ 'intercom_close_conversation_v2',
+ 'intercom_open_conversation_v2',
+ 'intercom_snooze_conversation_v2',
+ 'intercom_assign_conversation_v2',
+ 'intercom_list_tags_v2',
+ 'intercom_create_tag_v2',
+ 'intercom_tag_contact_v2',
+ 'intercom_untag_contact_v2',
+ 'intercom_tag_conversation_v2',
+ 'intercom_create_note_v2',
+ 'intercom_create_event_v2',
+ 'intercom_attach_contact_to_company_v2',
+ 'intercom_detach_contact_from_company_v2',
],
config: {
tool: createVersionedToolSelector({
@@ -999,8 +1474,38 @@ export const IntercomV2Block: BlockConfig = {
return 'intercom_create_ticket'
case 'get_ticket':
return 'intercom_get_ticket'
+ case 'update_ticket':
+ return 'intercom_update_ticket'
case 'create_message':
return 'intercom_create_message'
+ case 'list_admins':
+ return 'intercom_list_admins'
+ case 'close_conversation':
+ return 'intercom_close_conversation'
+ case 'open_conversation':
+ return 'intercom_open_conversation'
+ case 'snooze_conversation':
+ return 'intercom_snooze_conversation'
+ case 'assign_conversation':
+ return 'intercom_assign_conversation'
+ case 'list_tags':
+ return 'intercom_list_tags'
+ case 'create_tag':
+ return 'intercom_create_tag'
+ case 'tag_contact':
+ return 'intercom_tag_contact'
+ case 'untag_contact':
+ return 'intercom_untag_contact'
+ case 'tag_conversation':
+ return 'intercom_tag_conversation'
+ case 'create_note':
+ return 'intercom_create_note'
+ case 'create_event':
+ return 'intercom_create_event'
+ case 'attach_contact_to_company':
+ return 'intercom_attach_contact_to_company'
+ case 'detach_contact_from_company':
+ return 'intercom_detach_contact_from_company'
default:
return 'intercom_create_contact'
}
@@ -1008,7 +1513,158 @@ export const IntercomV2Block: BlockConfig = {
suffix: '_v2',
fallbackToolId: 'intercom_create_contact_v2',
}),
- params: IntercomBlock.tools!.config!.params,
+ params: (params) => {
+ const {
+ operation,
+ message_type_msg,
+ company_name,
+ contact_company_id,
+ reply_created_at,
+ ticket_company_id,
+ ticket_created_at,
+ message_created_at,
+ include_translations,
+ disable_notifications,
+ close_body,
+ assign_body,
+ tag_contact_id,
+ attach_company_id,
+ update_ticket_attributes,
+ ticket_open,
+ ticket_is_shared,
+ ticket_snoozed_until,
+ ticket_assignee_id,
+ tag_name,
+ tag_id_update,
+ note_body,
+ event_user_id,
+ event_email,
+ event_contact_id,
+ event_metadata,
+ event_created_at,
+ ...rest
+ } = params
+ const cleanParams: Record = {}
+
+ // Special mapping for message_type in create_message
+ if (operation === 'create_message' && message_type_msg) {
+ cleanParams.message_type = message_type_msg
+ }
+
+ // Special mapping for company name
+ if (operation === 'create_company' && company_name) {
+ cleanParams.name = company_name
+ }
+
+ // Map contact_company_id to company_id for contact operations
+ if (
+ (operation === 'create_contact' || operation === 'update_contact') &&
+ contact_company_id
+ ) {
+ cleanParams.company_id = contact_company_id
+ }
+
+ // Map reply_created_at to created_at for reply_conversation
+ if (operation === 'reply_conversation' && reply_created_at) {
+ cleanParams.created_at = Number(reply_created_at)
+ }
+
+ // Map ticket fields for create_ticket
+ if (operation === 'create_ticket') {
+ if (ticket_company_id) cleanParams.company_id = ticket_company_id
+ if (ticket_created_at) cleanParams.created_at = Number(ticket_created_at)
+ if (disable_notifications !== undefined && disable_notifications !== '') {
+ cleanParams.disable_notifications = disable_notifications === 'true'
+ }
+ }
+
+ // Map message_created_at to created_at for create_message
+ if (operation === 'create_message' && message_created_at) {
+ cleanParams.created_at = Number(message_created_at)
+ }
+
+ // Convert include_translations string to boolean for get_conversation
+ if (
+ operation === 'get_conversation' &&
+ include_translations !== undefined &&
+ include_translations !== ''
+ ) {
+ cleanParams.include_translations = include_translations === 'true'
+ }
+
+ // Map close_body to body for close_conversation
+ if (operation === 'close_conversation' && close_body) {
+ cleanParams.body = close_body
+ }
+
+ // Map assign_body to body for assign_conversation
+ if (operation === 'assign_conversation' && assign_body) {
+ cleanParams.body = assign_body
+ }
+
+ // Map tag_contact_id to contactId for tag/note/company attachment operations
+ if (
+ [
+ 'tag_contact',
+ 'untag_contact',
+ 'create_note',
+ 'attach_contact_to_company',
+ 'detach_contact_from_company',
+ ].includes(operation) &&
+ tag_contact_id
+ ) {
+ cleanParams.contactId = tag_contact_id
+ }
+
+ // Map attach_company_id to companyId for company attachment operations
+ if (
+ ['attach_contact_to_company', 'detach_contact_from_company'].includes(operation) &&
+ attach_company_id
+ ) {
+ cleanParams.companyId = attach_company_id
+ }
+
+ // Map update_ticket fields
+ if (operation === 'update_ticket') {
+ if (update_ticket_attributes) cleanParams.ticket_attributes = update_ticket_attributes
+ if (ticket_open !== undefined && ticket_open !== '') {
+ cleanParams.open = ticket_open === 'true'
+ }
+ if (ticket_is_shared !== undefined && ticket_is_shared !== '') {
+ cleanParams.is_shared = ticket_is_shared === 'true'
+ }
+ if (ticket_snoozed_until) cleanParams.snoozed_until = Number(ticket_snoozed_until)
+ if (ticket_assignee_id) cleanParams.assignee_id = ticket_assignee_id
+ }
+
+ // Map tag fields for create_tag
+ if (operation === 'create_tag') {
+ if (tag_name) cleanParams.name = tag_name
+ if (tag_id_update) cleanParams.id = tag_id_update
+ }
+
+ // Map note_body to body for create_note
+ if (operation === 'create_note' && note_body) {
+ cleanParams.body = note_body
+ }
+
+ // Map event fields for create_event
+ if (operation === 'create_event') {
+ if (event_user_id) cleanParams.user_id = event_user_id
+ if (event_email) cleanParams.email = event_email
+ if (event_contact_id) cleanParams.id = event_contact_id
+ if (event_metadata) cleanParams.metadata = event_metadata
+ if (event_created_at) cleanParams.created_at = Number(event_created_at)
+ }
+
+ Object.entries(rest).forEach(([key, value]) => {
+ if (value !== undefined && value !== null && value !== '') {
+ cleanParams[key] = value
+ }
+ })
+
+ return cleanParams
+ },
},
},
outputs: {
@@ -1031,10 +1687,23 @@ export const IntercomV2Block: BlockConfig = {
type: 'array',
description: 'Array of conversations (for list/search operations)',
},
+ state: { type: 'string', description: 'Conversation state (for close/open/snooze operations)' },
ticket: { type: 'json', description: 'Ticket object with id, ticket_id, ticket_state' },
- ticketId: { type: 'string', description: 'ID of the ticket (for create operations)' },
+ ticketId: { type: 'string', description: 'ID of the ticket (for create/update operations)' },
+ ticket_state: { type: 'string', description: 'Ticket state (for update_ticket operation)' },
message: { type: 'json', description: 'Message object with id, type' },
messageId: { type: 'string', description: 'ID of the message (for create operations)' },
+ admins: { type: 'array', description: 'Array of admin objects (for list_admins operation)' },
+ tags: { type: 'array', description: 'Array of tag objects (for list_tags operation)' },
+ tag: { type: 'json', description: 'Tag object with id and name (for tag operations)' },
+ tagId: { type: 'string', description: 'ID of the tag (for create_tag operation)' },
+ note: { type: 'json', description: 'Note object with id and body (for create_note operation)' },
+ noteId: { type: 'string', description: 'ID of the note (for create_note operation)' },
+ event_name: {
+ type: 'string',
+ description: 'Name of the tracked event (for create_event operation)',
+ },
+ name: { type: 'string', description: 'Name of the resource (for various operations)' },
total_count: { type: 'number', description: 'Total count (for list/search operations)' },
pages: { type: 'json', description: 'Pagination info with page, per_page, total_pages' },
id: { type: 'string', description: 'ID of the deleted item (for delete operations)' },
diff --git a/apps/sim/blocks/blocks/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts
index a594e61a3..d075867ce 100644
--- a/apps/sim/blocks/blocks/kalshi.ts
+++ b/apps/sim/blocks/blocks/kalshi.ts
@@ -1,16 +1,18 @@
import { KalshiIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
+import { createVersionedToolSelector } from '@/blocks/utils'
export const KalshiBlock: BlockConfig = {
type: 'kalshi',
- name: 'Kalshi',
+ name: 'Kalshi (Legacy)',
description: 'Access prediction markets and trade on Kalshi',
longDescription:
'Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, exchange status, and place/cancel/amend trades.',
docsLink: 'https://docs.sim.ai/tools/kalshi',
authMode: AuthMode.ApiKey,
category: 'tools',
+ hideFromToolbar: true,
bgColor: '#09C285',
icon: KalshiIcon,
subBlocks: [
@@ -349,8 +351,14 @@ Return ONLY the numeric timestamp (seconds since Unix epoch) - no explanations,
id: 'count',
title: 'Contracts',
type: 'short-input',
- placeholder: 'Number of contracts',
- required: true,
+ placeholder: 'Number of contracts (or use countFp)',
+ condition: { field: 'operation', value: ['create_order'] },
+ },
+ {
+ id: 'countFp',
+ title: 'Contracts (Fixed-Point)',
+ type: 'short-input',
+ placeholder: 'Fixed-point count (e.g., "10.50")',
condition: { field: 'operation', value: ['create_order'] },
},
{
@@ -674,3 +682,143 @@ Return ONLY the numeric timestamp (seconds since Unix epoch) - no explanations,
paging: { type: 'json', description: 'Pagination cursor for fetching more results' },
},
}
+
+export const KalshiV2Block: BlockConfig = {
+ ...KalshiBlock,
+ type: 'kalshi_v2',
+ name: 'Kalshi',
+ description: 'Access prediction markets and trade on Kalshi',
+ longDescription:
+ 'Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, exchange status, and place/cancel/amend trades.',
+ hideFromToolbar: false,
+ tools: {
+ ...KalshiBlock.tools,
+ access: [
+ 'kalshi_get_markets_v2',
+ 'kalshi_get_market_v2',
+ 'kalshi_get_events_v2',
+ 'kalshi_get_event_v2',
+ 'kalshi_get_balance_v2',
+ 'kalshi_get_positions_v2',
+ 'kalshi_get_orders_v2',
+ 'kalshi_get_order_v2',
+ 'kalshi_get_orderbook_v2',
+ 'kalshi_get_trades_v2',
+ 'kalshi_get_candlesticks_v2',
+ 'kalshi_get_fills_v2',
+ 'kalshi_get_series_by_ticker_v2',
+ 'kalshi_get_exchange_status_v2',
+ 'kalshi_create_order_v2',
+ 'kalshi_cancel_order_v2',
+ 'kalshi_amend_order_v2',
+ ],
+ config: {
+ ...KalshiBlock.tools!.config,
+ tool: createVersionedToolSelector({
+ baseToolSelector: (params) => {
+ switch (params.operation) {
+ case 'get_markets':
+ return 'kalshi_get_markets'
+ case 'get_market':
+ return 'kalshi_get_market'
+ case 'get_events':
+ return 'kalshi_get_events'
+ case 'get_event':
+ return 'kalshi_get_event'
+ case 'get_balance':
+ return 'kalshi_get_balance'
+ case 'get_positions':
+ return 'kalshi_get_positions'
+ case 'get_orders':
+ return 'kalshi_get_orders'
+ case 'get_order':
+ return 'kalshi_get_order'
+ case 'get_orderbook':
+ return 'kalshi_get_orderbook'
+ case 'get_trades':
+ return 'kalshi_get_trades'
+ case 'get_candlesticks':
+ return 'kalshi_get_candlesticks'
+ case 'get_fills':
+ return 'kalshi_get_fills'
+ case 'get_series_by_ticker':
+ return 'kalshi_get_series_by_ticker'
+ case 'get_exchange_status':
+ return 'kalshi_get_exchange_status'
+ case 'create_order':
+ return 'kalshi_create_order'
+ case 'cancel_order':
+ return 'kalshi_cancel_order'
+ case 'amend_order':
+ return 'kalshi_amend_order'
+ default:
+ return 'kalshi_get_markets'
+ }
+ },
+ suffix: '_v2',
+ fallbackToolId: 'kalshi_get_markets_v2',
+ }),
+ },
+ },
+ outputs: {
+ // List operations (V2 uses snake_case and flat cursor)
+ markets: { type: 'json', description: 'Array of market objects (get_markets)' },
+ events: { type: 'json', description: 'Array of event objects (get_events)' },
+ orders: { type: 'json', description: 'Array of order objects (get_orders)' },
+ market_positions: {
+ type: 'json',
+ description: 'Array of market position objects (get_positions)',
+ },
+ event_positions: {
+ type: 'json',
+ description: 'Array of event position objects (get_positions)',
+ },
+ fills: { type: 'json', description: 'Array of fill objects (get_fills)' },
+ trades: { type: 'json', description: 'Array of trade objects (get_trades)' },
+ candlesticks: {
+ type: 'json',
+ description: 'Array of candlestick data with yes_bid/yes_ask/price nested objects',
+ },
+ milestones: {
+ type: 'json',
+ description: 'Array of milestone objects (get_events with milestones)',
+ },
+ // Single item operations
+ market: { type: 'json', description: 'Single market object (get_market)' },
+ event: { type: 'json', description: 'Single event object (get_event)' },
+ order: {
+ type: 'json',
+ description: 'Order object with _dollars and _fp fields (get_order, create_order, etc.)',
+ },
+ series: { type: 'json', description: 'Series object (get_series_by_ticker)' },
+ // Account operations
+ balance: { type: 'number', description: 'Account balance in cents (get_balance)' },
+ portfolio_value: { type: 'number', description: 'Portfolio value in cents (get_balance)' },
+ updated_ts: { type: 'number', description: 'Unix timestamp of last update (get_balance)' },
+ // Orderbook (V2 uses tuple arrays)
+ orderbook: {
+ type: 'json',
+ description: 'Orderbook with yes/no/yes_dollars/no_dollars tuple arrays',
+ },
+ orderbook_fp: {
+ type: 'json',
+ description: 'Fixed-point orderbook with yes_dollars/no_dollars tuple arrays',
+ },
+ // Exchange status
+ exchange_status: {
+ type: 'string',
+ description: 'Exchange status string (get_exchange_status)',
+ },
+ trading_active: { type: 'boolean', description: 'Trading active flag (get_exchange_status)' },
+ // Cancel order specific
+ reduced_by: { type: 'number', description: 'Number of contracts reduced (cancel_order)' },
+ reduced_by_fp: {
+ type: 'string',
+ description: 'Contracts reduced in fixed-point (cancel_order)',
+ },
+ // Candlesticks ticker
+ ticker: { type: 'string', description: 'Market ticker (get_candlesticks)' },
+ // Pagination (flat cursor instead of nested paging object)
+ cursor: { type: 'string', description: 'Pagination cursor for fetching more results' },
+ },
+}
diff --git a/apps/sim/blocks/blocks/polymarket.ts b/apps/sim/blocks/blocks/polymarket.ts
index 5ecfdb02d..b953fc44a 100644
--- a/apps/sim/blocks/blocks/polymarket.ts
+++ b/apps/sim/blocks/blocks/polymarket.ts
@@ -6,7 +6,7 @@ export const PolymarketBlock: BlockConfig = {
name: 'Polymarket',
description: 'Access prediction markets data from Polymarket',
longDescription:
- 'Integrate Polymarket prediction markets into the workflow. Can get markets, market, events, event, tags, series, orderbook, price, midpoint, price history, last trade price, spread, tick size, positions, trades, and search.',
+ 'Integrate Polymarket prediction markets into the workflow. Can get markets, market, events, event, tags, series, orderbook, price, midpoint, price history, last trade price, spread, tick size, positions, trades, activity, leaderboard, holders, and search.',
docsLink: 'https://docs.sim.ai/tools/polymarket',
category: 'tools',
bgColor: '#4C82FB',
@@ -34,6 +34,9 @@ export const PolymarketBlock: BlockConfig = {
{ label: 'Get Tick Size', id: 'get_tick_size' },
{ label: 'Get Positions', id: 'get_positions' },
{ label: 'Get Trades', id: 'get_trades' },
+ { label: 'Get Activity', id: 'get_activity' },
+ { label: 'Get Leaderboard', id: 'get_leaderboard' },
+ { label: 'Get Market Holders', id: 'get_holders' },
],
value: () => 'get_markets',
},
@@ -101,14 +104,281 @@ export const PolymarketBlock: BlockConfig = {
placeholder: 'Wallet address (optional filter)',
condition: { field: 'operation', value: ['get_trades'] },
},
- // Market filter for positions and trades
+ // Market/Event filter for positions and trades
{
id: 'market',
- title: 'Market ID',
+ title: 'Condition ID',
type: 'short-input',
- placeholder: 'Market ID (optional filter)',
+ placeholder: 'Condition ID filter (comma-separated)',
condition: { field: 'operation', value: ['get_positions', 'get_trades'] },
},
+ {
+ id: 'positionEventId',
+ title: 'Event ID',
+ type: 'short-input',
+ placeholder: 'Event ID filter (alternative to Condition ID)',
+ condition: { field: 'operation', value: ['get_positions', 'get_trades'] },
+ },
+ // Positions-specific filters
+ {
+ id: 'sizeThreshold',
+ title: 'Size Threshold',
+ type: 'short-input',
+ placeholder: 'Minimum position size (default: 1)',
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ {
+ id: 'redeemable',
+ title: 'Redeemable',
+ type: 'dropdown',
+ options: [
+ { label: 'All', id: '' },
+ { label: 'Redeemable Only', id: 'true' },
+ { label: 'Non-Redeemable Only', id: 'false' },
+ ],
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ {
+ id: 'mergeable',
+ title: 'Mergeable',
+ type: 'dropdown',
+ options: [
+ { label: 'All', id: '' },
+ { label: 'Mergeable Only', id: 'true' },
+ { label: 'Non-Mergeable Only', id: 'false' },
+ ],
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ {
+ id: 'positionSortBy',
+ title: 'Sort By',
+ type: 'dropdown',
+ options: [
+ { label: 'Default', id: '' },
+ { label: 'Tokens', id: 'TOKENS' },
+ { label: 'Current Value', id: 'CURRENT' },
+ { label: 'Initial Value', id: 'INITIAL' },
+ { label: 'Cash P&L', id: 'CASHPNL' },
+ { label: 'Percent P&L', id: 'PERCENTPNL' },
+ { label: 'Title', id: 'TITLE' },
+ { label: 'Price', id: 'PRICE' },
+ { label: 'Avg Price', id: 'AVGPRICE' },
+ ],
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ {
+ id: 'positionSortDirection',
+ title: 'Sort Direction',
+ type: 'dropdown',
+ options: [
+ { label: 'Descending', id: 'DESC' },
+ { label: 'Ascending', id: 'ASC' },
+ ],
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ {
+ id: 'positionTitle',
+ title: 'Title Filter',
+ type: 'short-input',
+ placeholder: 'Search by title',
+ condition: { field: 'operation', value: ['get_positions'] },
+ },
+ // Trades-specific filters
+ {
+ id: 'tradeSide',
+ title: 'Trade Side',
+ type: 'dropdown',
+ options: [
+ { label: 'All', id: '' },
+ { label: 'Buy', id: 'BUY' },
+ { label: 'Sell', id: 'SELL' },
+ ],
+ condition: { field: 'operation', value: ['get_trades'] },
+ },
+ {
+ id: 'takerOnly',
+ title: 'Taker Only',
+ type: 'dropdown',
+ options: [
+ { label: 'Yes (default)', id: 'true' },
+ { label: 'No', id: 'false' },
+ ],
+ condition: { field: 'operation', value: ['get_trades'] },
+ },
+ {
+ id: 'filterType',
+ title: 'Filter Type',
+ type: 'dropdown',
+ options: [
+ { label: 'None', id: '' },
+ { label: 'Cash', id: 'CASH' },
+ { label: 'Tokens', id: 'TOKENS' },
+ ],
+ condition: { field: 'operation', value: ['get_trades'] },
+ },
+ {
+ id: 'filterAmount',
+ title: 'Filter Amount',
+ type: 'short-input',
+ placeholder: 'Minimum amount threshold',
+ condition: { field: 'operation', value: ['get_trades'] },
+ },
+ // Activity-specific fields
+ {
+ id: 'activityUser',
+ title: 'User Wallet Address',
+ type: 'short-input',
+ placeholder: 'Wallet address (0x-prefixed)',
+ required: true,
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activityType',
+ title: 'Activity Type',
+ type: 'dropdown',
+ options: [
+ { label: 'All', id: '' },
+ { label: 'Trade', id: 'TRADE' },
+ { label: 'Split', id: 'SPLIT' },
+ { label: 'Merge', id: 'MERGE' },
+ { label: 'Redeem', id: 'REDEEM' },
+ { label: 'Reward', id: 'REWARD' },
+ { label: 'Conversion', id: 'CONVERSION' },
+ { label: 'Maker Rebate', id: 'MAKER_REBATE' },
+ ],
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activityMarket',
+ title: 'Condition ID',
+ type: 'short-input',
+ placeholder: 'Condition ID filter (comma-separated)',
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activityEventId',
+ title: 'Event ID',
+ type: 'short-input',
+ placeholder: 'Event ID filter (comma-separated)',
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activitySide',
+ title: 'Trade Side',
+ type: 'dropdown',
+ options: [
+ { label: 'All', id: '' },
+ { label: 'Buy', id: 'BUY' },
+ { label: 'Sell', id: 'SELL' },
+ ],
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activitySortBy',
+ title: 'Sort By',
+ type: 'dropdown',
+ options: [
+ { label: 'Timestamp', id: 'TIMESTAMP' },
+ { label: 'Tokens', id: 'TOKENS' },
+ { label: 'Cash', id: 'CASH' },
+ ],
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activitySortDirection',
+ title: 'Sort Direction',
+ type: 'dropdown',
+ options: [
+ { label: 'Descending', id: 'DESC' },
+ { label: 'Ascending', id: 'ASC' },
+ ],
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activityStart',
+ title: 'Start Timestamp',
+ type: 'short-input',
+ placeholder: 'Unix timestamp (seconds)',
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ {
+ id: 'activityEnd',
+ title: 'End Timestamp',
+ type: 'short-input',
+ placeholder: 'Unix timestamp (seconds)',
+ condition: { field: 'operation', value: ['get_activity'] },
+ },
+ // Leaderboard-specific fields
+ {
+ id: 'leaderboardCategory',
+ title: 'Category',
+ type: 'dropdown',
+ options: [
+ { label: 'Overall', id: 'OVERALL' },
+ { label: 'Politics', id: 'POLITICS' },
+ { label: 'Sports', id: 'SPORTS' },
+ { label: 'Crypto', id: 'CRYPTO' },
+ { label: 'Culture', id: 'CULTURE' },
+ { label: 'Mentions', id: 'MENTIONS' },
+ { label: 'Weather', id: 'WEATHER' },
+ { label: 'Economics', id: 'ECONOMICS' },
+ { label: 'Tech', id: 'TECH' },
+ { label: 'Finance', id: 'FINANCE' },
+ ],
+ condition: { field: 'operation', value: ['get_leaderboard'] },
+ },
+ {
+ id: 'leaderboardTimePeriod',
+ title: 'Time Period',
+ type: 'dropdown',
+ options: [
+ { label: 'Day', id: 'DAY' },
+ { label: 'Week', id: 'WEEK' },
+ { label: 'Month', id: 'MONTH' },
+ { label: 'All Time', id: 'ALL' },
+ ],
+ condition: { field: 'operation', value: ['get_leaderboard'] },
+ },
+ {
+ id: 'leaderboardOrderBy',
+ title: 'Order By',
+ type: 'dropdown',
+ options: [
+ { label: 'Profit/Loss', id: 'PNL' },
+ { label: 'Volume', id: 'VOL' },
+ ],
+ condition: { field: 'operation', value: ['get_leaderboard'] },
+ },
+ {
+ id: 'leaderboardUser',
+ title: 'User Address',
+ type: 'short-input',
+ placeholder: 'Filter by specific user wallet',
+ condition: { field: 'operation', value: ['get_leaderboard'] },
+ },
+ {
+ id: 'leaderboardUserName',
+ title: 'Username',
+ type: 'short-input',
+ placeholder: 'Filter by username',
+ condition: { field: 'operation', value: ['get_leaderboard'] },
+ },
+ // Market Holders-specific fields
+ {
+ id: 'holdersMarket',
+ title: 'Condition ID',
+ type: 'short-input',
+ placeholder: 'Condition ID (comma-separated)',
+ required: true,
+ condition: { field: 'operation', value: ['get_holders'] },
+ },
+ {
+ id: 'holdersMinBalance',
+ title: 'Min Balance',
+ type: 'short-input',
+ placeholder: 'Minimum balance threshold (default: 1)',
+ condition: { field: 'operation', value: ['get_holders'] },
+ },
// Token ID for CLOB operations
{
id: 'tokenId',
@@ -205,11 +475,11 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
// Filters for list operations
{
id: 'closed',
- title: 'Status',
+ title: 'Closed Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
- { label: 'Active Only', id: 'false' },
+ { label: 'Open Only', id: 'false' },
{ label: 'Closed Only', id: 'true' },
],
condition: { field: 'operation', value: ['get_markets', 'get_events'] },
@@ -269,7 +539,18 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
placeholder: 'Number of results (max 50)',
condition: {
field: 'operation',
- value: ['get_markets', 'get_events', 'get_tags', 'search', 'get_series', 'get_trades'],
+ value: [
+ 'get_markets',
+ 'get_events',
+ 'get_tags',
+ 'search',
+ 'get_series',
+ 'get_trades',
+ 'get_positions',
+ 'get_activity',
+ 'get_leaderboard',
+ 'get_holders',
+ ],
},
},
{
@@ -279,9 +560,25 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
placeholder: 'Pagination offset',
condition: {
field: 'operation',
- value: ['get_markets', 'get_events', 'get_tags', 'search', 'get_series', 'get_trades'],
+ value: [
+ 'get_markets',
+ 'get_events',
+ 'get_tags',
+ 'get_series',
+ 'get_trades',
+ 'get_positions',
+ 'get_activity',
+ 'get_leaderboard',
+ ],
},
},
+ {
+ id: 'page',
+ title: 'Page',
+ type: 'short-input',
+ placeholder: 'Page number (1-indexed)',
+ condition: { field: 'operation', value: ['search'] },
+ },
],
tools: {
access: [
@@ -302,6 +599,9 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
'polymarket_get_tick_size',
'polymarket_get_positions',
'polymarket_get_trades',
+ 'polymarket_get_activity',
+ 'polymarket_get_leaderboard',
+ 'polymarket_get_holders',
],
config: {
tool: (params) => {
@@ -340,12 +640,49 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
return 'polymarket_get_positions'
case 'get_trades':
return 'polymarket_get_trades'
+ case 'get_activity':
+ return 'polymarket_get_activity'
+ case 'get_leaderboard':
+ return 'polymarket_get_leaderboard'
+ case 'get_holders':
+ return 'polymarket_get_holders'
default:
return 'polymarket_get_markets'
}
},
params: (params) => {
- const { operation, marketSlug, eventSlug, orderEvents, order, ...rest } = params
+ const {
+ operation,
+ marketSlug,
+ eventSlug,
+ orderEvents,
+ order,
+ positionEventId,
+ tradeSide,
+ positionSortBy,
+ positionSortDirection,
+ positionTitle,
+ // Activity params
+ activityUser,
+ activityType,
+ activityMarket,
+ activityEventId,
+ activitySide,
+ activitySortBy,
+ activitySortDirection,
+ activityStart,
+ activityEnd,
+ // Leaderboard params
+ leaderboardCategory,
+ leaderboardTimePeriod,
+ leaderboardOrderBy,
+ leaderboardUser,
+ leaderboardUserName,
+ // Holders params
+ holdersMarket,
+ holdersMinBalance,
+ ...rest
+ } = params
const cleanParams: Record = {}
// Map marketSlug to slug for get_market
@@ -365,6 +702,51 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
cleanParams.order = orderEvents
}
+ // Map positionEventId to eventId for positions and trades
+ if ((operation === 'get_positions' || operation === 'get_trades') && positionEventId) {
+ cleanParams.eventId = positionEventId
+ }
+
+ // Map tradeSide to side for trades
+ if (operation === 'get_trades' && tradeSide) {
+ cleanParams.side = tradeSide
+ }
+
+ // Map position-specific fields
+ if (operation === 'get_positions') {
+ if (positionSortBy) cleanParams.sortBy = positionSortBy
+ if (positionSortDirection) cleanParams.sortDirection = positionSortDirection
+ if (positionTitle) cleanParams.title = positionTitle
+ }
+
+ // Map activity-specific fields
+ if (operation === 'get_activity') {
+ if (activityUser) cleanParams.user = activityUser
+ if (activityType) cleanParams.type = activityType
+ if (activityMarket) cleanParams.market = activityMarket
+ if (activityEventId) cleanParams.eventId = activityEventId
+ if (activitySide) cleanParams.side = activitySide
+ if (activitySortBy) cleanParams.sortBy = activitySortBy
+ if (activitySortDirection) cleanParams.sortDirection = activitySortDirection
+ if (activityStart) cleanParams.start = Number(activityStart)
+ if (activityEnd) cleanParams.end = Number(activityEnd)
+ }
+
+ // Map leaderboard-specific fields
+ if (operation === 'get_leaderboard') {
+ if (leaderboardCategory) cleanParams.category = leaderboardCategory
+ if (leaderboardTimePeriod) cleanParams.timePeriod = leaderboardTimePeriod
+ if (leaderboardOrderBy) cleanParams.orderBy = leaderboardOrderBy
+ if (leaderboardUser) cleanParams.user = leaderboardUser
+ if (leaderboardUserName) cleanParams.userName = leaderboardUserName
+ }
+
+ // Map holders-specific fields
+ if (operation === 'get_holders') {
+ if (holdersMarket) cleanParams.market = holdersMarket
+ if (holdersMinBalance) cleanParams.minBalance = holdersMinBalance
+ }
+
// Convert numeric fields from string to number for get_price_history
if (operation === 'get_price_history') {
if (rest.fidelity) cleanParams.fidelity = Number(rest.fidelity)
@@ -394,13 +776,55 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
seriesId: { type: 'string', description: 'Series ID' },
query: { type: 'string', description: 'Search query' },
user: { type: 'string', description: 'User wallet address' },
- market: { type: 'string', description: 'Market ID filter' },
+ market: { type: 'string', description: 'Condition ID filter' },
+ positionEventId: { type: 'string', description: 'Event ID filter for positions/trades' },
tokenId: { type: 'string', description: 'CLOB Token ID' },
side: { type: 'string', description: 'Order side (buy/sell)' },
interval: { type: 'string', description: 'Price history interval' },
fidelity: { type: 'number', description: 'Data resolution in minutes' },
startTs: { type: 'number', description: 'Start timestamp (Unix)' },
endTs: { type: 'number', description: 'End timestamp (Unix)' },
+ // Positions-specific inputs
+ sizeThreshold: { type: 'string', description: 'Minimum position size threshold' },
+ redeemable: { type: 'string', description: 'Filter by redeemable status' },
+ mergeable: { type: 'string', description: 'Filter by mergeable status' },
+ positionSortBy: { type: 'string', description: 'Sort positions by field' },
+ positionSortDirection: { type: 'string', description: 'Sort direction (ASC/DESC)' },
+ positionTitle: { type: 'string', description: 'Filter positions by title' },
+ // Trades-specific inputs
+ tradeSide: { type: 'string', description: 'Filter trades by side (BUY/SELL)' },
+ takerOnly: { type: 'string', description: 'Filter to taker trades only' },
+ filterType: { type: 'string', description: 'Trade filter type (CASH/TOKENS)' },
+ filterAmount: { type: 'string', description: 'Minimum trade amount threshold' },
+ // List operation filters
+ closed: { type: 'string', description: 'Filter by closed status' },
+ order: { type: 'string', description: 'Sort field for markets' },
+ orderEvents: { type: 'string', description: 'Sort field for events' },
+ ascending: { type: 'string', description: 'Sort order (true/false)' },
+ tagId: { type: 'string', description: 'Filter by tag ID' },
+ // Pagination
+ limit: { type: 'string', description: 'Number of results per page' },
+ offset: { type: 'string', description: 'Pagination offset' },
+ page: { type: 'string', description: 'Page number for search' },
+ // Activity-specific inputs
+ activityUser: { type: 'string', description: 'User wallet address for activity' },
+ activityType: { type: 'string', description: 'Activity type filter' },
+ activityMarket: { type: 'string', description: 'Condition ID filter for activity' },
+ activityEventId: { type: 'string', description: 'Event ID filter for activity' },
+ activitySide: { type: 'string', description: 'Trade side filter for activity' },
+ activitySortBy: { type: 'string', description: 'Sort field for activity' },
+ activitySortDirection: { type: 'string', description: 'Sort direction for activity' },
+ activityStart: { type: 'string', description: 'Start timestamp for activity' },
+ activityEnd: { type: 'string', description: 'End timestamp for activity' },
+ // Leaderboard-specific inputs
+ leaderboardCategory: { type: 'string', description: 'Leaderboard category' },
+ leaderboardTimePeriod: { type: 'string', description: 'Leaderboard time period' },
+ leaderboardOrderBy: { type: 'string', description: 'Leaderboard order by field' },
+ leaderboardUser: { type: 'string', description: 'Filter leaderboard by user' },
+ leaderboardUserName: { type: 'string', description: 'Filter leaderboard by username' },
+ // Holders-specific inputs
+ holdersMarket: { type: 'string', description: 'Condition ID for holders lookup' },
+ holdersMinBalance: { type: 'string', description: 'Minimum balance threshold' },
},
outputs: {
// List operations
@@ -422,11 +846,19 @@ Return ONLY the Unix timestamp as a number - no explanations, no quotes, no extr
description: 'Search results with markets, events, profiles (search)',
},
// CLOB operations
- orderbook: { type: 'json', description: 'Order book with bids and asks (get_orderbook)' },
+ orderbook: {
+ type: 'json',
+ description: 'Order book with bids and asks (get_orderbook)',
+ },
price: { type: 'string', description: 'Market price (get_price, get_last_trade_price)' },
+ side: { type: 'string', description: 'Last trade side - BUY or SELL (get_last_trade_price)' },
midpoint: { type: 'string', description: 'Midpoint price (get_midpoint)' },
history: { type: 'json', description: 'Price history entries (get_price_history)' },
- spread: { type: 'json', description: 'Bid-ask spread (get_spread)' },
+ spread: { type: 'json', description: 'Spread value object (get_spread)' },
tickSize: { type: 'string', description: 'Minimum tick size (get_tick_size)' },
+ // Data API operations
+ activity: { type: 'json', description: 'Array of user activity entries (get_activity)' },
+ leaderboard: { type: 'json', description: 'Array of leaderboard entries (get_leaderboard)' },
+ holders: { type: 'json', description: 'Array of market holder groups (get_holders)' },
},
}
diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts
index e28ca604e..6138a4072 100644
--- a/apps/sim/blocks/registry.ts
+++ b/apps/sim/blocks/registry.ts
@@ -58,7 +58,7 @@ import { IntercomBlock, IntercomV2Block } from '@/blocks/blocks/intercom'
import { JinaBlock } from '@/blocks/blocks/jina'
import { JiraBlock } from '@/blocks/blocks/jira'
import { JiraServiceManagementBlock } from '@/blocks/blocks/jira_service_management'
-import { KalshiBlock } from '@/blocks/blocks/kalshi'
+import { KalshiBlock, KalshiV2Block } from '@/blocks/blocks/kalshi'
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { LangsmithBlock } from '@/blocks/blocks/langsmith'
import { LemlistBlock } from '@/blocks/blocks/lemlist'
@@ -222,6 +222,7 @@ export const registry: Record = {
jira: JiraBlock,
jira_service_management: JiraServiceManagementBlock,
kalshi: KalshiBlock,
+ kalshi_v2: KalshiV2Block,
knowledge: KnowledgeBlock,
langsmith: LangsmithBlock,
lemlist: LemlistBlock,
diff --git a/apps/sim/components/emcn/components/switch/switch.tsx b/apps/sim/components/emcn/components/switch/switch.tsx
index c9fc42261..118eb6f63 100644
--- a/apps/sim/components/emcn/components/switch/switch.tsx
+++ b/apps/sim/components/emcn/components/switch/switch.tsx
@@ -5,9 +5,8 @@ import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from '@/lib/core/utils/cn'
/**
- * Custom switch component with thin track design.
- * Track: 28px width, 6px height, 20px border-radius
- * Thumb: 14px diameter circle that overlaps the track
+ * Switch component styled to match Sim's design system.
+ * Uses brand color for checked state, neutral border for unchecked.
*/
const Switch = React.forwardRef<
React.ElementRef,
@@ -16,21 +15,13 @@ const Switch = React.forwardRef<
-
+
))
diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts
index 387f560c4..ba2c2fc23 100644
--- a/apps/sim/executor/constants.ts
+++ b/apps/sim/executor/constants.ts
@@ -275,6 +275,26 @@ export function isTriggerBlockType(blockType: string | undefined): boolean {
return blockType !== undefined && (TRIGGER_BLOCK_TYPES as readonly string[]).includes(blockType)
}
+/**
+ * Determines if a block behaves as a trigger based on its metadata and config.
+ * This is used for execution flow decisions where trigger-like behavior matters.
+ *
+ * A block is considered trigger-like if:
+ * - Its category is 'triggers'
+ * - It has triggerMode enabled
+ * - It's a starter block (legacy entry point)
+ */
+export function isTriggerBehavior(block: {
+ metadata?: { category?: string; id?: string }
+ config?: { params?: { triggerMode?: boolean } }
+}): boolean {
+ return (
+ block.metadata?.category === 'triggers' ||
+ block.config?.params?.triggerMode === true ||
+ block.metadata?.id === BlockType.STARTER
+ )
+}
+
export function isMetadataOnlyBlockType(blockType: string | undefined): boolean {
return (
blockType !== undefined && (METADATA_ONLY_BLOCK_TYPES as readonly string[]).includes(blockType)
diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts
index 5e2ec09cc..2cc37a77e 100644
--- a/apps/sim/executor/execution/block-executor.ts
+++ b/apps/sim/executor/execution/block-executor.ts
@@ -11,6 +11,8 @@ import {
DEFAULTS,
EDGE,
isSentinelBlockType,
+ isTriggerBehavior,
+ isWorkflowBlockType,
} from '@/executor/constants'
import type { DAGNode } from '@/executor/dag/builder'
import { ChildWorkflowError } from '@/executor/errors/child-workflow-error'
@@ -153,8 +155,8 @@ export class BlockExecutor {
this.state.setBlockOutput(node.id, normalizedOutput, duration)
if (!isSentinel) {
- const filteredOutput = this.filterOutputForLog(block, normalizedOutput)
- this.callOnBlockComplete(ctx, node, block, resolvedInputs, filteredOutput, duration)
+ const displayOutput = this.filterOutputForDisplay(block, normalizedOutput)
+ this.callOnBlockComplete(ctx, node, block, resolvedInputs, displayOutput, duration)
}
return normalizedOutput
@@ -244,7 +246,8 @@ export class BlockExecutor {
)
if (!isSentinel) {
- this.callOnBlockComplete(ctx, node, block, input, errorOutput, duration)
+ const displayOutput = this.filterOutputForDisplay(block, errorOutput)
+ this.callOnBlockComplete(ctx, node, block, input, displayOutput, duration)
}
const hasErrorPort = this.hasErrorPortEdge(node)
@@ -336,7 +339,9 @@ export class BlockExecutor {
block: SerializedBlock,
output: NormalizedBlockOutput
): NormalizedBlockOutput {
- if (block.metadata?.id === BlockType.HUMAN_IN_THE_LOOP) {
+ const blockType = block.metadata?.id
+
+ if (blockType === BlockType.HUMAN_IN_THE_LOOP) {
const filtered: NormalizedBlockOutput = {}
for (const [key, value] of Object.entries(output)) {
if (key.startsWith('_')) continue
@@ -346,12 +351,7 @@ export class BlockExecutor {
return filtered
}
- const isTrigger =
- block.metadata?.category === 'triggers' ||
- block.config?.params?.triggerMode === true ||
- block.metadata?.id === BlockType.STARTER
-
- if (isTrigger) {
+ if (isTriggerBehavior(block)) {
const filtered: NormalizedBlockOutput = {}
const internalKeys = ['webhook', 'workflowId']
for (const [key, value] of Object.entries(output)) {
@@ -364,6 +364,22 @@ export class BlockExecutor {
return output
}
+ private filterOutputForDisplay(
+ block: SerializedBlock,
+ output: NormalizedBlockOutput
+ ): NormalizedBlockOutput {
+ const filtered = this.filterOutputForLog(block, output)
+
+ if (isWorkflowBlockType(block.metadata?.id)) {
+ const { childTraceSpans: _, ...displayOutput } = filtered as {
+ childTraceSpans?: unknown
+ } & Record
+ return displayOutput
+ }
+
+ return filtered
+ }
+
private callOnBlockStart(ctx: ExecutionContext, node: DAGNode, block: SerializedBlock): void {
const blockId = node.id
const blockName = block.metadata?.name ?? blockId
diff --git a/apps/sim/executor/execution/edge-manager.test.ts b/apps/sim/executor/execution/edge-manager.test.ts
index f78bb8cf2..f7b332792 100644
--- a/apps/sim/executor/execution/edge-manager.test.ts
+++ b/apps/sim/executor/execution/edge-manager.test.ts
@@ -773,6 +773,176 @@ describe('EdgeManager', () => {
})
})
+ describe('Multiple error ports to same target', () => {
+ it('should mark target ready when one source errors and another succeeds', () => {
+ // This tests the case where a node has multiple incoming error edges
+ // from different sources. When one source errors (activating its error edge)
+ // and another source succeeds (deactivating its error edge), the target
+ // should become ready after both sources complete.
+ //
+ // Workflow 1 (errors) ─── error ───┐
+ // ├──→ Error Handler
+ // Workflow 7 (succeeds) ─ error ───┘
+
+ const workflow1Id = 'workflow-1'
+ const workflow7Id = 'workflow-7'
+ const errorHandlerId = 'error-handler'
+
+ const workflow1Node = createMockNode(workflow1Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const workflow7Node = createMockNode(workflow7Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const errorHandlerNode = createMockNode(errorHandlerId, [], [workflow1Id, workflow7Id])
+
+ const nodes = new Map([
+ [workflow1Id, workflow1Node],
+ [workflow7Id, workflow7Node],
+ [errorHandlerId, errorHandlerNode],
+ ])
+
+ const dag = createMockDAG(nodes)
+ const edgeManager = new EdgeManager(dag)
+
+ // Workflow 1 errors first - error edge activates
+ const readyAfterWorkflow1 = edgeManager.processOutgoingEdges(workflow1Node, {
+ error: 'Something went wrong',
+ })
+ // Error handler should NOT be ready yet (waiting for workflow 7)
+ expect(readyAfterWorkflow1).not.toContain(errorHandlerId)
+
+ // Workflow 7 succeeds - error edge deactivates
+ const readyAfterWorkflow7 = edgeManager.processOutgoingEdges(workflow7Node, {
+ result: 'success',
+ })
+ // Error handler SHOULD be ready now (workflow 1's error edge activated)
+ expect(readyAfterWorkflow7).toContain(errorHandlerId)
+ })
+
+ it('should mark target ready when first source succeeds then second errors', () => {
+ // Opposite order: first source succeeds, then second errors
+
+ const workflow1Id = 'workflow-1'
+ const workflow7Id = 'workflow-7'
+ const errorHandlerId = 'error-handler'
+
+ const workflow1Node = createMockNode(workflow1Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const workflow7Node = createMockNode(workflow7Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const errorHandlerNode = createMockNode(errorHandlerId, [], [workflow1Id, workflow7Id])
+
+ const nodes = new Map([
+ [workflow1Id, workflow1Node],
+ [workflow7Id, workflow7Node],
+ [errorHandlerId, errorHandlerNode],
+ ])
+
+ const dag = createMockDAG(nodes)
+ const edgeManager = new EdgeManager(dag)
+
+ // Workflow 1 succeeds first - error edge deactivates
+ const readyAfterWorkflow1 = edgeManager.processOutgoingEdges(workflow1Node, {
+ result: 'success',
+ })
+ // Error handler should NOT be ready yet (waiting for workflow 7)
+ expect(readyAfterWorkflow1).not.toContain(errorHandlerId)
+
+ // Workflow 7 errors - error edge activates
+ const readyAfterWorkflow7 = edgeManager.processOutgoingEdges(workflow7Node, {
+ error: 'Something went wrong',
+ })
+ // Error handler SHOULD be ready now (workflow 7's error edge activated)
+ expect(readyAfterWorkflow7).toContain(errorHandlerId)
+ })
+
+ it('should NOT mark target ready when all sources succeed (no errors)', () => {
+ // When neither source errors, the error handler should NOT run
+
+ const workflow1Id = 'workflow-1'
+ const workflow7Id = 'workflow-7'
+ const errorHandlerId = 'error-handler'
+
+ const workflow1Node = createMockNode(workflow1Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const workflow7Node = createMockNode(workflow7Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const errorHandlerNode = createMockNode(errorHandlerId, [], [workflow1Id, workflow7Id])
+
+ const nodes = new Map([
+ [workflow1Id, workflow1Node],
+ [workflow7Id, workflow7Node],
+ [errorHandlerId, errorHandlerNode],
+ ])
+
+ const dag = createMockDAG(nodes)
+ const edgeManager = new EdgeManager(dag)
+
+ // Both workflows succeed - both error edges deactivate
+ const readyAfterWorkflow1 = edgeManager.processOutgoingEdges(workflow1Node, {
+ result: 'success',
+ })
+ expect(readyAfterWorkflow1).not.toContain(errorHandlerId)
+
+ const readyAfterWorkflow7 = edgeManager.processOutgoingEdges(workflow7Node, {
+ result: 'success',
+ })
+ // Error handler should NOT be ready (no errors occurred)
+ expect(readyAfterWorkflow7).not.toContain(errorHandlerId)
+ })
+
+ it('should mark target ready when both sources error', () => {
+ // When both sources error, the error handler should run
+
+ const workflow1Id = 'workflow-1'
+ const workflow7Id = 'workflow-7'
+ const errorHandlerId = 'error-handler'
+
+ const workflow1Node = createMockNode(workflow1Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const workflow7Node = createMockNode(workflow7Id, [
+ { target: errorHandlerId, sourceHandle: 'error' },
+ ])
+
+ const errorHandlerNode = createMockNode(errorHandlerId, [], [workflow1Id, workflow7Id])
+
+ const nodes = new Map([
+ [workflow1Id, workflow1Node],
+ [workflow7Id, workflow7Node],
+ [errorHandlerId, errorHandlerNode],
+ ])
+
+ const dag = createMockDAG(nodes)
+ const edgeManager = new EdgeManager(dag)
+
+ // Workflow 1 errors
+ const readyAfterWorkflow1 = edgeManager.processOutgoingEdges(workflow1Node, {
+ error: 'Error 1',
+ })
+ expect(readyAfterWorkflow1).not.toContain(errorHandlerId)
+
+ // Workflow 7 errors
+ const readyAfterWorkflow7 = edgeManager.processOutgoingEdges(workflow7Node, {
+ error: 'Error 2',
+ })
+ // Error handler SHOULD be ready (both edges activated)
+ expect(readyAfterWorkflow7).toContain(errorHandlerId)
+ })
+ })
+
describe('Chained conditions', () => {
it('should handle sequential conditions (condition1 → condition2)', () => {
const condition1Id = 'condition-1'
diff --git a/apps/sim/executor/execution/edge-manager.ts b/apps/sim/executor/execution/edge-manager.ts
index 28416333c..f0ac33fa7 100644
--- a/apps/sim/executor/execution/edge-manager.ts
+++ b/apps/sim/executor/execution/edge-manager.ts
@@ -8,6 +8,7 @@ const logger = createLogger('EdgeManager')
export class EdgeManager {
private deactivatedEdges = new Set()
+ private nodesWithActivatedEdge = new Set()
constructor(private dag: DAG) {}
@@ -35,6 +36,11 @@ export class EdgeManager {
activatedTargets.push(edge.target)
}
+ // Track nodes that have received at least one activated edge
+ for (const targetId of activatedTargets) {
+ this.nodesWithActivatedEdge.add(targetId)
+ }
+
const cascadeTargets = new Set()
for (const { target, handle } of edgesToDeactivate) {
this.deactivateEdgeAndDescendants(node.id, target, handle, cascadeTargets)
@@ -71,6 +77,18 @@ export class EdgeManager {
}
}
+ // Check if any deactivation targets that previously received an activated edge are now ready
+ for (const { target } of edgesToDeactivate) {
+ if (
+ !readyNodes.includes(target) &&
+ !activatedTargets.includes(target) &&
+ this.nodesWithActivatedEdge.has(target) &&
+ this.isTargetReady(target)
+ ) {
+ readyNodes.push(target)
+ }
+ }
+
return readyNodes
}
@@ -90,6 +108,7 @@ export class EdgeManager {
clearDeactivatedEdges(): void {
this.deactivatedEdges.clear()
+ this.nodesWithActivatedEdge.clear()
}
/**
@@ -108,6 +127,10 @@ export class EdgeManager {
for (const edgeKey of edgesToRemove) {
this.deactivatedEdges.delete(edgeKey)
}
+ // Also clear activated edge tracking for these nodes
+ for (const nodeId of nodeIds) {
+ this.nodesWithActivatedEdge.delete(nodeId)
+ }
}
private isTargetReady(targetId: string): boolean {
@@ -210,7 +233,11 @@ export class EdgeManager {
cascadeTargets?.add(targetId)
}
- if (this.hasActiveIncomingEdges(targetNode, edgeKey)) {
+ // Don't cascade if node has active incoming edges OR has received an activated edge
+ if (
+ this.hasActiveIncomingEdges(targetNode, edgeKey) ||
+ this.nodesWithActivatedEdge.has(targetId)
+ ) {
return
}
diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts
index fd0508f7c..d9be91d23 100644
--- a/apps/sim/executor/handlers/trigger/trigger-handler.ts
+++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts
@@ -1,5 +1,5 @@
import { createLogger } from '@sim/logger'
-import { BlockType } from '@/executor/constants'
+import { BlockType, isTriggerBehavior } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
@@ -7,15 +7,7 @@ const logger = createLogger('TriggerBlockHandler')
export class TriggerBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
- if (block.metadata?.id === BlockType.STARTER) {
- return true
- }
-
- const isTriggerCategory = block.metadata?.category === 'triggers'
-
- const hasTriggerMode = block.config?.params?.triggerMode === true
-
- return isTriggerCategory || hasTriggerMode
+ return isTriggerBehavior(block)
}
async execute(
diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts
index 0c7ec4bd1..c6fc1c185 100644
--- a/apps/sim/executor/utils/block-data.ts
+++ b/apps/sim/executor/utils/block-data.ts
@@ -1,4 +1,5 @@
-import { normalizeName } from '@/executor/constants'
+import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
+import { isTriggerBehavior, normalizeName } from '@/executor/constants'
import type { ExecutionContext } from '@/executor/types'
import type { OutputSchema } from '@/executor/utils/block-reference'
import type { SerializedBlock } from '@/serializer/types'
@@ -11,25 +12,73 @@ export interface BlockDataCollection {
blockOutputSchemas: Record
}
+/**
+ * Block types where inputFormat fields should be merged into outputs schema.
+ * These are blocks where users define custom fields via inputFormat that become
+ * valid output paths (e.g., , , ).
+ *
+ * Note: This includes non-trigger blocks like 'starter' and 'human_in_the_loop' which
+ * have category 'blocks' but still need their inputFormat exposed as outputs.
+ */
+const BLOCKS_WITH_INPUT_FORMAT_OUTPUTS = [
+ 'start_trigger',
+ 'starter',
+ 'api_trigger',
+ 'input_trigger',
+ 'generic_webhook',
+ 'human_in_the_loop',
+] as const
+
+function getInputFormatFields(block: SerializedBlock): OutputSchema {
+ const inputFormat = normalizeInputFormatValue(block.config?.params?.inputFormat)
+ if (inputFormat.length === 0) {
+ return {}
+ }
+
+ const schema: OutputSchema = {}
+ for (const field of inputFormat) {
+ if (!field.name) continue
+ schema[field.name] = {
+ type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any',
+ }
+ }
+
+ return schema
+}
+
export function getBlockSchema(
block: SerializedBlock,
toolConfig?: ToolConfig
): OutputSchema | undefined {
- const isTrigger =
- block.metadata?.category === 'triggers' ||
- (block.config?.params as Record | undefined)?.triggerMode === true
+ const blockType = block.metadata?.id
+
+ // For blocks that expose inputFormat as outputs, always merge them
+ // This includes both triggers (start_trigger, generic_webhook) and
+ // non-triggers (starter, human_in_the_loop) that have inputFormat
+ if (
+ blockType &&
+ BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes(
+ blockType as (typeof BLOCKS_WITH_INPUT_FORMAT_OUTPUTS)[number]
+ )
+ ) {
+ const baseOutputs = (block.outputs as OutputSchema) || {}
+ const inputFormatFields = getInputFormatFields(block)
+ const merged = { ...baseOutputs, ...inputFormatFields }
+ if (Object.keys(merged).length > 0) {
+ return merged
+ }
+ }
+
+ const isTrigger = isTriggerBehavior(block)
- // Triggers use saved outputs (defines the trigger payload schema)
if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}
- // When a tool is selected, tool outputs are the source of truth
if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) {
return toolConfig.outputs as OutputSchema
}
- // Fallback to saved outputs for blocks without tools
if (block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}
diff --git a/apps/sim/executor/utils/code-formatting.ts b/apps/sim/executor/utils/code-formatting.ts
new file mode 100644
index 000000000..a4a73dee8
--- /dev/null
+++ b/apps/sim/executor/utils/code-formatting.ts
@@ -0,0 +1,48 @@
+/**
+ * Formats a JavaScript/TypeScript value as a code literal for the target language.
+ * Handles special cases like null, undefined, booleans, and Python-specific number representations.
+ *
+ * @param value - The value to format
+ * @param language - Target language ('javascript' or 'python')
+ * @returns A string literal representation valid in the target language
+ *
+ * @example
+ * formatLiteralForCode(null, 'python') // => 'None'
+ * formatLiteralForCode(true, 'python') // => 'True'
+ * formatLiteralForCode(NaN, 'python') // => "float('nan')"
+ * formatLiteralForCode("hello", 'javascript') // => '"hello"'
+ * formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
+ */
+export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
+ const isPython = language === 'python'
+
+ if (value === undefined) {
+ return isPython ? 'None' : 'undefined'
+ }
+ if (value === null) {
+ return isPython ? 'None' : 'null'
+ }
+ if (typeof value === 'boolean') {
+ return isPython ? (value ? 'True' : 'False') : String(value)
+ }
+ if (typeof value === 'number') {
+ if (Number.isNaN(value)) {
+ return isPython ? "float('nan')" : 'NaN'
+ }
+ if (value === Number.POSITIVE_INFINITY) {
+ return isPython ? "float('inf')" : 'Infinity'
+ }
+ if (value === Number.NEGATIVE_INFINITY) {
+ return isPython ? "float('-inf')" : '-Infinity'
+ }
+ return String(value)
+ }
+ if (typeof value === 'string') {
+ return JSON.stringify(value)
+ }
+ // Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
+ if (isPython) {
+ return `json.loads(${JSON.stringify(JSON.stringify(value))})`
+ }
+ return JSON.stringify(value)
+}
diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts
index 980708931..05077c81e 100644
--- a/apps/sim/executor/variables/resolver.ts
+++ b/apps/sim/executor/variables/resolver.ts
@@ -157,7 +157,14 @@ export class VariableResolver {
let replacementError: Error | null = null
- // Use generic utility for smart variable reference replacement
+ const blockType = block?.metadata?.id
+ const language =
+ blockType === BlockType.FUNCTION
+ ? ((block?.config?.params as Record | undefined)?.language as
+ | string
+ | undefined)
+ : undefined
+
let result = replaceValidReferences(template, (match) => {
if (replacementError) return match
@@ -167,14 +174,7 @@ export class VariableResolver {
return match
}
- const blockType = block?.metadata?.id
- const isInTemplateLiteral =
- blockType === BlockType.FUNCTION &&
- template.includes('${') &&
- template.includes('}') &&
- template.includes('`')
-
- return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
+ return this.blockResolver.formatValueForBlock(resolved, blockType, language)
} catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error))
return match
diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts
index f08b22fc2..01a804900 100644
--- a/apps/sim/executor/variables/resolvers/block.test.ts
+++ b/apps/sim/executor/variables/resolvers/block.test.ts
@@ -257,15 +257,9 @@ describe('BlockResolver', () => {
expect(result).toBe('"hello"')
})
- it.concurrent('should format string for function block in template literal', () => {
+ it.concurrent('should format object for function block', () => {
const resolver = new BlockResolver(createTestWorkflow())
- const result = resolver.formatValueForBlock('hello', 'function', true)
- expect(result).toBe('hello')
- })
-
- it.concurrent('should format object for function block in template literal', () => {
- const resolver = new BlockResolver(createTestWorkflow())
- const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
+ const result = resolver.formatValueForBlock({ a: 1 }, 'function')
expect(result).toBe('{"a":1}')
})
diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts
index 09d246e80..63ab36138 100644
--- a/apps/sim/executor/variables/resolvers/block.ts
+++ b/apps/sim/executor/variables/resolvers/block.ts
@@ -10,6 +10,7 @@ import {
type OutputSchema,
resolveBlockReference,
} from '@/executor/utils/block-reference'
+import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
navigatePath,
type ResolutionContext,
@@ -159,17 +160,13 @@ export class BlockResolver implements Resolver {
return this.nameToBlockId.get(normalizeName(name))
}
- public formatValueForBlock(
- value: any,
- blockType: string | undefined,
- isInTemplateLiteral = false
- ): string {
+ public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
if (blockType === 'condition') {
return this.stringifyForCondition(value)
}
if (blockType === 'function') {
- return this.formatValueForCodeContext(value, isInTemplateLiteral)
+ return this.formatValueForCodeContext(value, language)
}
if (blockType === 'response') {
@@ -210,29 +207,7 @@ export class BlockResolver implements Resolver {
return String(value)
}
- private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string {
- if (isInTemplateLiteral) {
- if (typeof value === 'string') {
- return value
- }
- if (typeof value === 'object' && value !== null) {
- return JSON.stringify(value)
- }
- return String(value)
- }
-
- if (typeof value === 'string') {
- return JSON.stringify(value)
- }
- if (typeof value === 'object' && value !== null) {
- return JSON.stringify(value)
- }
- if (value === undefined) {
- return 'undefined'
- }
- if (value === null) {
- return 'null'
- }
- return String(value)
+ private formatValueForCodeContext(value: any, language?: string): string {
+ return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
}
}
diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts
index 9f4b69eec..2c153154f 100644
--- a/apps/sim/executor/variables/resolvers/reference.ts
+++ b/apps/sim/executor/variables/resolvers/reference.ts
@@ -30,7 +30,10 @@ export function navigatePath(obj: any, path: string[]): any {
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
if (arrayMatch) {
const [, prop, bracketsPart] = arrayMatch
- current = current[prop]
+ current =
+ typeof current === 'object' && current !== null
+ ? (current as Record)[prop]
+ : undefined
if (current === undefined || current === null) {
return undefined
}
@@ -49,7 +52,10 @@ export function navigatePath(obj: any, path: string[]): any {
const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined
} else {
- current = current[part]
+ current =
+ typeof current === 'object' && current !== null
+ ? (current as Record)[part]
+ : undefined
}
}
return current
diff --git a/apps/sim/hooks/use-canvas-viewport.ts b/apps/sim/hooks/use-canvas-viewport.ts
index ad570d8e0..1e1cfba51 100644
--- a/apps/sim/hooks/use-canvas-viewport.ts
+++ b/apps/sim/hooks/use-canvas-viewport.ts
@@ -1,5 +1,6 @@
import { useCallback } from 'react'
import type { Node, ReactFlowInstance } from 'reactflow'
+import { BLOCK_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
interface VisibleBounds {
width: number
@@ -139,8 +140,8 @@ export function useCanvasViewport(reactFlowInstance: ReactFlowInstance | null) {
let maxY = Number.NEGATIVE_INFINITY
nodes.forEach((node) => {
- const nodeWidth = node.width ?? 200
- const nodeHeight = node.height ?? 100
+ const nodeWidth = node.width ?? BLOCK_DIMENSIONS.FIXED_WIDTH
+ const nodeHeight = node.height ?? BLOCK_DIMENSIONS.MIN_HEIGHT
minX = Math.min(minX, node.position.x)
minY = Math.min(minY, node.position.y)
diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts
new file mode 100644
index 000000000..d0edbb535
--- /dev/null
+++ b/apps/sim/hooks/use-code-undo-redo.ts
@@ -0,0 +1,239 @@
+import { useCallback, useEffect, useMemo, useRef } from 'react'
+import { createLogger } from '@sim/logger'
+import { useShallow } from 'zustand/react/shallow'
+import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
+import { useCodeUndoRedoStore } from '@/stores/undo-redo'
+import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
+import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+
+const logger = createLogger('CodeUndoRedo')
+
+interface UseCodeUndoRedoOptions {
+ blockId: string
+ subBlockId: string
+ value: string
+ enabled?: boolean
+ isReadOnly?: boolean
+ isStreaming?: boolean
+ debounceMs?: number
+}
+
+export function useCodeUndoRedo({
+ blockId,
+ subBlockId,
+ value,
+ enabled = true,
+ isReadOnly = false,
+ isStreaming = false,
+ debounceMs = 500,
+}: UseCodeUndoRedoOptions) {
+ const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
+ const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
+ const { isShowingDiff, hasActiveDiff } = useWorkflowDiffStore(
+ useShallow((state) => ({
+ isShowingDiff: state.isShowingDiff,
+ hasActiveDiff: state.hasActiveDiff,
+ }))
+ )
+
+ const isBaselineView = hasActiveDiff && !isShowingDiff
+ const isEnabled = useMemo(
+ () => Boolean(enabled && activeWorkflowId && !isReadOnly && !isStreaming && !isBaselineView),
+ [enabled, activeWorkflowId, isReadOnly, isStreaming, isBaselineView]
+ )
+ const isReplaceEnabled = useMemo(
+ () => Boolean(enabled && activeWorkflowId && !isReadOnly && !isBaselineView),
+ [enabled, activeWorkflowId, isReadOnly, isBaselineView]
+ )
+
+ const lastCommittedValueRef = useRef(value ?? '')
+ const pendingBeforeRef = useRef(null)
+ const pendingAfterRef = useRef(null)
+ const timeoutRef = useRef | null>(null)
+ const isApplyingRef = useRef(false)
+
+ const clearTimer = useCallback(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ timeoutRef.current = null
+ }
+ }, [])
+
+ const resetPending = useCallback(() => {
+ pendingBeforeRef.current = null
+ pendingAfterRef.current = null
+ }, [])
+
+ const commitPending = useCallback(() => {
+ if (!isEnabled || !activeWorkflowId) {
+ clearTimer()
+ resetPending()
+ return
+ }
+
+ const before = pendingBeforeRef.current
+ const after = pendingAfterRef.current
+ if (before === null || after === null) return
+
+ if (before === after) {
+ lastCommittedValueRef.current = after
+ clearTimer()
+ resetPending()
+ return
+ }
+
+ useCodeUndoRedoStore.getState().push({
+ id: crypto.randomUUID(),
+ createdAt: Date.now(),
+ workflowId: activeWorkflowId,
+ blockId,
+ subBlockId,
+ before,
+ after,
+ })
+
+ lastCommittedValueRef.current = after
+ clearTimer()
+ resetPending()
+ }, [activeWorkflowId, blockId, clearTimer, isEnabled, resetPending, subBlockId])
+
+ const recordChange = useCallback(
+ (nextValue: string) => {
+ if (!isEnabled || isApplyingRef.current) return
+
+ if (pendingBeforeRef.current === null) {
+ pendingBeforeRef.current = lastCommittedValueRef.current ?? ''
+ }
+
+ pendingAfterRef.current = nextValue
+ clearTimer()
+ timeoutRef.current = setTimeout(commitPending, debounceMs)
+ },
+ [clearTimer, commitPending, debounceMs, isEnabled]
+ )
+
+ const recordReplace = useCallback(
+ (nextValue: string) => {
+ if (!isReplaceEnabled || isApplyingRef.current || !activeWorkflowId) return
+
+ if (pendingBeforeRef.current !== null) {
+ commitPending()
+ }
+
+ const before = lastCommittedValueRef.current ?? ''
+ if (before === nextValue) {
+ lastCommittedValueRef.current = nextValue
+ resetPending()
+ return
+ }
+
+ useCodeUndoRedoStore.getState().push({
+ id: crypto.randomUUID(),
+ createdAt: Date.now(),
+ workflowId: activeWorkflowId,
+ blockId,
+ subBlockId,
+ before,
+ after: nextValue,
+ })
+
+ lastCommittedValueRef.current = nextValue
+ clearTimer()
+ resetPending()
+ },
+ [
+ activeWorkflowId,
+ blockId,
+ clearTimer,
+ commitPending,
+ isReplaceEnabled,
+ resetPending,
+ subBlockId,
+ ]
+ )
+
+ const flushPending = useCallback(() => {
+ if (pendingBeforeRef.current === null) return
+ clearTimer()
+ commitPending()
+ }, [clearTimer, commitPending])
+
+ const startSession = useCallback(
+ (currentValue: string) => {
+ clearTimer()
+ resetPending()
+ lastCommittedValueRef.current = currentValue ?? ''
+ },
+ [clearTimer, resetPending]
+ )
+
+ const applyValue = useCallback(
+ (nextValue: string) => {
+ if (!isEnabled) return
+ isApplyingRef.current = true
+ try {
+ collaborativeSetSubblockValue(blockId, subBlockId, nextValue)
+ } finally {
+ isApplyingRef.current = false
+ }
+ lastCommittedValueRef.current = nextValue
+ clearTimer()
+ resetPending()
+ },
+ [blockId, clearTimer, collaborativeSetSubblockValue, isEnabled, resetPending, subBlockId]
+ )
+
+ const undo = useCallback(() => {
+ if (!activeWorkflowId || !isEnabled) return
+ if (pendingBeforeRef.current !== null) {
+ flushPending()
+ }
+ const entry = useCodeUndoRedoStore.getState().undo(activeWorkflowId, blockId, subBlockId)
+ if (!entry) return
+ logger.debug('Undo code edit', { blockId, subBlockId })
+ applyValue(entry.before)
+ }, [activeWorkflowId, applyValue, blockId, flushPending, isEnabled, subBlockId])
+
+ const redo = useCallback(() => {
+ if (!activeWorkflowId || !isEnabled) return
+ if (pendingBeforeRef.current !== null) {
+ flushPending()
+ }
+ const entry = useCodeUndoRedoStore.getState().redo(activeWorkflowId, blockId, subBlockId)
+ if (!entry) return
+ logger.debug('Redo code edit', { blockId, subBlockId })
+ applyValue(entry.after)
+ }, [activeWorkflowId, applyValue, blockId, flushPending, isEnabled, subBlockId])
+
+ useEffect(() => {
+ if (isApplyingRef.current || isStreaming) return
+
+ const nextValue = value ?? ''
+
+ if (pendingBeforeRef.current !== null) {
+ if (pendingAfterRef.current !== nextValue) {
+ clearTimer()
+ resetPending()
+ lastCommittedValueRef.current = nextValue
+ }
+ return
+ }
+
+ lastCommittedValueRef.current = nextValue
+ }, [clearTimer, isStreaming, resetPending, value])
+
+ useEffect(() => {
+ return () => {
+ flushPending()
+ }
+ }, [flushPending])
+
+ return {
+ recordChange,
+ recordReplace,
+ flushPending,
+ startSession,
+ undo,
+ redo,
+ }
+}
diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts
index 89aff1d12..5f5721549 100644
--- a/apps/sim/hooks/use-collaborative-workflow.ts
+++ b/apps/sim/hooks/use-collaborative-workflow.ts
@@ -20,7 +20,7 @@ import {
import { useNotificationStore } from '@/stores/notifications'
import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store'
import { usePanelEditorStore, useVariablesStore } from '@/stores/panel'
-import { useUndoRedoStore } from '@/stores/undo-redo'
+import { useCodeUndoRedoStore, useUndoRedoStore } from '@/stores/undo-redo'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -449,6 +449,10 @@ export function useCollaborativeWorkflow() {
try {
// The setValue function automatically uses the active workflow ID
useSubBlockStore.getState().setValue(blockId, subblockId, value)
+ const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type
+ if (activeWorkflowId && blockType === 'function' && subblockId === 'code') {
+ useCodeUndoRedoStore.getState().clear(activeWorkflowId, blockId, subblockId)
+ }
} catch (error) {
logger.error('Error applying remote subblock update:', error)
} finally {
diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
index 853638d89..d6d5af7ba 100644
--- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
+++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts
@@ -37,6 +37,13 @@ export const knowledgeBaseServerTool: BaseServerTool {
})
describe('InputFormat SubBlock Special Handling', () => {
- it.concurrent('should ignore value and collapsed fields in inputFormat', () => {
+ it.concurrent('should ignore collapsed field but detect value changes in inputFormat', () => {
+ // Only collapsed changes - should NOT detect as change
const state1 = createWorkflowState({
blocks: {
block1: createBlock('block1', {
@@ -578,8 +579,8 @@ describe('hasWorkflowChanged', () => {
subBlocks: {
inputFormat: {
value: [
- { id: 'input1', name: 'Name', value: 'Jane', collapsed: false },
- { id: 'input2', name: 'Age', value: 30, collapsed: true },
+ { id: 'input1', name: 'Name', value: 'John', collapsed: false },
+ { id: 'input2', name: 'Age', value: 25, collapsed: true },
],
},
},
@@ -589,6 +590,32 @@ describe('hasWorkflowChanged', () => {
expect(hasWorkflowChanged(state1, state2)).toBe(false)
})
+ it.concurrent('should detect value changes in inputFormat', () => {
+ const state1 = createWorkflowState({
+ blocks: {
+ block1: createBlock('block1', {
+ subBlocks: {
+ inputFormat: {
+ value: [{ id: 'input1', name: 'Name', value: 'John' }],
+ },
+ },
+ }),
+ },
+ })
+ const state2 = createWorkflowState({
+ blocks: {
+ block1: createBlock('block1', {
+ subBlocks: {
+ inputFormat: {
+ value: [{ id: 'input1', name: 'Name', value: 'Jane' }],
+ },
+ },
+ }),
+ },
+ })
+ expect(hasWorkflowChanged(state1, state2)).toBe(true)
+ })
+
it.concurrent('should detect actual inputFormat changes', () => {
const state1 = createWorkflowState({
blocks: {
@@ -1712,15 +1739,15 @@ describe('hasWorkflowChanged', () => {
})
describe('Input Format Field Scenarios', () => {
- it.concurrent('should not detect change when inputFormat value is typed and cleared', () => {
- // The "value" field in inputFormat is UI-only and should be ignored
+ it.concurrent('should not detect change when only inputFormat collapsed changes', () => {
+ // The "collapsed" field in inputFormat is UI-only and should be ignored
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [
- { id: 'field1', name: 'Name', type: 'string', value: '', collapsed: false },
+ { id: 'field1', name: 'Name', type: 'string', value: 'test', collapsed: false },
],
},
},
@@ -1738,7 +1765,7 @@ describe('hasWorkflowChanged', () => {
id: 'field1',
name: 'Name',
type: 'string',
- value: 'typed then cleared',
+ value: 'test',
collapsed: true,
},
],
@@ -1748,10 +1775,40 @@ describe('hasWorkflowChanged', () => {
},
})
- // value and collapsed are UI-only fields - should NOT detect as change
+ // collapsed is UI-only field - should NOT detect as change
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
})
+ it.concurrent('should detect change when inputFormat value changes', () => {
+ // The "value" field in inputFormat is meaningful and should trigger change detection
+ const deployedState = createWorkflowState({
+ blocks: {
+ block1: createBlock('block1', {
+ subBlocks: {
+ inputFormat: {
+ value: [{ id: 'field1', name: 'Name', type: 'string', value: '' }],
+ },
+ },
+ }),
+ },
+ })
+
+ const currentState = createWorkflowState({
+ blocks: {
+ block1: createBlock('block1', {
+ subBlocks: {
+ inputFormat: {
+ value: [{ id: 'field1', name: 'Name', type: 'string', value: 'new value' }],
+ },
+ },
+ }),
+ },
+ })
+
+ // value changes should be detected
+ expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
+ })
+
it.concurrent('should detect change when inputFormat field name changes', () => {
const deployedState = createWorkflowState({
blocks: {
diff --git a/apps/sim/lib/workflows/comparison/normalize.test.ts b/apps/sim/lib/workflows/comparison/normalize.test.ts
index ca2220587..66bf52f34 100644
--- a/apps/sim/lib/workflows/comparison/normalize.test.ts
+++ b/apps/sim/lib/workflows/comparison/normalize.test.ts
@@ -370,7 +370,7 @@ describe('Workflow Normalization Utilities', () => {
expect(sanitizeInputFormat({} as any)).toEqual([])
})
- it.concurrent('should remove value and collapsed fields', () => {
+ it.concurrent('should remove collapsed field but keep value', () => {
const inputFormat = [
{ id: 'input1', name: 'Name', value: 'John', collapsed: true },
{ id: 'input2', name: 'Age', value: 25, collapsed: false },
@@ -379,13 +379,13 @@ describe('Workflow Normalization Utilities', () => {
const result = sanitizeInputFormat(inputFormat)
expect(result).toEqual([
- { id: 'input1', name: 'Name' },
- { id: 'input2', name: 'Age' },
+ { id: 'input1', name: 'Name', value: 'John' },
+ { id: 'input2', name: 'Age', value: 25 },
{ id: 'input3', name: 'Email' },
])
})
- it.concurrent('should preserve all other fields', () => {
+ it.concurrent('should preserve all other fields including value', () => {
const inputFormat = [
{
id: 'input1',
@@ -402,6 +402,7 @@ describe('Workflow Normalization Utilities', () => {
expect(result[0]).toEqual({
id: 'input1',
name: 'Complex Input',
+ value: 'test-value',
type: 'string',
required: true,
validation: { min: 0, max: 100 },
diff --git a/apps/sim/lib/workflows/comparison/normalize.ts b/apps/sim/lib/workflows/comparison/normalize.ts
index 571f20113..c467f73e0 100644
--- a/apps/sim/lib/workflows/comparison/normalize.ts
+++ b/apps/sim/lib/workflows/comparison/normalize.ts
@@ -156,10 +156,10 @@ export function normalizeVariables(variables: unknown): Record
}
/** Input format item with optional UI-only fields */
-type InputFormatItem = Record & { value?: unknown; collapsed?: boolean }
+type InputFormatItem = Record & { collapsed?: boolean }
/**
- * Sanitizes inputFormat array by removing UI-only fields like value and collapsed
+ * Sanitizes inputFormat array by removing UI-only fields like collapsed
* @param inputFormat - Array of input format configurations
* @returns Sanitized input format array
*/
@@ -167,7 +167,7 @@ export function sanitizeInputFormat(inputFormat: unknown[] | undefined): Record<
if (!Array.isArray(inputFormat)) return []
return inputFormat.map((item) => {
if (item && typeof item === 'object' && !Array.isArray(item)) {
- const { value, collapsed, ...rest } = item as InputFormatItem
+ const { collapsed, ...rest } = item as InputFormatItem
return rest
}
return item as Record
diff --git a/apps/sim/lib/workflows/variables/variable-manager.test.ts b/apps/sim/lib/workflows/variables/variable-manager.test.ts
index 4796ce19b..08da0cc85 100644
--- a/apps/sim/lib/workflows/variables/variable-manager.test.ts
+++ b/apps/sim/lib/workflows/variables/variable-manager.test.ts
@@ -26,7 +26,7 @@ describe('VariableManager', () => {
it.concurrent('should handle boolean type variables', () => {
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
- expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true)
+ expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
- expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true)
+ expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
})
diff --git a/apps/sim/lib/workflows/variables/variable-manager.ts b/apps/sim/lib/workflows/variables/variable-manager.ts
index 04ed5b9e4..7807d466c 100644
--- a/apps/sim/lib/workflows/variables/variable-manager.ts
+++ b/apps/sim/lib/workflows/variables/variable-manager.ts
@@ -61,7 +61,7 @@ export class VariableManager {
// Special case for 'anything else' in the test
if (unquoted === 'anything else') return true
const normalized = String(unquoted).toLowerCase().trim()
- return normalized === 'true' || normalized === '1'
+ return normalized === 'true'
}
case 'object':
diff --git a/apps/sim/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts
index d597557fb..29e277825 100644
--- a/apps/sim/providers/anthropic/index.ts
+++ b/apps/sim/providers/anthropic/index.ts
@@ -9,6 +9,7 @@ import {
generateToolUseId,
} from '@/providers/anthropic/utils'
import {
+ getMaxOutputTokensForModel,
getProviderDefaultModel,
getProviderModels,
supportsNativeStructuredOutputs,
@@ -178,7 +179,9 @@ export const anthropicProvider: ProviderConfig = {
model: request.model,
messages,
system: systemPrompt,
- max_tokens: Number.parseInt(String(request.maxTokens)) || 1024,
+ max_tokens:
+ Number.parseInt(String(request.maxTokens)) ||
+ getMaxOutputTokensForModel(request.model, request.stream ?? false),
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
}
diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts
index 4e614b8c2..1edee08d8 100644
--- a/apps/sim/providers/bedrock/index.ts
+++ b/apps/sim/providers/bedrock/index.ts
@@ -20,7 +20,11 @@ import {
generateToolUseId,
getBedrockInferenceProfileId,
} from '@/providers/bedrock/utils'
-import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
+import {
+ getMaxOutputTokensForModel,
+ getProviderDefaultModel,
+ getProviderModels,
+} from '@/providers/models'
import type {
ProviderConfig,
ProviderRequest,
@@ -259,7 +263,9 @@ export const bedrockProvider: ProviderConfig = {
const inferenceConfig = {
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
- maxTokens: Number.parseInt(String(request.maxTokens)) || 4096,
+ maxTokens:
+ Number.parseInt(String(request.maxTokens)) ||
+ getMaxOutputTokensForModel(request.model, request.stream ?? false),
}
const shouldStreamToolCalls = request.streamToolCalls ?? false
diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts
index eb655e66f..06ee14ae6 100644
--- a/apps/sim/providers/models.ts
+++ b/apps/sim/providers/models.ts
@@ -34,6 +34,12 @@ export interface ModelCapabilities {
toolUsageControl?: boolean
computerUse?: boolean
nativeStructuredOutputs?: boolean
+ maxOutputTokens?: {
+ /** Maximum tokens for streaming requests */
+ max: number
+ /** Safe default for non-streaming requests (to avoid timeout issues) */
+ default: number
+ }
reasoningEffort?: {
values: string[]
}
@@ -613,6 +619,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -627,6 +634,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -640,6 +648,7 @@ export const PROVIDER_DEFINITIONS: Record = {
},
capabilities: {
temperature: { min: 0, max: 1 },
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -654,6 +663,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -668,6 +678,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -681,6 +692,7 @@ export const PROVIDER_DEFINITIONS: Record = {
},
capabilities: {
temperature: { min: 0, max: 1 },
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -695,6 +707,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
computerUse: true,
+ maxOutputTokens: { max: 8192, default: 8192 },
},
contextWindow: 200000,
},
@@ -709,6 +722,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
computerUse: true,
+ maxOutputTokens: { max: 8192, default: 8192 },
},
contextWindow: 200000,
},
@@ -1655,6 +1669,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -1668,6 +1683,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -1681,6 +1697,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -1694,6 +1711,7 @@ export const PROVIDER_DEFINITIONS: Record = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
+ maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
@@ -2333,3 +2351,31 @@ export function getThinkingLevelsForModel(modelId: string): string[] | null {
const capability = getThinkingCapability(modelId)
return capability?.levels ?? null
}
+
+/**
+ * Get the max output tokens for a specific model
+ * Returns the model's max capacity for streaming requests,
+ * or the model's safe default for non-streaming requests to avoid timeout issues.
+ *
+ * @param modelId - The model ID
+ * @param streaming - Whether the request is streaming (default: false)
+ */
+export function getMaxOutputTokensForModel(modelId: string, streaming = false): number {
+ const normalizedModelId = modelId.toLowerCase()
+ const STANDARD_MAX_OUTPUT_TOKENS = 4096
+
+ for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
+ for (const model of provider.models) {
+ const baseModelId = model.id.toLowerCase()
+ if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
+ const outputTokens = model.capabilities.maxOutputTokens
+ if (outputTokens) {
+ return streaming ? outputTokens.max : outputTokens.default
+ }
+ return STANDARD_MAX_OUTPUT_TOKENS
+ }
+ }
+ }
+
+ return STANDARD_MAX_OUTPUT_TOKENS
+}
diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts
index def15fbb5..b064b4220 100644
--- a/apps/sim/providers/utils.ts
+++ b/apps/sim/providers/utils.ts
@@ -8,6 +8,7 @@ import {
getComputerUseModels,
getEmbeddingModelPricing,
getHostedModels as getHostedModelsFromDefinitions,
+ getMaxOutputTokensForModel as getMaxOutputTokensForModelFromDefinitions,
getMaxTemperature as getMaxTempFromDefinitions,
getModelPricing as getModelPricingFromDefinitions,
getModelsWithReasoningEffort,
@@ -992,6 +993,18 @@ export function getThinkingLevelsForModel(model: string): string[] | null {
return getThinkingLevelsForModelFromDefinitions(model)
}
+/**
+ * Get max output tokens for a specific model
+ * Returns the model's maxOutputTokens capability for streaming requests,
+ * or a conservative default (8192) for non-streaming requests to avoid timeout issues.
+ *
+ * @param model - The model ID
+ * @param streaming - Whether the request is streaming (default: false)
+ */
+export function getMaxOutputTokensForModel(model: string, streaming = false): number {
+ return getMaxOutputTokensForModelFromDefinitions(model, streaming)
+}
+
/**
* Prepare tool execution parameters, separating tool parameters from system parameters
*/
diff --git a/apps/sim/stores/undo-redo/code-storage.ts b/apps/sim/stores/undo-redo/code-storage.ts
new file mode 100644
index 000000000..05f86c82b
--- /dev/null
+++ b/apps/sim/stores/undo-redo/code-storage.ts
@@ -0,0 +1,36 @@
+import { createLogger } from '@sim/logger'
+import { del, get, set } from 'idb-keyval'
+import type { StateStorage } from 'zustand/middleware'
+
+const logger = createLogger('CodeUndoRedoStorage')
+
+export const codeUndoRedoStorage: StateStorage = {
+ getItem: async (name: string): Promise => {
+ if (typeof window === 'undefined') return null
+ try {
+ const value = await get(name)
+ return value ?? null
+ } catch (error) {
+ logger.warn('IndexedDB read failed', { name, error })
+ return null
+ }
+ },
+
+ setItem: async (name: string, value: string): Promise => {
+ if (typeof window === 'undefined') return
+ try {
+ await set(name, value)
+ } catch (error) {
+ logger.warn('IndexedDB write failed', { name, error })
+ }
+ },
+
+ removeItem: async (name: string): Promise => {
+ if (typeof window === 'undefined') return
+ try {
+ await del(name)
+ } catch (error) {
+ logger.warn('IndexedDB delete failed', { name, error })
+ }
+ },
+}
diff --git a/apps/sim/stores/undo-redo/code-store.ts b/apps/sim/stores/undo-redo/code-store.ts
new file mode 100644
index 000000000..c421126d5
--- /dev/null
+++ b/apps/sim/stores/undo-redo/code-store.ts
@@ -0,0 +1,151 @@
+import { create } from 'zustand'
+import { createJSONStorage, devtools, persist } from 'zustand/middleware'
+import { codeUndoRedoStorage } from '@/stores/undo-redo/code-storage'
+
+interface CodeUndoRedoEntry {
+ id: string
+ createdAt: number
+ workflowId: string
+ blockId: string
+ subBlockId: string
+ before: string
+ after: string
+}
+
+interface CodeUndoRedoStack {
+ undo: CodeUndoRedoEntry[]
+ redo: CodeUndoRedoEntry[]
+ lastUpdated?: number
+}
+
+interface CodeUndoRedoState {
+ stacks: Record
+ capacity: number
+ push: (entry: CodeUndoRedoEntry) => void
+ undo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null
+ redo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null
+ clear: (workflowId: string, blockId: string, subBlockId: string) => void
+}
+
+const DEFAULT_CAPACITY = 500
+const MAX_STACKS = 50
+
+function getStackKey(workflowId: string, blockId: string, subBlockId: string): string {
+ return `${workflowId}:${blockId}:${subBlockId}`
+}
+
+const initialState = {
+ stacks: {} as Record,
+ capacity: DEFAULT_CAPACITY,
+}
+
+export const useCodeUndoRedoStore = create()(
+ devtools(
+ persist(
+ (set, get) => ({
+ ...initialState,
+ push: (entry) => {
+ if (entry.before === entry.after) return
+
+ const state = get()
+ const key = getStackKey(entry.workflowId, entry.blockId, entry.subBlockId)
+ const currentStacks = { ...state.stacks }
+
+ const stackKeys = Object.keys(currentStacks)
+ if (stackKeys.length >= MAX_STACKS && !currentStacks[key]) {
+ let oldestKey: string | null = null
+ let oldestTime = Number.POSITIVE_INFINITY
+
+ for (const stackKey of stackKeys) {
+ const t = currentStacks[stackKey].lastUpdated ?? 0
+ if (t < oldestTime) {
+ oldestTime = t
+ oldestKey = stackKey
+ }
+ }
+
+ if (oldestKey) {
+ delete currentStacks[oldestKey]
+ }
+ }
+
+ const stack = currentStacks[key] || { undo: [], redo: [] }
+
+ const newUndo = [...stack.undo, entry]
+ if (newUndo.length > state.capacity) {
+ newUndo.shift()
+ }
+
+ currentStacks[key] = {
+ undo: newUndo,
+ redo: [],
+ lastUpdated: Date.now(),
+ }
+
+ set({ stacks: currentStacks })
+ },
+ undo: (workflowId, blockId, subBlockId) => {
+ const key = getStackKey(workflowId, blockId, subBlockId)
+ const state = get()
+ const stack = state.stacks[key]
+ if (!stack || stack.undo.length === 0) return null
+
+ const entry = stack.undo[stack.undo.length - 1]
+ const newUndo = stack.undo.slice(0, -1)
+ const newRedo = [...stack.redo, entry]
+
+ set({
+ stacks: {
+ ...state.stacks,
+ [key]: {
+ undo: newUndo,
+ redo: newRedo.slice(-state.capacity),
+ lastUpdated: Date.now(),
+ },
+ },
+ })
+
+ return entry
+ },
+ redo: (workflowId, blockId, subBlockId) => {
+ const key = getStackKey(workflowId, blockId, subBlockId)
+ const state = get()
+ const stack = state.stacks[key]
+ if (!stack || stack.redo.length === 0) return null
+
+ const entry = stack.redo[stack.redo.length - 1]
+ const newRedo = stack.redo.slice(0, -1)
+ const newUndo = [...stack.undo, entry]
+
+ set({
+ stacks: {
+ ...state.stacks,
+ [key]: {
+ undo: newUndo.slice(-state.capacity),
+ redo: newRedo,
+ lastUpdated: Date.now(),
+ },
+ },
+ })
+
+ return entry
+ },
+ clear: (workflowId, blockId, subBlockId) => {
+ const key = getStackKey(workflowId, blockId, subBlockId)
+ const state = get()
+ const { [key]: _, ...rest } = state.stacks
+ set({ stacks: rest })
+ },
+ }),
+ {
+ name: 'code-undo-redo-store',
+ storage: createJSONStorage(() => codeUndoRedoStorage),
+ partialize: (state) => ({
+ stacks: state.stacks,
+ capacity: state.capacity,
+ }),
+ }
+ ),
+ { name: 'code-undo-redo-store' }
+ )
+)
diff --git a/apps/sim/stores/undo-redo/index.ts b/apps/sim/stores/undo-redo/index.ts
index d97adabaf..5c9d815fc 100644
--- a/apps/sim/stores/undo-redo/index.ts
+++ b/apps/sim/stores/undo-redo/index.ts
@@ -1,3 +1,4 @@
+export { useCodeUndoRedoStore } from './code-store'
export { runWithUndoRedoRecordingSuspended, useUndoRedoStore } from './store'
export * from './types'
export * from './utils'
diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts
index e5a6f5381..9dbeeb5b6 100644
--- a/apps/sim/tools/browser_use/run_task.ts
+++ b/apps/sim/tools/browser_use/run_task.ts
@@ -5,7 +5,7 @@ import type { ToolConfig, ToolResponse } from '@/tools/types'
const logger = createLogger('BrowserUseTool')
const POLL_INTERVAL_MS = 5000
-const MAX_POLL_TIME_MS = 180000
+const MAX_POLL_TIME_MS = 600000 // 10 minutes
const MAX_CONSECUTIVE_ERRORS = 3
async function createSessionWithProfile(
diff --git a/apps/sim/tools/intercom/assign_conversation.ts b/apps/sim/tools/intercom/assign_conversation.ts
new file mode 100644
index 000000000..1780c52c9
--- /dev/null
+++ b/apps/sim/tools/intercom/assign_conversation.ts
@@ -0,0 +1,146 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomAssignConversationParams {
+ accessToken: string
+ conversationId: string
+ admin_id: string
+ assignee_id: string
+ body?: string
+}
+
+export interface IntercomAssignConversationV2Response {
+ success: boolean
+ output: {
+ conversation: any
+ conversationId: string
+ admin_assignee_id: number | null
+ team_assignee_id: string | null
+ }
+}
+
+const assignConversationBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ conversationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the conversation to assign',
+ },
+ admin_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin performing the assignment',
+ },
+ assignee_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the admin or team to assign the conversation to. Set to "0" to unassign.',
+ },
+ body: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Optional message to add when assigning (e.g., "Passing to the support team")',
+ },
+ },
+
+ request: {
+ url: (params: IntercomAssignConversationParams) =>
+ buildIntercomUrl(`/conversations/${params.conversationId}/parts`),
+ method: 'POST',
+ headers: (params: IntercomAssignConversationParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomAssignConversationParams) => {
+ const payload: any = {
+ message_type: 'assignment',
+ type: 'admin',
+ admin_id: params.admin_id,
+ assignee_id: params.assignee_id,
+ }
+
+ if (params.body) {
+ payload.body = params.body
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomAssignConversationV2Tool: ToolConfig<
+ IntercomAssignConversationParams,
+ IntercomAssignConversationV2Response
+> = {
+ ...assignConversationBase,
+ id: 'intercom_assign_conversation_v2',
+ name: 'Assign Conversation in Intercom',
+ description: 'Assign a conversation to an admin or team in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'assign_conversation')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ conversation: data,
+ conversationId: data.id,
+ admin_assignee_id: data.admin_assignee_id ?? null,
+ team_assignee_id: data.team_assignee_id ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ conversation: {
+ type: 'object',
+ description: 'The assigned conversation object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the conversation' },
+ type: { type: 'string', description: 'Object type (conversation)' },
+ state: { type: 'string', description: 'State of the conversation' },
+ open: { type: 'boolean', description: 'Whether the conversation is open' },
+ admin_assignee_id: {
+ type: 'number',
+ description: 'ID of the assigned admin',
+ optional: true,
+ },
+ team_assignee_id: {
+ type: 'string',
+ description: 'ID of the assigned team',
+ optional: true,
+ },
+ created_at: { type: 'number', description: 'Unix timestamp when conversation was created' },
+ updated_at: {
+ type: 'number',
+ description: 'Unix timestamp when conversation was last updated',
+ },
+ },
+ },
+ conversationId: { type: 'string', description: 'ID of the assigned conversation' },
+ admin_assignee_id: {
+ type: 'number',
+ description: 'ID of the assigned admin',
+ optional: true,
+ },
+ team_assignee_id: { type: 'string', description: 'ID of the assigned team', optional: true },
+ },
+}
diff --git a/apps/sim/tools/intercom/attach_contact_to_company.ts b/apps/sim/tools/intercom/attach_contact_to_company.ts
new file mode 100644
index 000000000..9f6cf834b
--- /dev/null
+++ b/apps/sim/tools/intercom/attach_contact_to_company.ts
@@ -0,0 +1,115 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomAttachContactToCompanyParams {
+ accessToken: string
+ contactId: string
+ companyId: string
+}
+
+export interface IntercomAttachContactToCompanyV2Response {
+ success: boolean
+ output: {
+ company: any
+ companyId: string
+ name: string | null
+ }
+}
+
+const attachContactToCompanyBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ contactId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the contact to attach to the company',
+ },
+ companyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the company to attach the contact to',
+ },
+ },
+
+ request: {
+ url: (params: IntercomAttachContactToCompanyParams) =>
+ buildIntercomUrl(`/contacts/${params.contactId}/companies`),
+ method: 'POST',
+ headers: (params: IntercomAttachContactToCompanyParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomAttachContactToCompanyParams) => ({
+ id: params.companyId,
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomAttachContactToCompanyV2Tool: ToolConfig<
+ IntercomAttachContactToCompanyParams,
+ IntercomAttachContactToCompanyV2Response
+> = {
+ ...attachContactToCompanyBase,
+ id: 'intercom_attach_contact_to_company_v2',
+ name: 'Attach Contact to Company in Intercom',
+ description: 'Attach a contact to a company in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'attach_contact_to_company')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ company: {
+ id: data.id,
+ type: data.type ?? 'company',
+ company_id: data.company_id ?? null,
+ name: data.name ?? null,
+ created_at: data.created_at ?? null,
+ updated_at: data.updated_at ?? null,
+ user_count: data.user_count ?? null,
+ session_count: data.session_count ?? null,
+ monthly_spend: data.monthly_spend ?? null,
+ plan: data.plan ?? null,
+ },
+ companyId: data.id,
+ name: data.name ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ company: {
+ type: 'object',
+ description: 'The company object the contact was attached to',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the company' },
+ type: { type: 'string', description: 'Object type (company)' },
+ company_id: { type: 'string', description: 'The company_id you defined' },
+ name: { type: 'string', description: 'Name of the company' },
+ created_at: { type: 'number', description: 'Unix timestamp when company was created' },
+ updated_at: { type: 'number', description: 'Unix timestamp when company was updated' },
+ user_count: { type: 'number', description: 'Number of users in the company' },
+ session_count: { type: 'number', description: 'Number of sessions' },
+ monthly_spend: { type: 'number', description: 'Monthly spend amount' },
+ plan: { type: 'object', description: 'Company plan details' },
+ },
+ },
+ companyId: { type: 'string', description: 'ID of the company' },
+ name: { type: 'string', description: 'Name of the company', optional: true },
+ },
+}
diff --git a/apps/sim/tools/intercom/close_conversation.ts b/apps/sim/tools/intercom/close_conversation.ts
new file mode 100644
index 000000000..424b5aa63
--- /dev/null
+++ b/apps/sim/tools/intercom/close_conversation.ts
@@ -0,0 +1,129 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomCloseConversationParams {
+ accessToken: string
+ conversationId: string
+ admin_id: string
+ body?: string
+}
+
+export interface IntercomCloseConversationV2Response {
+ success: boolean
+ output: {
+ conversation: any
+ conversationId: string
+ state: string
+ }
+}
+
+const closeConversationBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ conversationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the conversation to close',
+ },
+ admin_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin performing the action',
+ },
+ body: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Optional closing message to add to the conversation',
+ },
+ },
+
+ request: {
+ url: (params: IntercomCloseConversationParams) =>
+ buildIntercomUrl(`/conversations/${params.conversationId}/parts`),
+ method: 'POST',
+ headers: (params: IntercomCloseConversationParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomCloseConversationParams) => {
+ const payload: any = {
+ message_type: 'close',
+ type: 'admin',
+ admin_id: params.admin_id,
+ }
+
+ if (params.body) {
+ payload.body = params.body
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomCloseConversationV2Tool: ToolConfig<
+ IntercomCloseConversationParams,
+ IntercomCloseConversationV2Response
+> = {
+ ...closeConversationBase,
+ id: 'intercom_close_conversation_v2',
+ name: 'Close Conversation in Intercom',
+ description: 'Close a conversation in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'close_conversation')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ conversation: {
+ id: data.id,
+ type: data.type ?? 'conversation',
+ state: data.state ?? 'closed',
+ open: data.open ?? false,
+ read: data.read ?? false,
+ created_at: data.created_at ?? null,
+ updated_at: data.updated_at ?? null,
+ },
+ conversationId: data.id,
+ state: data.state ?? 'closed',
+ },
+ }
+ },
+
+ outputs: {
+ conversation: {
+ type: 'object',
+ description: 'The closed conversation object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the conversation' },
+ type: { type: 'string', description: 'Object type (conversation)' },
+ state: { type: 'string', description: 'State of the conversation (closed)' },
+ open: { type: 'boolean', description: 'Whether the conversation is open (false)' },
+ read: { type: 'boolean', description: 'Whether the conversation has been read' },
+ created_at: { type: 'number', description: 'Unix timestamp when conversation was created' },
+ updated_at: {
+ type: 'number',
+ description: 'Unix timestamp when conversation was last updated',
+ },
+ },
+ },
+ conversationId: { type: 'string', description: 'ID of the closed conversation' },
+ state: { type: 'string', description: 'State of the conversation (closed)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/create_company.ts b/apps/sim/tools/intercom/create_company.ts
index 5795afce3..f8848868a 100644
--- a/apps/sim/tools/intercom/create_company.ts
+++ b/apps/sim/tools/intercom/create_company.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomCreateCompany')
@@ -60,7 +60,7 @@ const createCompanyBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
company_id: {
diff --git a/apps/sim/tools/intercom/create_contact.ts b/apps/sim/tools/intercom/create_contact.ts
index 6e9a15a99..9d8aded58 100644
--- a/apps/sim/tools/intercom/create_contact.ts
+++ b/apps/sim/tools/intercom/create_contact.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomCreateContact')
@@ -88,7 +88,7 @@ const intercomCreateContactBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
role: {
diff --git a/apps/sim/tools/intercom/create_event.ts b/apps/sim/tools/intercom/create_event.ts
new file mode 100644
index 000000000..016ade9d2
--- /dev/null
+++ b/apps/sim/tools/intercom/create_event.ts
@@ -0,0 +1,148 @@
+import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+const logger = createLogger('IntercomCreateEvent')
+
+export interface IntercomCreateEventParams {
+ accessToken: string
+ event_name: string
+ created_at?: number
+ user_id?: string
+ email?: string
+ id?: string
+ metadata?: string
+}
+
+export interface IntercomCreateEventV2Response {
+ success: boolean
+ output: {
+ accepted: boolean
+ }
+}
+
+const createEventBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ event_name: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The name of the event (e.g., "order-completed"). Use past-tense verb-noun format for readability.',
+ },
+ created_at: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'Unix timestamp for when the event occurred. Strongly recommended for uniqueness.',
+ },
+ user_id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Your identifier for the user (external_id)',
+ },
+ email: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'Email address of the user. Use only if your app uses email to uniquely identify users.',
+ },
+ id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The Intercom contact ID',
+ },
+ metadata: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'JSON object with up to 10 metadata key-value pairs about the event (e.g., {"order_value": 99.99})',
+ },
+ },
+
+ request: {
+ url: () => buildIntercomUrl('/events'),
+ method: 'POST',
+ headers: (params: IntercomCreateEventParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomCreateEventParams) => {
+ const payload: any = {
+ event_name: params.event_name,
+ }
+
+ if (params.created_at) {
+ payload.created_at = params.created_at
+ } else {
+ payload.created_at = Math.floor(Date.now() / 1000)
+ }
+
+ if (params.user_id) {
+ payload.user_id = params.user_id
+ }
+
+ if (params.email) {
+ payload.email = params.email
+ }
+
+ if (params.id) {
+ payload.id = params.id
+ }
+
+ if (params.metadata) {
+ try {
+ payload.metadata = JSON.parse(params.metadata)
+ } catch (error) {
+ logger.warn('Failed to parse metadata, ignoring', { error })
+ }
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomCreateEventV2Tool: ToolConfig<
+ IntercomCreateEventParams,
+ IntercomCreateEventV2Response
+> = {
+ ...createEventBase,
+ id: 'intercom_create_event_v2',
+ name: 'Create Event in Intercom',
+ description: 'Track a custom event for a contact in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'create_event')
+ }
+
+ return {
+ success: true,
+ output: {
+ accepted: true,
+ },
+ }
+ },
+
+ outputs: {
+ accepted: {
+ type: 'boolean',
+ description: 'Whether the event was accepted (202 Accepted)',
+ },
+ },
+}
diff --git a/apps/sim/tools/intercom/create_message.ts b/apps/sim/tools/intercom/create_message.ts
index 2cea1ce07..0b6ef2fc5 100644
--- a/apps/sim/tools/intercom/create_message.ts
+++ b/apps/sim/tools/intercom/create_message.ts
@@ -1,5 +1,5 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
export interface IntercomCreateMessageParams {
accessToken: string
@@ -40,7 +40,7 @@ const createMessageBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
message_type: {
diff --git a/apps/sim/tools/intercom/create_note.ts b/apps/sim/tools/intercom/create_note.ts
new file mode 100644
index 000000000..56fa3df26
--- /dev/null
+++ b/apps/sim/tools/intercom/create_note.ts
@@ -0,0 +1,131 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomCreateNoteParams {
+ accessToken: string
+ contactId: string
+ body: string
+ admin_id?: string
+}
+
+export interface IntercomCreateNoteV2Response {
+ success: boolean
+ output: {
+ id: string
+ body: string
+ created_at: number
+ type: string
+ author: any | null
+ contact: any | null
+ }
+}
+
+const createNoteBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ contactId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the contact to add the note to',
+ },
+ body: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The text content of the note',
+ },
+ admin_id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin creating the note',
+ },
+ },
+
+ request: {
+ url: (params: IntercomCreateNoteParams) =>
+ buildIntercomUrl(`/contacts/${params.contactId}/notes`),
+ method: 'POST',
+ headers: (params: IntercomCreateNoteParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomCreateNoteParams) => {
+ const payload: any = {
+ body: params.body,
+ }
+
+ if (params.admin_id) {
+ payload.admin_id = params.admin_id
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomCreateNoteV2Tool: ToolConfig<
+ IntercomCreateNoteParams,
+ IntercomCreateNoteV2Response
+> = {
+ ...createNoteBase,
+ id: 'intercom_create_note_v2',
+ name: 'Create Note in Intercom',
+ description: 'Add a note to a specific contact',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'create_note')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ id: data.id,
+ body: data.body,
+ created_at: data.created_at,
+ type: data.type ?? 'note',
+ author: data.author ?? null,
+ contact: data.contact ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ id: { type: 'string', description: 'Unique identifier for the note' },
+ body: { type: 'string', description: 'The text content of the note' },
+ created_at: { type: 'number', description: 'Unix timestamp when the note was created' },
+ type: { type: 'string', description: 'Object type (note)' },
+ author: {
+ type: 'object',
+ description: 'The admin who created the note',
+ optional: true,
+ properties: {
+ type: { type: 'string', description: 'Author type (admin)' },
+ id: { type: 'string', description: 'Author ID' },
+ name: { type: 'string', description: 'Author name' },
+ email: { type: 'string', description: 'Author email' },
+ },
+ },
+ contact: {
+ type: 'object',
+ description: 'The contact the note was created for',
+ optional: true,
+ properties: {
+ type: { type: 'string', description: 'Contact type' },
+ id: { type: 'string', description: 'Contact ID' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/intercom/create_tag.ts b/apps/sim/tools/intercom/create_tag.ts
new file mode 100644
index 000000000..85561f180
--- /dev/null
+++ b/apps/sim/tools/intercom/create_tag.ts
@@ -0,0 +1,97 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomCreateTagParams {
+ accessToken: string
+ name: string
+ id?: string
+}
+
+export interface IntercomCreateTagV2Response {
+ success: boolean
+ output: {
+ id: string
+ name: string
+ type: string
+ }
+}
+
+const createTagBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ name: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'The name of the tag. Will create a new tag if not found, or update the name if id is provided.',
+ },
+ id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The ID of an existing tag to update. Omit to create a new tag.',
+ },
+ },
+
+ request: {
+ url: () => buildIntercomUrl('/tags'),
+ method: 'POST',
+ headers: (params: IntercomCreateTagParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomCreateTagParams) => {
+ const payload: any = {
+ name: params.name,
+ }
+
+ if (params.id) {
+ payload.id = params.id
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomCreateTagV2Tool: ToolConfig<
+ IntercomCreateTagParams,
+ IntercomCreateTagV2Response
+> = {
+ ...createTagBase,
+ id: 'intercom_create_tag_v2',
+ name: 'Create Tag in Intercom',
+ description: 'Create a new tag or update an existing tag name',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'create_tag')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ id: data.id,
+ name: data.name,
+ type: data.type ?? 'tag',
+ },
+ }
+ },
+
+ outputs: {
+ id: { type: 'string', description: 'Unique identifier for the tag' },
+ name: { type: 'string', description: 'Name of the tag' },
+ type: { type: 'string', description: 'Object type (tag)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/create_ticket.ts b/apps/sim/tools/intercom/create_ticket.ts
index 34fae4838..cdcf83375 100644
--- a/apps/sim/tools/intercom/create_ticket.ts
+++ b/apps/sim/tools/intercom/create_ticket.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomCreateTicket')
@@ -41,7 +41,7 @@ const createTicketBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
ticket_type_id: {
diff --git a/apps/sim/tools/intercom/delete_contact.ts b/apps/sim/tools/intercom/delete_contact.ts
index 5b5bd9432..87e01e3cf 100644
--- a/apps/sim/tools/intercom/delete_contact.ts
+++ b/apps/sim/tools/intercom/delete_contact.ts
@@ -1,5 +1,5 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
export interface IntercomDeleteContactParams {
accessToken: string
@@ -23,7 +23,7 @@ const intercomDeleteContactBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
contactId: {
diff --git a/apps/sim/tools/intercom/detach_contact_from_company.ts b/apps/sim/tools/intercom/detach_contact_from_company.ts
new file mode 100644
index 000000000..df18e4998
--- /dev/null
+++ b/apps/sim/tools/intercom/detach_contact_from_company.ts
@@ -0,0 +1,101 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomDetachContactFromCompanyParams {
+ accessToken: string
+ contactId: string
+ companyId: string
+}
+
+export interface IntercomDetachContactFromCompanyV2Response {
+ success: boolean
+ output: {
+ company: any
+ companyId: string
+ name: string | null
+ }
+}
+
+const detachContactFromCompanyBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ contactId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the contact to detach from the company',
+ },
+ companyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the company to detach the contact from',
+ },
+ },
+
+ request: {
+ url: (params: IntercomDetachContactFromCompanyParams) =>
+ buildIntercomUrl(`/contacts/${params.contactId}/companies/${params.companyId}`),
+ method: 'DELETE',
+ headers: (params: IntercomDetachContactFromCompanyParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomDetachContactFromCompanyV2Tool: ToolConfig<
+ IntercomDetachContactFromCompanyParams,
+ IntercomDetachContactFromCompanyV2Response
+> = {
+ ...detachContactFromCompanyBase,
+ id: 'intercom_detach_contact_from_company_v2',
+ name: 'Detach Contact from Company in Intercom',
+ description: 'Remove a contact from a company in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'detach_contact_from_company')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ company: {
+ id: data.id,
+ type: data.type ?? 'company',
+ company_id: data.company_id ?? null,
+ name: data.name ?? null,
+ },
+ companyId: data.id,
+ name: data.name ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ company: {
+ type: 'object',
+ description: 'The company object the contact was detached from',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the company' },
+ type: { type: 'string', description: 'Object type (company)' },
+ company_id: { type: 'string', description: 'The company_id you defined' },
+ name: { type: 'string', description: 'Name of the company' },
+ },
+ },
+ companyId: { type: 'string', description: 'ID of the company' },
+ name: { type: 'string', description: 'Name of the company', optional: true },
+ },
+}
diff --git a/apps/sim/tools/intercom/get_company.ts b/apps/sim/tools/intercom/get_company.ts
index ae311a074..1d2474d02 100644
--- a/apps/sim/tools/intercom/get_company.ts
+++ b/apps/sim/tools/intercom/get_company.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomGetCompany')
@@ -25,7 +25,7 @@ const getCompanyBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
companyId: {
diff --git a/apps/sim/tools/intercom/get_contact.ts b/apps/sim/tools/intercom/get_contact.ts
index bcba2d892..1090116ed 100644
--- a/apps/sim/tools/intercom/get_contact.ts
+++ b/apps/sim/tools/intercom/get_contact.ts
@@ -1,5 +1,5 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
export interface IntercomGetContactParams {
accessToken: string
@@ -22,7 +22,7 @@ const intercomGetContactBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
contactId: {
diff --git a/apps/sim/tools/intercom/get_conversation.ts b/apps/sim/tools/intercom/get_conversation.ts
index b5f708fd9..21413b226 100644
--- a/apps/sim/tools/intercom/get_conversation.ts
+++ b/apps/sim/tools/intercom/get_conversation.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomGetConversation')
@@ -27,7 +27,7 @@ const getConversationBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
conversationId: {
diff --git a/apps/sim/tools/intercom/get_ticket.ts b/apps/sim/tools/intercom/get_ticket.ts
index da111a6bf..1febb2b7e 100644
--- a/apps/sim/tools/intercom/get_ticket.ts
+++ b/apps/sim/tools/intercom/get_ticket.ts
@@ -1,5 +1,5 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
export interface IntercomGetTicketParams {
accessToken: string
@@ -31,7 +31,7 @@ const getTicketBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
ticketId: {
diff --git a/apps/sim/tools/intercom/index.ts b/apps/sim/tools/intercom/index.ts
index 626c93cff..143bca0c6 100644
--- a/apps/sim/tools/intercom/index.ts
+++ b/apps/sim/tools/intercom/index.ts
@@ -1,24 +1,28 @@
-// Contact tools
-
-// Company tools
+export { intercomAssignConversationV2Tool } from './assign_conversation'
+export { intercomAttachContactToCompanyV2Tool } from './attach_contact_to_company'
+export { intercomCloseConversationV2Tool } from './close_conversation'
export { intercomCreateCompanyTool, intercomCreateCompanyV2Tool } from './create_company'
export { intercomCreateContactTool, intercomCreateContactV2Tool } from './create_contact'
-// Message tools
+export { intercomCreateEventV2Tool } from './create_event'
export { intercomCreateMessageTool, intercomCreateMessageV2Tool } from './create_message'
-// Ticket tools
+export { intercomCreateNoteV2Tool } from './create_note'
+export { intercomCreateTagV2Tool } from './create_tag'
export { intercomCreateTicketTool, intercomCreateTicketV2Tool } from './create_ticket'
export { intercomDeleteContactTool, intercomDeleteContactV2Tool } from './delete_contact'
+export { intercomDetachContactFromCompanyV2Tool } from './detach_contact_from_company'
export { intercomGetCompanyTool, intercomGetCompanyV2Tool } from './get_company'
export { intercomGetContactTool, intercomGetContactV2Tool } from './get_contact'
-// Conversation tools
export { intercomGetConversationTool, intercomGetConversationV2Tool } from './get_conversation'
export { intercomGetTicketTool, intercomGetTicketV2Tool } from './get_ticket'
+export { intercomListAdminsV2Tool } from './list_admins'
export { intercomListCompaniesTool, intercomListCompaniesV2Tool } from './list_companies'
export { intercomListContactsTool, intercomListContactsV2Tool } from './list_contacts'
export {
intercomListConversationsTool,
intercomListConversationsV2Tool,
} from './list_conversations'
+export { intercomListTagsV2Tool } from './list_tags'
+export { intercomOpenConversationV2Tool } from './open_conversation'
export {
intercomReplyConversationTool,
intercomReplyConversationV2Tool,
@@ -28,4 +32,9 @@ export {
intercomSearchConversationsTool,
intercomSearchConversationsV2Tool,
} from './search_conversations'
+export { intercomSnoozeConversationV2Tool } from './snooze_conversation'
+export { intercomTagContactV2Tool } from './tag_contact'
+export { intercomTagConversationV2Tool } from './tag_conversation'
+export { intercomUntagContactV2Tool } from './untag_contact'
export { intercomUpdateContactTool, intercomUpdateContactV2Tool } from './update_contact'
+export { intercomUpdateTicketV2Tool } from './update_ticket'
diff --git a/apps/sim/tools/intercom/list_admins.ts b/apps/sim/tools/intercom/list_admins.ts
new file mode 100644
index 000000000..d6ce5c81e
--- /dev/null
+++ b/apps/sim/tools/intercom/list_admins.ts
@@ -0,0 +1,125 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomListAdminsParams {
+ accessToken: string
+}
+
+interface IntercomAdmin {
+ type: string
+ id: string
+ name: string
+ email: string
+ job_title: string | null
+ away_mode_enabled: boolean
+ away_mode_reassign: boolean
+ has_inbox_seat: boolean
+ team_ids: number[]
+ avatar: {
+ type: string
+ image_url: string | null
+ } | null
+ email_verified: boolean | null
+}
+
+export interface IntercomListAdminsV2Response {
+ success: boolean
+ output: {
+ admins: IntercomAdmin[]
+ type: string
+ }
+}
+
+const listAdminsBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ },
+
+ request: {
+ url: () => buildIntercomUrl('/admins'),
+ method: 'GET',
+ headers: (params: IntercomListAdminsParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomListAdminsV2Tool: ToolConfig<
+ IntercomListAdminsParams,
+ IntercomListAdminsV2Response
+> = {
+ ...listAdminsBase,
+ id: 'intercom_list_admins_v2',
+ name: 'List Admins from Intercom',
+ description: 'Fetch a list of all admins for the workspace',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'list_admins')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ admins: data.admins ?? [],
+ type: data.type ?? 'admin.list',
+ },
+ }
+ },
+
+ outputs: {
+ admins: {
+ type: 'array',
+ description: 'Array of admin objects',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the admin' },
+ type: { type: 'string', description: 'Object type (admin)' },
+ name: { type: 'string', description: 'Name of the admin' },
+ email: { type: 'string', description: 'Email of the admin' },
+ job_title: { type: 'string', description: 'Job title of the admin', optional: true },
+ away_mode_enabled: {
+ type: 'boolean',
+ description: 'Whether admin is in away mode',
+ },
+ away_mode_reassign: {
+ type: 'boolean',
+ description: 'Whether to reassign conversations when away',
+ },
+ has_inbox_seat: {
+ type: 'boolean',
+ description: 'Whether admin has a paid inbox seat',
+ },
+ team_ids: {
+ type: 'array',
+ description: 'List of team IDs the admin belongs to',
+ },
+ avatar: {
+ type: 'object',
+ description: 'Avatar information',
+ optional: true,
+ },
+ email_verified: {
+ type: 'boolean',
+ description: 'Whether email is verified',
+ optional: true,
+ },
+ },
+ },
+ },
+ type: { type: 'string', description: 'Object type (admin.list)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/list_companies.ts b/apps/sim/tools/intercom/list_companies.ts
index d06030405..10f95ac10 100644
--- a/apps/sim/tools/intercom/list_companies.ts
+++ b/apps/sim/tools/intercom/list_companies.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomListCompanies')
@@ -29,7 +29,7 @@ const listCompaniesBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
per_page: {
diff --git a/apps/sim/tools/intercom/list_contacts.ts b/apps/sim/tools/intercom/list_contacts.ts
index 44d7b5c06..b3f483788 100644
--- a/apps/sim/tools/intercom/list_contacts.ts
+++ b/apps/sim/tools/intercom/list_contacts.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomListContacts')
@@ -28,7 +28,7 @@ const listContactsBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
per_page: {
diff --git a/apps/sim/tools/intercom/list_conversations.ts b/apps/sim/tools/intercom/list_conversations.ts
index 50542515d..92b52060c 100644
--- a/apps/sim/tools/intercom/list_conversations.ts
+++ b/apps/sim/tools/intercom/list_conversations.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomListConversations')
@@ -30,7 +30,7 @@ const listConversationsBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
per_page: {
diff --git a/apps/sim/tools/intercom/list_tags.ts b/apps/sim/tools/intercom/list_tags.ts
new file mode 100644
index 000000000..c59fc2899
--- /dev/null
+++ b/apps/sim/tools/intercom/list_tags.ts
@@ -0,0 +1,86 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomListTagsParams {
+ accessToken: string
+}
+
+interface IntercomTag {
+ type: string
+ id: string
+ name: string
+}
+
+export interface IntercomListTagsV2Response {
+ success: boolean
+ output: {
+ tags: IntercomTag[]
+ type: string
+ }
+}
+
+const listTagsBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ },
+
+ request: {
+ url: () => buildIntercomUrl('/tags'),
+ method: 'GET',
+ headers: (params: IntercomListTagsParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomListTagsV2Tool: ToolConfig<
+ IntercomListTagsParams,
+ IntercomListTagsV2Response
+> = {
+ ...listTagsBase,
+ id: 'intercom_list_tags_v2',
+ name: 'List Tags from Intercom',
+ description: 'Fetch a list of all tags in the workspace',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'list_tags')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ tags: data.tags ?? [],
+ type: data.type ?? 'tag.list',
+ },
+ }
+ },
+
+ outputs: {
+ tags: {
+ type: 'array',
+ description: 'Array of tag objects',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the tag' },
+ type: { type: 'string', description: 'Object type (tag)' },
+ name: { type: 'string', description: 'Name of the tag' },
+ },
+ },
+ },
+ type: { type: 'string', description: 'Object type (list)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/open_conversation.ts b/apps/sim/tools/intercom/open_conversation.ts
new file mode 100644
index 000000000..243d889b3
--- /dev/null
+++ b/apps/sim/tools/intercom/open_conversation.ts
@@ -0,0 +1,114 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomOpenConversationParams {
+ accessToken: string
+ conversationId: string
+ admin_id: string
+}
+
+export interface IntercomOpenConversationV2Response {
+ success: boolean
+ output: {
+ conversation: any
+ conversationId: string
+ state: string
+ }
+}
+
+const openConversationBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ conversationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the conversation to open',
+ },
+ admin_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin performing the action',
+ },
+ },
+
+ request: {
+ url: (params: IntercomOpenConversationParams) =>
+ buildIntercomUrl(`/conversations/${params.conversationId}/parts`),
+ method: 'POST',
+ headers: (params: IntercomOpenConversationParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomOpenConversationParams) => ({
+ message_type: 'open',
+ type: 'admin',
+ admin_id: params.admin_id,
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomOpenConversationV2Tool: ToolConfig<
+ IntercomOpenConversationParams,
+ IntercomOpenConversationV2Response
+> = {
+ ...openConversationBase,
+ id: 'intercom_open_conversation_v2',
+ name: 'Open Conversation in Intercom',
+ description: 'Open a closed or snoozed conversation in Intercom',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'open_conversation')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ conversation: {
+ id: data.id,
+ type: data.type ?? 'conversation',
+ state: data.state ?? 'open',
+ open: data.open ?? true,
+ read: data.read ?? false,
+ created_at: data.created_at ?? null,
+ updated_at: data.updated_at ?? null,
+ },
+ conversationId: data.id,
+ state: data.state ?? 'open',
+ },
+ }
+ },
+
+ outputs: {
+ conversation: {
+ type: 'object',
+ description: 'The opened conversation object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the conversation' },
+ type: { type: 'string', description: 'Object type (conversation)' },
+ state: { type: 'string', description: 'State of the conversation (open)' },
+ open: { type: 'boolean', description: 'Whether the conversation is open (true)' },
+ read: { type: 'boolean', description: 'Whether the conversation has been read' },
+ created_at: { type: 'number', description: 'Unix timestamp when conversation was created' },
+ updated_at: {
+ type: 'number',
+ description: 'Unix timestamp when conversation was last updated',
+ },
+ },
+ },
+ conversationId: { type: 'string', description: 'ID of the opened conversation' },
+ state: { type: 'string', description: 'State of the conversation (open)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/reply_conversation.ts b/apps/sim/tools/intercom/reply_conversation.ts
index a6f8cb18f..bf6304af8 100644
--- a/apps/sim/tools/intercom/reply_conversation.ts
+++ b/apps/sim/tools/intercom/reply_conversation.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomReplyConversation')
@@ -31,7 +31,7 @@ const replyConversationBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
conversationId: {
diff --git a/apps/sim/tools/intercom/search_contacts.ts b/apps/sim/tools/intercom/search_contacts.ts
index 5c4d7788c..32d314523 100644
--- a/apps/sim/tools/intercom/search_contacts.ts
+++ b/apps/sim/tools/intercom/search_contacts.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomSearchContacts')
@@ -87,7 +87,7 @@ const searchContactsBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
query: {
diff --git a/apps/sim/tools/intercom/search_conversations.ts b/apps/sim/tools/intercom/search_conversations.ts
index 8ab9c8f71..4b2c36752 100644
--- a/apps/sim/tools/intercom/search_conversations.ts
+++ b/apps/sim/tools/intercom/search_conversations.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomSearchConversations')
@@ -41,7 +41,7 @@ const searchConversationsBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
query: {
diff --git a/apps/sim/tools/intercom/snooze_conversation.ts b/apps/sim/tools/intercom/snooze_conversation.ts
new file mode 100644
index 000000000..717e821e8
--- /dev/null
+++ b/apps/sim/tools/intercom/snooze_conversation.ts
@@ -0,0 +1,124 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomSnoozeConversationParams {
+ accessToken: string
+ conversationId: string
+ admin_id: string
+ snoozed_until: number
+}
+
+export interface IntercomSnoozeConversationV2Response {
+ success: boolean
+ output: {
+ conversation: any
+ conversationId: string
+ state: string
+ snoozed_until: number | null
+ }
+}
+
+const snoozeConversationBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ conversationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the conversation to snooze',
+ },
+ admin_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin performing the action',
+ },
+ snoozed_until: {
+ type: 'number',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Unix timestamp for when the conversation should reopen',
+ },
+ },
+
+ request: {
+ url: (params: IntercomSnoozeConversationParams) =>
+ buildIntercomUrl(`/conversations/${params.conversationId}/reply`),
+ method: 'POST',
+ headers: (params: IntercomSnoozeConversationParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomSnoozeConversationParams) => ({
+ message_type: 'snoozed',
+ admin_id: params.admin_id,
+ snoozed_until: params.snoozed_until,
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomSnoozeConversationV2Tool: ToolConfig<
+ IntercomSnoozeConversationParams,
+ IntercomSnoozeConversationV2Response
+> = {
+ ...snoozeConversationBase,
+ id: 'intercom_snooze_conversation_v2',
+ name: 'Snooze Conversation in Intercom',
+ description: 'Snooze a conversation to reopen at a future time',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'snooze_conversation')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ conversation: data,
+ conversationId: data.id,
+ state: data.state ?? 'snoozed',
+ snoozed_until: data.snoozed_until ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ conversation: {
+ type: 'object',
+ description: 'The snoozed conversation object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the conversation' },
+ type: { type: 'string', description: 'Object type (conversation)' },
+ state: { type: 'string', description: 'State of the conversation (snoozed)' },
+ open: { type: 'boolean', description: 'Whether the conversation is open' },
+ snoozed_until: {
+ type: 'number',
+ description: 'Unix timestamp when conversation will reopen',
+ optional: true,
+ },
+ created_at: { type: 'number', description: 'Unix timestamp when conversation was created' },
+ updated_at: {
+ type: 'number',
+ description: 'Unix timestamp when conversation was last updated',
+ },
+ },
+ },
+ conversationId: { type: 'string', description: 'ID of the snoozed conversation' },
+ state: { type: 'string', description: 'State of the conversation (snoozed)' },
+ snoozed_until: {
+ type: 'number',
+ description: 'Unix timestamp when conversation will reopen',
+ optional: true,
+ },
+ },
+}
diff --git a/apps/sim/tools/intercom/tag_contact.ts b/apps/sim/tools/intercom/tag_contact.ts
new file mode 100644
index 000000000..c2a2628a2
--- /dev/null
+++ b/apps/sim/tools/intercom/tag_contact.ts
@@ -0,0 +1,89 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomTagContactParams {
+ accessToken: string
+ contactId: string
+ tagId: string
+}
+
+export interface IntercomTagContactV2Response {
+ success: boolean
+ output: {
+ id: string
+ name: string
+ type: string
+ }
+}
+
+const tagContactBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ contactId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the contact to tag',
+ },
+ tagId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the tag to apply',
+ },
+ },
+
+ request: {
+ url: (params: IntercomTagContactParams) =>
+ buildIntercomUrl(`/contacts/${params.contactId}/tags`),
+ method: 'POST',
+ headers: (params: IntercomTagContactParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomTagContactParams) => ({
+ id: params.tagId,
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomTagContactV2Tool: ToolConfig<
+ IntercomTagContactParams,
+ IntercomTagContactV2Response
+> = {
+ ...tagContactBase,
+ id: 'intercom_tag_contact_v2',
+ name: 'Tag Contact in Intercom',
+ description: 'Add a tag to a specific contact',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'tag_contact')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ id: data.id,
+ name: data.name,
+ type: data.type ?? 'tag',
+ },
+ }
+ },
+
+ outputs: {
+ id: { type: 'string', description: 'Unique identifier for the tag' },
+ name: { type: 'string', description: 'Name of the tag' },
+ type: { type: 'string', description: 'Object type (tag)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/tag_conversation.ts b/apps/sim/tools/intercom/tag_conversation.ts
new file mode 100644
index 000000000..18485f74f
--- /dev/null
+++ b/apps/sim/tools/intercom/tag_conversation.ts
@@ -0,0 +1,97 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomTagConversationParams {
+ accessToken: string
+ conversationId: string
+ tagId: string
+ admin_id: string
+}
+
+export interface IntercomTagConversationV2Response {
+ success: boolean
+ output: {
+ id: string
+ name: string
+ type: string
+ }
+}
+
+const tagConversationBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ conversationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the conversation to tag',
+ },
+ tagId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the tag to apply',
+ },
+ admin_id: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin applying the tag',
+ },
+ },
+
+ request: {
+ url: (params: IntercomTagConversationParams) =>
+ buildIntercomUrl(`/conversations/${params.conversationId}/tags`),
+ method: 'POST',
+ headers: (params: IntercomTagConversationParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomTagConversationParams) => ({
+ id: params.tagId,
+ admin_id: params.admin_id,
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomTagConversationV2Tool: ToolConfig<
+ IntercomTagConversationParams,
+ IntercomTagConversationV2Response
+> = {
+ ...tagConversationBase,
+ id: 'intercom_tag_conversation_v2',
+ name: 'Tag Conversation in Intercom',
+ description: 'Add a tag to a specific conversation',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'tag_conversation')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ id: data.id,
+ name: data.name,
+ type: data.type ?? 'tag',
+ },
+ }
+ },
+
+ outputs: {
+ id: { type: 'string', description: 'Unique identifier for the tag' },
+ name: { type: 'string', description: 'Name of the tag' },
+ type: { type: 'string', description: 'Object type (tag)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/types.ts b/apps/sim/tools/intercom/types.ts
index d15940d3f..c2a17b787 100644
--- a/apps/sim/tools/intercom/types.ts
+++ b/apps/sim/tools/intercom/types.ts
@@ -2,14 +2,13 @@ import { createLogger } from '@sim/logger'
const logger = createLogger('Intercom')
-// Base params for Intercom API
export interface IntercomBaseParams {
- accessToken: string // OAuth token or API token (hidden)
+ accessToken: string
}
export interface IntercomPaginationParams {
per_page?: number
- starting_after?: string // Cursor for pagination
+ starting_after?: string
}
export interface IntercomPagingInfo {
@@ -33,12 +32,10 @@ export interface IntercomResponse {
}
}
-// Helper function to build Intercom API URLs
export function buildIntercomUrl(path: string): string {
return `https://api.intercom.io${path}`
}
-// Helper function for consistent error handling
export function handleIntercomError(data: any, status: number, operation: string): never {
logger.error(`Intercom API request failed for ${operation}`, { data, status })
diff --git a/apps/sim/tools/intercom/untag_contact.ts b/apps/sim/tools/intercom/untag_contact.ts
new file mode 100644
index 000000000..c5e8cc351
--- /dev/null
+++ b/apps/sim/tools/intercom/untag_contact.ts
@@ -0,0 +1,87 @@
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface IntercomUntagContactParams {
+ accessToken: string
+ contactId: string
+ tagId: string
+}
+
+export interface IntercomUntagContactV2Response {
+ success: boolean
+ output: {
+ id: string
+ name: string
+ type: string
+ }
+}
+
+const untagContactBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ contactId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the contact to untag',
+ },
+ tagId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the tag to remove',
+ },
+ },
+
+ request: {
+ url: (params: IntercomUntagContactParams) =>
+ buildIntercomUrl(`/contacts/${params.contactId}/tags/${params.tagId}`),
+ method: 'DELETE',
+ headers: (params: IntercomUntagContactParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomUntagContactV2Tool: ToolConfig<
+ IntercomUntagContactParams,
+ IntercomUntagContactV2Response
+> = {
+ ...untagContactBase,
+ id: 'intercom_untag_contact_v2',
+ name: 'Untag Contact in Intercom',
+ description: 'Remove a tag from a specific contact',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'untag_contact')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ id: data.id,
+ name: data.name,
+ type: data.type ?? 'tag',
+ },
+ }
+ },
+
+ outputs: {
+ id: { type: 'string', description: 'Unique identifier for the tag that was removed' },
+ name: { type: 'string', description: 'Name of the tag that was removed' },
+ type: { type: 'string', description: 'Object type (tag)' },
+ },
+}
diff --git a/apps/sim/tools/intercom/update_contact.ts b/apps/sim/tools/intercom/update_contact.ts
index 7ac467d9f..9a5ce7e9e 100644
--- a/apps/sim/tools/intercom/update_contact.ts
+++ b/apps/sim/tools/intercom/update_contact.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
import type { ToolConfig } from '@/tools/types'
-import { buildIntercomUrl, handleIntercomError } from './types'
const logger = createLogger('IntercomUpdateContact')
@@ -38,7 +38,7 @@ const intercomUpdateContactBase = {
accessToken: {
type: 'string',
required: true,
- visibility: 'hidden',
+ visibility: 'user-only',
description: 'Intercom API access token',
},
contactId: {
diff --git a/apps/sim/tools/intercom/update_ticket.ts b/apps/sim/tools/intercom/update_ticket.ts
new file mode 100644
index 000000000..6f8bc1f66
--- /dev/null
+++ b/apps/sim/tools/intercom/update_ticket.ts
@@ -0,0 +1,193 @@
+import { createLogger } from '@sim/logger'
+import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
+import type { ToolConfig } from '@/tools/types'
+
+const logger = createLogger('IntercomUpdateTicket')
+
+export interface IntercomUpdateTicketParams {
+ accessToken: string
+ ticketId: string
+ ticket_attributes?: string
+ open?: boolean
+ is_shared?: boolean
+ snoozed_until?: number
+ admin_id?: string
+ assignee_id?: string
+}
+
+export interface IntercomUpdateTicketV2Response {
+ success: boolean
+ output: {
+ ticket: any
+ ticketId: string
+ ticket_state: string
+ }
+}
+
+const updateTicketBase = {
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Intercom API access token',
+ },
+ ticketId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The ID of the ticket to update',
+ },
+ ticket_attributes: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'JSON object with ticket attributes (e.g., {"_default_title_":"New Title","_default_description_":"Updated description"})',
+ },
+ open: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Set to false to close the ticket, true to keep it open',
+ },
+ is_shared: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Whether the ticket is visible to users',
+ },
+ snoozed_until: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Unix timestamp for when the ticket should reopen',
+ },
+ admin_id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'The ID of the admin performing the update (needed for workflows and attribution)',
+ },
+ assignee_id: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The ID of the admin or team to assign the ticket to. Set to "0" to unassign.',
+ },
+ },
+
+ request: {
+ url: (params: IntercomUpdateTicketParams) => buildIntercomUrl(`/tickets/${params.ticketId}`),
+ method: 'PUT',
+ headers: (params: IntercomUpdateTicketParams) => ({
+ Authorization: `Bearer ${params.accessToken}`,
+ 'Content-Type': 'application/json',
+ 'Intercom-Version': '2.14',
+ }),
+ body: (params: IntercomUpdateTicketParams) => {
+ const payload: any = {}
+
+ if (params.ticket_attributes) {
+ try {
+ payload.ticket_attributes = JSON.parse(params.ticket_attributes)
+ } catch (error) {
+ logger.error('Failed to parse ticket_attributes', { error })
+ throw new Error('ticket_attributes must be a valid JSON object')
+ }
+ }
+
+ if (params.open !== undefined) {
+ payload.open = params.open
+ }
+
+ if (params.is_shared !== undefined) {
+ payload.is_shared = params.is_shared
+ }
+
+ if (params.snoozed_until !== undefined) {
+ payload.snoozed_until = params.snoozed_until
+ }
+
+ if (params.admin_id) {
+ payload.admin_id = params.admin_id
+ }
+
+ if (params.assignee_id) {
+ payload.assignee_id = params.assignee_id
+ }
+
+ return payload
+ },
+ },
+} satisfies Pick, 'params' | 'request'>
+
+export const intercomUpdateTicketV2Tool: ToolConfig<
+ IntercomUpdateTicketParams,
+ IntercomUpdateTicketV2Response
+> = {
+ ...updateTicketBase,
+ id: 'intercom_update_ticket_v2',
+ name: 'Update Ticket in Intercom',
+ description: 'Update a ticket in Intercom (change state, assignment, attributes)',
+ version: '2.0.0',
+
+ transformResponse: async (response: Response) => {
+ if (!response.ok) {
+ const data = await response.json()
+ handleIntercomError(data, response.status, 'update_ticket')
+ }
+
+ const data = await response.json()
+
+ return {
+ success: true,
+ output: {
+ ticket: {
+ id: data.id,
+ type: data.type ?? 'ticket',
+ ticket_id: data.ticket_id ?? null,
+ ticket_state: data.ticket_state ?? null,
+ ticket_attributes: data.ticket_attributes ?? null,
+ open: data.open ?? null,
+ is_shared: data.is_shared ?? null,
+ snoozed_until: data.snoozed_until ?? null,
+ admin_assignee_id: data.admin_assignee_id ?? null,
+ team_assignee_id: data.team_assignee_id ?? null,
+ created_at: data.created_at ?? null,
+ updated_at: data.updated_at ?? null,
+ },
+ ticketId: data.id,
+ ticket_state: data.ticket_state ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ ticket: {
+ type: 'object',
+ description: 'The updated ticket object',
+ properties: {
+ id: { type: 'string', description: 'Unique identifier for the ticket' },
+ type: { type: 'string', description: 'Object type (ticket)' },
+ ticket_id: { type: 'string', description: 'Ticket ID shown in Intercom UI' },
+ ticket_state: { type: 'string', description: 'State of the ticket' },
+ ticket_attributes: { type: 'object', description: 'Attributes of the ticket' },
+ open: { type: 'boolean', description: 'Whether the ticket is open' },
+ is_shared: { type: 'boolean', description: 'Whether the ticket is visible to users' },
+ snoozed_until: {
+ type: 'number',
+ description: 'Unix timestamp when ticket will reopen',
+ optional: true,
+ },
+ admin_assignee_id: { type: 'string', description: 'ID of assigned admin', optional: true },
+ team_assignee_id: { type: 'string', description: 'ID of assigned team', optional: true },
+ created_at: { type: 'number', description: 'Unix timestamp when ticket was created' },
+ updated_at: { type: 'number', description: 'Unix timestamp when ticket was last updated' },
+ },
+ },
+ ticketId: { type: 'string', description: 'ID of the updated ticket' },
+ ticket_state: { type: 'string', description: 'Current state of the ticket' },
+ },
+}
diff --git a/apps/sim/tools/kalshi/amend_order.ts b/apps/sim/tools/kalshi/amend_order.ts
index 360104a2e..8c5955eac 100644
--- a/apps/sim/tools/kalshi/amend_order.ts
+++ b/apps/sim/tools/kalshi/amend_order.ts
@@ -158,3 +158,300 @@ export const kalshiAmendOrderTool: ToolConfig = {
+ id: 'kalshi_amend_order_v2',
+ name: 'Amend Order on Kalshi V2',
+ description:
+ 'Modify the price or quantity of an existing order on Kalshi (V2 with full API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ orderId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The order ID to amend',
+ },
+ ticker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Market ticker',
+ },
+ side: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: "Side of the order: 'yes' or 'no'",
+ },
+ action: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: "Action type: 'buy' or 'sell'",
+ },
+ clientOrderId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The original client-specified order ID',
+ },
+ updatedClientOrderId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The new client-specified order ID after amendment',
+ },
+ count: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Updated quantity for the order',
+ },
+ yesPrice: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Updated yes price in cents (1-99)',
+ },
+ noPrice: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Updated no price in cents (1-99)',
+ },
+ yesPriceDollars: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Updated yes price in dollars (e.g., "0.56")',
+ },
+ noPriceDollars: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Updated no price in dollars (e.g., "0.56")',
+ },
+ countFp: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Count in fixed-point for fractional contracts',
+ },
+ },
+
+ request: {
+ url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}/amend`),
+ method: 'POST',
+ headers: (params) => {
+ const path = `/trade-api/v2/portfolio/orders/${params.orderId}/amend`
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'POST', path)
+ },
+ body: (params) => {
+ const body: Record = {
+ ticker: params.ticker,
+ side: params.side.toLowerCase(),
+ action: params.action.toLowerCase(),
+ }
+
+ if (params.clientOrderId) body.client_order_id = params.clientOrderId
+ if (params.updatedClientOrderId) body.updated_client_order_id = params.updatedClientOrderId
+ if (params.count) body.count = Number.parseInt(params.count, 10)
+ if (params.yesPrice) body.yes_price = Number.parseInt(params.yesPrice, 10)
+ if (params.noPrice) body.no_price = Number.parseInt(params.noPrice, 10)
+ if (params.yesPriceDollars) body.yes_price_dollars = params.yesPriceDollars
+ if (params.noPriceDollars) body.no_price_dollars = params.noPriceDollars
+ if (params.countFp) body.count_fp = params.countFp
+
+ return body
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'amend_order_v2')
+ }
+
+ const mapOrder = (order: any): KalshiAmendOrderV2Order => ({
+ order_id: order.order_id ?? null,
+ user_id: order.user_id ?? null,
+ ticker: order.ticker ?? null,
+ event_ticker: order.event_ticker ?? null,
+ status: order.status ?? null,
+ side: order.side ?? null,
+ type: order.type ?? null,
+ yes_price: order.yes_price ?? null,
+ no_price: order.no_price ?? null,
+ action: order.action ?? null,
+ count: order.count ?? null,
+ remaining_count: order.remaining_count ?? null,
+ created_time: order.created_time ?? null,
+ expiration_time: order.expiration_time ?? null,
+ order_group_id: order.order_group_id ?? null,
+ client_order_id: order.client_order_id ?? null,
+ place_count: order.place_count ?? null,
+ decrease_count: order.decrease_count ?? null,
+ queue_position: order.queue_position ?? null,
+ maker_fill_count: order.maker_fill_count ?? null,
+ taker_fill_count: order.taker_fill_count ?? null,
+ maker_fees: order.maker_fees ?? null,
+ taker_fees: order.taker_fees ?? null,
+ last_update_time: order.last_update_time ?? null,
+ take_profit_order_id: order.take_profit_order_id ?? null,
+ stop_loss_order_id: order.stop_loss_order_id ?? null,
+ amend_count: order.amend_count ?? null,
+ amend_taker_fill_count: order.amend_taker_fill_count ?? null,
+ })
+
+ return {
+ success: true,
+ output: {
+ old_order: mapOrder(data.old_order || {}),
+ order: mapOrder(data.order || {}),
+ },
+ }
+ },
+
+ outputs: {
+ old_order: {
+ type: 'object',
+ description: 'The original order object before amendment',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ status: { type: 'string', description: 'Order status' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ count: { type: 'number', description: 'Number of contracts' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ place_count: { type: 'number', description: 'Place count' },
+ decrease_count: { type: 'number', description: 'Decrease count' },
+ queue_position: { type: 'number', description: 'Queue position' },
+ maker_fill_count: { type: 'number', description: 'Maker fill count' },
+ taker_fill_count: { type: 'number', description: 'Taker fill count' },
+ maker_fees: { type: 'number', description: 'Maker fees' },
+ taker_fees: { type: 'number', description: 'Taker fees' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ take_profit_order_id: { type: 'string', description: 'Take profit order ID' },
+ stop_loss_order_id: { type: 'string', description: 'Stop loss order ID' },
+ amend_count: { type: 'number', description: 'Amend count' },
+ amend_taker_fill_count: { type: 'number', description: 'Amend taker fill count' },
+ },
+ },
+ order: {
+ type: 'object',
+ description: 'The amended order object with full API response fields',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ status: { type: 'string', description: 'Order status' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ count: { type: 'number', description: 'Number of contracts' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ place_count: { type: 'number', description: 'Place count' },
+ decrease_count: { type: 'number', description: 'Decrease count' },
+ queue_position: { type: 'number', description: 'Queue position' },
+ maker_fill_count: { type: 'number', description: 'Maker fill count' },
+ taker_fill_count: { type: 'number', description: 'Taker fill count' },
+ maker_fees: { type: 'number', description: 'Maker fees' },
+ taker_fees: { type: 'number', description: 'Taker fees' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ take_profit_order_id: { type: 'string', description: 'Take profit order ID' },
+ stop_loss_order_id: { type: 'string', description: 'Stop loss order ID' },
+ amend_count: { type: 'number', description: 'Amend count' },
+ amend_taker_fill_count: { type: 'number', description: 'Amend taker fill count' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/cancel_order.ts b/apps/sim/tools/kalshi/cancel_order.ts
index d5f8d7843..5d443f108 100644
--- a/apps/sim/tools/kalshi/cancel_order.ts
+++ b/apps/sim/tools/kalshi/cancel_order.ts
@@ -78,3 +78,193 @@ export const kalshiCancelOrderTool: ToolConfig = {
+ id: 'kalshi_cancel_order_v2',
+ name: 'Cancel Order on Kalshi V2',
+ description: 'Cancel an existing order on Kalshi (V2 with full API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ orderId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The order ID to cancel',
+ },
+ },
+
+ request: {
+ url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}`),
+ method: 'DELETE',
+ headers: (params) => {
+ const path = `/trade-api/v2/portfolio/orders/${params.orderId}`
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'DELETE', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'cancel_order_v2')
+ }
+
+ const order = data.order || {}
+
+ return {
+ success: true,
+ output: {
+ order: {
+ order_id: order.order_id ?? null,
+ user_id: order.user_id ?? null,
+ client_order_id: order.client_order_id ?? null,
+ ticker: order.ticker ?? null,
+ side: order.side ?? null,
+ action: order.action ?? null,
+ type: order.type ?? null,
+ status: order.status ?? null,
+ yes_price: order.yes_price ?? null,
+ no_price: order.no_price ?? null,
+ yes_price_dollars: order.yes_price_dollars ?? null,
+ no_price_dollars: order.no_price_dollars ?? null,
+ fill_count: order.fill_count ?? null,
+ fill_count_fp: order.fill_count_fp ?? null,
+ remaining_count: order.remaining_count ?? null,
+ remaining_count_fp: order.remaining_count_fp ?? null,
+ initial_count: order.initial_count ?? null,
+ initial_count_fp: order.initial_count_fp ?? null,
+ taker_fees: order.taker_fees ?? null,
+ maker_fees: order.maker_fees ?? null,
+ taker_fees_dollars: order.taker_fees_dollars ?? null,
+ maker_fees_dollars: order.maker_fees_dollars ?? null,
+ taker_fill_cost: order.taker_fill_cost ?? null,
+ maker_fill_cost: order.maker_fill_cost ?? null,
+ taker_fill_cost_dollars: order.taker_fill_cost_dollars ?? null,
+ maker_fill_cost_dollars: order.maker_fill_cost_dollars ?? null,
+ queue_position: order.queue_position ?? null,
+ expiration_time: order.expiration_time ?? null,
+ created_time: order.created_time ?? null,
+ last_update_time: order.last_update_time ?? null,
+ self_trade_prevention_type: order.self_trade_prevention_type ?? null,
+ order_group_id: order.order_group_id ?? null,
+ cancel_order_on_pause: order.cancel_order_on_pause ?? null,
+ },
+ reduced_by: data.reduced_by ?? 0,
+ reduced_by_fp: data.reduced_by_fp ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ order: {
+ type: 'object',
+ description: 'The canceled order object with full API response fields',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ status: { type: 'string', description: 'Order status (resting/canceled/executed)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ yes_price_dollars: { type: 'string', description: 'Yes price in dollars' },
+ no_price_dollars: { type: 'string', description: 'No price in dollars' },
+ fill_count: { type: 'number', description: 'Filled contract count' },
+ fill_count_fp: { type: 'string', description: 'Filled count (fixed-point)' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ remaining_count_fp: { type: 'string', description: 'Remaining count (fixed-point)' },
+ initial_count: { type: 'number', description: 'Initial contract count' },
+ initial_count_fp: { type: 'string', description: 'Initial count (fixed-point)' },
+ taker_fees: { type: 'number', description: 'Taker fees in cents' },
+ maker_fees: { type: 'number', description: 'Maker fees in cents' },
+ taker_fees_dollars: { type: 'string', description: 'Taker fees in dollars' },
+ maker_fees_dollars: { type: 'string', description: 'Maker fees in dollars' },
+ taker_fill_cost: { type: 'number', description: 'Taker fill cost in cents' },
+ maker_fill_cost: { type: 'number', description: 'Maker fill cost in cents' },
+ taker_fill_cost_dollars: { type: 'string', description: 'Taker fill cost in dollars' },
+ maker_fill_cost_dollars: { type: 'string', description: 'Maker fill cost in dollars' },
+ queue_position: { type: 'number', description: 'Queue position (deprecated)' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ self_trade_prevention_type: { type: 'string', description: 'Self-trade prevention type' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ cancel_order_on_pause: { type: 'boolean', description: 'Cancel on market pause' },
+ },
+ },
+ reduced_by: {
+ type: 'number',
+ description: 'Number of contracts canceled',
+ },
+ reduced_by_fp: {
+ type: 'string',
+ description: 'Number of contracts canceled in fixed-point format',
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/create_order.ts b/apps/sim/tools/kalshi/create_order.ts
index 551807882..e0fa1e9f1 100644
--- a/apps/sim/tools/kalshi/create_order.ts
+++ b/apps/sim/tools/kalshi/create_order.ts
@@ -209,3 +209,344 @@ export const kalshiCreateOrderTool: ToolConfig = {
+ id: 'kalshi_create_order_v2',
+ name: 'Create Order on Kalshi V2',
+ description: 'Create a new order on a Kalshi prediction market (V2 with full API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ ticker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Market ticker (e.g., KXBTC-24DEC31)',
+ },
+ side: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: "Side of the order: 'yes' or 'no'",
+ },
+ action: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: "Action type: 'buy' or 'sell'",
+ },
+ count: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of contracts (provide count or countFp)',
+ },
+ type: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Order type: 'limit' or 'market' (default: limit)",
+ },
+ yesPrice: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Yes price in cents (1-99)',
+ },
+ noPrice: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'No price in cents (1-99)',
+ },
+ yesPriceDollars: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Yes price in dollars (e.g., "0.56")',
+ },
+ noPriceDollars: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'No price in dollars (e.g., "0.56")',
+ },
+ clientOrderId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Custom order identifier',
+ },
+ expirationTs: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Unix timestamp for order expiration',
+ },
+ timeInForce: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Time in force: 'fill_or_kill', 'good_till_canceled', 'immediate_or_cancel'",
+ },
+ buyMaxCost: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum cost in cents (auto-enables fill_or_kill)',
+ },
+ postOnly: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Set to 'true' for maker-only orders",
+ },
+ reduceOnly: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Set to 'true' for position reduction only",
+ },
+ selfTradePreventionType: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Self-trade prevention: 'taker_at_cross' or 'maker'",
+ },
+ orderGroupId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Associated order group ID',
+ },
+ countFp: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Count in fixed-point for fractional contracts',
+ },
+ cancelOrderOnPause: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: "Set to 'true' to cancel order on market pause",
+ },
+ subaccount: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Subaccount to use for the order',
+ },
+ },
+
+ request: {
+ url: () => buildKalshiUrl('/portfolio/orders'),
+ method: 'POST',
+ headers: (params) => {
+ const path = '/trade-api/v2/portfolio/orders'
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'POST', path)
+ },
+ body: (params) => {
+ const body: Record = {
+ ticker: params.ticker,
+ side: params.side.toLowerCase(),
+ action: params.action.toLowerCase(),
+ }
+
+ // count or count_fp must be provided (but not both required)
+ if (params.count) body.count = Number.parseInt(params.count, 10)
+ if (params.countFp) body.count_fp = params.countFp
+ if (params.type) body.type = params.type.toLowerCase()
+ if (params.yesPrice) body.yes_price = Number.parseInt(params.yesPrice, 10)
+ if (params.noPrice) body.no_price = Number.parseInt(params.noPrice, 10)
+ if (params.yesPriceDollars) body.yes_price_dollars = params.yesPriceDollars
+ if (params.noPriceDollars) body.no_price_dollars = params.noPriceDollars
+ if (params.clientOrderId) body.client_order_id = params.clientOrderId
+ if (params.expirationTs) body.expiration_ts = Number.parseInt(params.expirationTs, 10)
+ if (params.timeInForce) body.time_in_force = params.timeInForce
+ if (params.buyMaxCost) body.buy_max_cost = Number.parseInt(params.buyMaxCost, 10)
+ if (params.postOnly) body.post_only = params.postOnly === 'true'
+ if (params.reduceOnly) body.reduce_only = params.reduceOnly === 'true'
+ if (params.selfTradePreventionType)
+ body.self_trade_prevention_type = params.selfTradePreventionType
+ if (params.orderGroupId) body.order_group_id = params.orderGroupId
+ if (params.cancelOrderOnPause)
+ body.cancel_order_on_pause = params.cancelOrderOnPause === 'true'
+ if (params.subaccount) body.subaccount = params.subaccount
+
+ return body
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'create_order_v2')
+ }
+
+ const order = data.order || {}
+
+ return {
+ success: true,
+ output: {
+ order: {
+ order_id: order.order_id ?? null,
+ user_id: order.user_id ?? null,
+ client_order_id: order.client_order_id ?? null,
+ ticker: order.ticker ?? null,
+ side: order.side ?? null,
+ action: order.action ?? null,
+ type: order.type ?? null,
+ status: order.status ?? null,
+ yes_price: order.yes_price ?? null,
+ no_price: order.no_price ?? null,
+ yes_price_dollars: order.yes_price_dollars ?? null,
+ no_price_dollars: order.no_price_dollars ?? null,
+ fill_count: order.fill_count ?? null,
+ fill_count_fp: order.fill_count_fp ?? null,
+ remaining_count: order.remaining_count ?? null,
+ remaining_count_fp: order.remaining_count_fp ?? null,
+ initial_count: order.initial_count ?? null,
+ initial_count_fp: order.initial_count_fp ?? null,
+ taker_fees: order.taker_fees ?? null,
+ maker_fees: order.maker_fees ?? null,
+ taker_fees_dollars: order.taker_fees_dollars ?? null,
+ maker_fees_dollars: order.maker_fees_dollars ?? null,
+ taker_fill_cost: order.taker_fill_cost ?? null,
+ maker_fill_cost: order.maker_fill_cost ?? null,
+ taker_fill_cost_dollars: order.taker_fill_cost_dollars ?? null,
+ maker_fill_cost_dollars: order.maker_fill_cost_dollars ?? null,
+ queue_position: order.queue_position ?? null,
+ expiration_time: order.expiration_time ?? null,
+ created_time: order.created_time ?? null,
+ last_update_time: order.last_update_time ?? null,
+ self_trade_prevention_type: order.self_trade_prevention_type ?? null,
+ order_group_id: order.order_group_id ?? null,
+ cancel_order_on_pause: order.cancel_order_on_pause ?? null,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ order: {
+ type: 'object',
+ description: 'The created order object with full API response fields',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ status: { type: 'string', description: 'Order status (resting/canceled/executed)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ yes_price_dollars: { type: 'string', description: 'Yes price in dollars' },
+ no_price_dollars: { type: 'string', description: 'No price in dollars' },
+ fill_count: { type: 'number', description: 'Filled contract count' },
+ fill_count_fp: { type: 'string', description: 'Filled count (fixed-point)' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ remaining_count_fp: { type: 'string', description: 'Remaining count (fixed-point)' },
+ initial_count: { type: 'number', description: 'Initial contract count' },
+ initial_count_fp: { type: 'string', description: 'Initial count (fixed-point)' },
+ taker_fees: { type: 'number', description: 'Taker fees in cents' },
+ maker_fees: { type: 'number', description: 'Maker fees in cents' },
+ taker_fees_dollars: { type: 'string', description: 'Taker fees in dollars' },
+ maker_fees_dollars: { type: 'string', description: 'Maker fees in dollars' },
+ taker_fill_cost: { type: 'number', description: 'Taker fill cost in cents' },
+ maker_fill_cost: { type: 'number', description: 'Maker fill cost in cents' },
+ taker_fill_cost_dollars: { type: 'string', description: 'Taker fill cost in dollars' },
+ maker_fill_cost_dollars: { type: 'string', description: 'Maker fill cost in dollars' },
+ queue_position: { type: 'number', description: 'Queue position (deprecated)' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ self_trade_prevention_type: { type: 'string', description: 'Self-trade prevention type' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ cancel_order_on_pause: { type: 'boolean', description: 'Cancel on market pause' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_balance.ts b/apps/sim/tools/kalshi/get_balance.ts
index 1ebcd9437..716d9b6fe 100644
--- a/apps/sim/tools/kalshi/get_balance.ts
+++ b/apps/sim/tools/kalshi/get_balance.ts
@@ -66,3 +66,78 @@ export const kalshiGetBalanceTool: ToolConfig = {
+ id: 'kalshi_get_balance_v2',
+ name: 'Get Balance from Kalshi V2',
+ description:
+ 'Retrieve your account balance and portfolio value from Kalshi (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ },
+
+ request: {
+ url: () => buildKalshiUrl('/portfolio/balance'),
+ method: 'GET',
+ headers: (params) => {
+ const path = '/trade-api/v2/portfolio/balance'
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_balance_v2')
+ }
+
+ return {
+ success: true,
+ output: {
+ balance: data.balance ?? 0,
+ portfolio_value: data.portfolio_value ?? 0,
+ updated_ts: data.updated_ts ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ balance: { type: 'number', description: 'Account balance in cents' },
+ portfolio_value: { type: 'number', description: 'Portfolio value in cents' },
+ updated_ts: { type: 'number', description: 'Unix timestamp of last update (milliseconds)' },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_candlesticks.ts b/apps/sim/tools/kalshi/get_candlesticks.ts
index 6a140d3cd..34428d2a2 100644
--- a/apps/sim/tools/kalshi/get_candlesticks.ts
+++ b/apps/sim/tools/kalshi/get_candlesticks.ts
@@ -102,3 +102,246 @@ export const kalshiGetCandlesticksTool: ToolConfig<
},
},
}
+
+/**
+ * BidAskDistribution - OHLC data for yes_bid and yes_ask
+ */
+export interface BidAskDistribution {
+ open: number | null
+ open_dollars: string | null
+ low: number | null
+ low_dollars: string | null
+ high: number | null
+ high_dollars: string | null
+ close: number | null
+ close_dollars: string | null
+}
+
+/**
+ * PriceDistribution - Extended OHLC data for price field
+ */
+export interface PriceDistribution {
+ open: number | null
+ open_dollars: string | null
+ low: number | null
+ low_dollars: string | null
+ high: number | null
+ high_dollars: string | null
+ close: number | null
+ close_dollars: string | null
+ mean: number | null
+ mean_dollars: string | null
+ previous: number | null
+ previous_dollars: string | null
+ min: number | null
+ min_dollars: string | null
+ max: number | null
+ max_dollars: string | null
+}
+
+/**
+ * V2 Get Candlesticks Tool - Returns exact Kalshi API response structure
+ */
+export interface KalshiGetCandlesticksV2Response {
+ success: boolean
+ output: {
+ ticker: string
+ candlesticks: Array<{
+ end_period_ts: number | null
+ yes_bid: BidAskDistribution
+ yes_ask: BidAskDistribution
+ price: PriceDistribution
+ volume: number | null
+ volume_fp: string | null
+ open_interest: number | null
+ open_interest_fp: string | null
+ }>
+ }
+}
+
+export const kalshiGetCandlesticksV2Tool: ToolConfig<
+ KalshiGetCandlesticksParams,
+ KalshiGetCandlesticksV2Response
+> = {
+ id: 'kalshi_get_candlesticks_v2',
+ name: 'Get Market Candlesticks from Kalshi V2',
+ description: 'Retrieve OHLC candlestick data for a specific market (V2 - full API response)',
+ version: '2.0.0',
+
+ params: {
+ seriesTicker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Series ticker',
+ },
+ ticker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Market ticker (e.g., KXBTC-24DEC31)',
+ },
+ startTs: {
+ type: 'number',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Start timestamp (Unix seconds)',
+ },
+ endTs: {
+ type: 'number',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'End timestamp (Unix seconds)',
+ },
+ periodInterval: {
+ type: 'number',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Period interval: 1 (1min), 60 (1hour), or 1440 (1day)',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ queryParams.append('start_ts', params.startTs.toString())
+ queryParams.append('end_ts', params.endTs.toString())
+ queryParams.append('period_interval', params.periodInterval.toString())
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl(
+ `/series/${params.seriesTicker}/markets/${params.ticker}/candlesticks`
+ )
+ return `${url}?${query}`
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_candlesticks_v2')
+ }
+
+ const mapBidAsk = (obj: Record | null): BidAskDistribution => ({
+ open: (obj?.open as number) ?? null,
+ open_dollars: (obj?.open_dollars as string) ?? null,
+ low: (obj?.low as number) ?? null,
+ low_dollars: (obj?.low_dollars as string) ?? null,
+ high: (obj?.high as number) ?? null,
+ high_dollars: (obj?.high_dollars as string) ?? null,
+ close: (obj?.close as number) ?? null,
+ close_dollars: (obj?.close_dollars as string) ?? null,
+ })
+
+ const mapPrice = (obj: Record | null): PriceDistribution => ({
+ open: (obj?.open as number) ?? null,
+ open_dollars: (obj?.open_dollars as string) ?? null,
+ low: (obj?.low as number) ?? null,
+ low_dollars: (obj?.low_dollars as string) ?? null,
+ high: (obj?.high as number) ?? null,
+ high_dollars: (obj?.high_dollars as string) ?? null,
+ close: (obj?.close as number) ?? null,
+ close_dollars: (obj?.close_dollars as string) ?? null,
+ mean: (obj?.mean as number) ?? null,
+ mean_dollars: (obj?.mean_dollars as string) ?? null,
+ previous: (obj?.previous as number) ?? null,
+ previous_dollars: (obj?.previous_dollars as string) ?? null,
+ min: (obj?.min as number) ?? null,
+ min_dollars: (obj?.min_dollars as string) ?? null,
+ max: (obj?.max as number) ?? null,
+ max_dollars: (obj?.max_dollars as string) ?? null,
+ })
+
+ const candlesticks = (data.candlesticks || []).map((c: Record) => ({
+ end_period_ts: (c.end_period_ts as number) ?? null,
+ yes_bid: mapBidAsk(c.yes_bid as Record | null),
+ yes_ask: mapBidAsk(c.yes_ask as Record | null),
+ price: mapPrice(c.price as Record | null),
+ volume: (c.volume as number) ?? null,
+ volume_fp: (c.volume_fp as string) ?? null,
+ open_interest: (c.open_interest as number) ?? null,
+ open_interest_fp: (c.open_interest_fp as string) ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ ticker: data.ticker ?? null,
+ candlesticks,
+ },
+ }
+ },
+
+ outputs: {
+ ticker: {
+ type: 'string',
+ description: 'Market ticker',
+ },
+ candlesticks: {
+ type: 'array',
+ description: 'Array of OHLC candlestick data with nested bid/ask/price objects',
+ properties: {
+ end_period_ts: { type: 'number', description: 'End period timestamp (Unix)' },
+ yes_bid: {
+ type: 'object',
+ description: 'Yes bid OHLC data',
+ properties: {
+ open: { type: 'number', description: 'Open price (cents)' },
+ open_dollars: { type: 'string', description: 'Open price (dollars)' },
+ low: { type: 'number', description: 'Low price (cents)' },
+ low_dollars: { type: 'string', description: 'Low price (dollars)' },
+ high: { type: 'number', description: 'High price (cents)' },
+ high_dollars: { type: 'string', description: 'High price (dollars)' },
+ close: { type: 'number', description: 'Close price (cents)' },
+ close_dollars: { type: 'string', description: 'Close price (dollars)' },
+ },
+ },
+ yes_ask: {
+ type: 'object',
+ description: 'Yes ask OHLC data',
+ properties: {
+ open: { type: 'number', description: 'Open price (cents)' },
+ open_dollars: { type: 'string', description: 'Open price (dollars)' },
+ low: { type: 'number', description: 'Low price (cents)' },
+ low_dollars: { type: 'string', description: 'Low price (dollars)' },
+ high: { type: 'number', description: 'High price (cents)' },
+ high_dollars: { type: 'string', description: 'High price (dollars)' },
+ close: { type: 'number', description: 'Close price (cents)' },
+ close_dollars: { type: 'string', description: 'Close price (dollars)' },
+ },
+ },
+ price: {
+ type: 'object',
+ description: 'Trade price OHLC data with additional statistics',
+ properties: {
+ open: { type: 'number', description: 'Open price (cents)' },
+ open_dollars: { type: 'string', description: 'Open price (dollars)' },
+ low: { type: 'number', description: 'Low price (cents)' },
+ low_dollars: { type: 'string', description: 'Low price (dollars)' },
+ high: { type: 'number', description: 'High price (cents)' },
+ high_dollars: { type: 'string', description: 'High price (dollars)' },
+ close: { type: 'number', description: 'Close price (cents)' },
+ close_dollars: { type: 'string', description: 'Close price (dollars)' },
+ mean: { type: 'number', description: 'Mean price (cents)' },
+ mean_dollars: { type: 'string', description: 'Mean price (dollars)' },
+ previous: { type: 'number', description: 'Previous price (cents)' },
+ previous_dollars: { type: 'string', description: 'Previous price (dollars)' },
+ min: { type: 'number', description: 'Min price (cents)' },
+ min_dollars: { type: 'string', description: 'Min price (dollars)' },
+ max: { type: 'number', description: 'Max price (cents)' },
+ max_dollars: { type: 'string', description: 'Max price (dollars)' },
+ },
+ },
+ volume: { type: 'number', description: 'Volume (contracts)' },
+ volume_fp: { type: 'string', description: 'Volume (fixed-point string)' },
+ open_interest: { type: 'number', description: 'Open interest (contracts)' },
+ open_interest_fp: { type: 'string', description: 'Open interest (fixed-point string)' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_event.ts b/apps/sim/tools/kalshi/get_event.ts
index ec32965a9..a0428454a 100644
--- a/apps/sim/tools/kalshi/get_event.ts
+++ b/apps/sim/tools/kalshi/get_event.ts
@@ -73,3 +73,179 @@ export const kalshiGetEventTool: ToolConfig | null
+ markets: Array<{
+ ticker: string
+ event_ticker: string
+ market_type: string
+ title: string
+ subtitle: string | null
+ yes_sub_title: string | null
+ no_sub_title: string | null
+ open_time: string
+ close_time: string
+ expiration_time: string
+ status: string
+ yes_bid: number
+ yes_ask: number
+ no_bid: number
+ no_ask: number
+ last_price: number
+ previous_yes_bid: number | null
+ previous_yes_ask: number | null
+ previous_price: number | null
+ volume: number
+ volume_24h: number
+ liquidity: number | null
+ open_interest: number | null
+ result: string | null
+ cap_strike: number | null
+ floor_strike: number | null
+ }> | null
+ }
+ }
+}
+
+export const kalshiGetEventV2Tool: ToolConfig = {
+ id: 'kalshi_get_event_v2',
+ name: 'Get Event from Kalshi V2',
+ description: 'Retrieve details of a specific event by ticker (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {
+ eventTicker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The event ticker',
+ },
+ withNestedMarkets: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Include nested markets in response (true/false)',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.withNestedMarkets)
+ queryParams.append('with_nested_markets', params.withNestedMarkets)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl(`/events/${params.eventTicker}`)
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_event_v2')
+ }
+
+ const event = data.event || {}
+ const markets =
+ event.markets?.map((m: Record) => ({
+ ticker: m.ticker ?? null,
+ event_ticker: m.event_ticker ?? null,
+ market_type: m.market_type ?? null,
+ title: m.title ?? null,
+ subtitle: m.subtitle ?? null,
+ yes_sub_title: m.yes_sub_title ?? null,
+ no_sub_title: m.no_sub_title ?? null,
+ open_time: m.open_time ?? null,
+ close_time: m.close_time ?? null,
+ expiration_time: m.expiration_time ?? null,
+ status: m.status ?? null,
+ yes_bid: m.yes_bid ?? 0,
+ yes_ask: m.yes_ask ?? 0,
+ no_bid: m.no_bid ?? 0,
+ no_ask: m.no_ask ?? 0,
+ last_price: m.last_price ?? 0,
+ previous_yes_bid: m.previous_yes_bid ?? null,
+ previous_yes_ask: m.previous_yes_ask ?? null,
+ previous_price: m.previous_price ?? null,
+ volume: m.volume ?? 0,
+ volume_24h: m.volume_24h ?? 0,
+ liquidity: m.liquidity ?? null,
+ open_interest: m.open_interest ?? null,
+ result: m.result ?? null,
+ cap_strike: m.cap_strike ?? null,
+ floor_strike: m.floor_strike ?? null,
+ })) ?? null
+
+ return {
+ success: true,
+ output: {
+ event: {
+ event_ticker: event.event_ticker ?? null,
+ series_ticker: event.series_ticker ?? null,
+ title: event.title ?? null,
+ sub_title: event.sub_title ?? null,
+ mutually_exclusive: event.mutually_exclusive ?? false,
+ category: event.category ?? null,
+ collateral_return_type: event.collateral_return_type ?? null,
+ strike_date: event.strike_date ?? null,
+ strike_period: event.strike_period ?? null,
+ available_on_brokers: event.available_on_brokers ?? null,
+ product_metadata: event.product_metadata ?? null,
+ markets,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ event: {
+ type: 'object',
+ description: 'Event object with full details matching Kalshi API response',
+ properties: {
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ series_ticker: { type: 'string', description: 'Series ticker' },
+ title: { type: 'string', description: 'Event title' },
+ sub_title: { type: 'string', description: 'Event subtitle' },
+ mutually_exclusive: { type: 'boolean', description: 'Mutually exclusive markets' },
+ category: { type: 'string', description: 'Event category' },
+ collateral_return_type: { type: 'string', description: 'Collateral return type' },
+ strike_date: { type: 'string', description: 'Strike date' },
+ strike_period: { type: 'string', description: 'Strike period' },
+ available_on_brokers: { type: 'boolean', description: 'Available on brokers' },
+ product_metadata: { type: 'object', description: 'Product metadata' },
+ markets: { type: 'array', description: 'Nested markets (if requested)' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_events.ts b/apps/sim/tools/kalshi/get_events.ts
index 4ed5c29e4..0b6c4fcc2 100644
--- a/apps/sim/tools/kalshi/get_events.ts
+++ b/apps/sim/tools/kalshi/get_events.ts
@@ -106,3 +106,197 @@ export const kalshiGetEventsTool: ToolConfig | null
+ markets: Array> | null
+ }>
+ milestones: Array<{
+ event_ticker: string
+ milestone_type: string
+ milestone_date: string
+ milestone_title: string | null
+ }> | null
+ cursor: string | null
+ }
+}
+
+export const kalshiGetEventsV2Tool: ToolConfig =
+ {
+ id: 'kalshi_get_events_v2',
+ name: 'Get Events from Kalshi V2',
+ description:
+ 'Retrieve a list of events from Kalshi with optional filtering (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {
+ status: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by status (open, closed, settled)',
+ },
+ seriesTicker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by series ticker',
+ },
+ withNestedMarkets: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Include nested markets in response (true/false)',
+ },
+ withMilestones: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Include milestones in response (true/false)',
+ },
+ minCloseTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum close timestamp (Unix seconds)',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-200, default: 200)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.status) queryParams.append('status', params.status)
+ if (params.seriesTicker) queryParams.append('series_ticker', params.seriesTicker)
+ if (params.withNestedMarkets)
+ queryParams.append('with_nested_markets', params.withNestedMarkets)
+ if (params.withMilestones) queryParams.append('with_milestones', params.withMilestones)
+ if (params.minCloseTs !== undefined)
+ queryParams.append('min_close_ts', params.minCloseTs.toString())
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/events')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_events_v2')
+ }
+
+ const events = (data.events || []).map((e: Record) => ({
+ event_ticker: e.event_ticker ?? null,
+ series_ticker: e.series_ticker ?? null,
+ title: e.title ?? null,
+ sub_title: e.sub_title ?? null,
+ mutually_exclusive: e.mutually_exclusive ?? false,
+ category: e.category ?? null,
+ collateral_return_type: e.collateral_return_type ?? null,
+ strike_date: e.strike_date ?? null,
+ strike_period: e.strike_period ?? null,
+ available_on_brokers: e.available_on_brokers ?? null,
+ product_metadata: e.product_metadata ?? null,
+ markets: e.markets ?? null,
+ }))
+
+ const milestones = data.milestones
+ ? (data.milestones as Array>).map((m) => ({
+ event_ticker: (m.event_ticker as string) ?? '',
+ milestone_type: (m.milestone_type as string) ?? '',
+ milestone_date: (m.milestone_date as string) ?? '',
+ milestone_title: (m.milestone_title as string | null) ?? null,
+ }))
+ : null
+
+ return {
+ success: true,
+ output: {
+ events,
+ milestones,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ events: {
+ type: 'array',
+ description: 'Array of event objects',
+ properties: {
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ series_ticker: { type: 'string', description: 'Series ticker' },
+ title: { type: 'string', description: 'Event title' },
+ sub_title: { type: 'string', description: 'Event subtitle' },
+ mutually_exclusive: { type: 'boolean', description: 'Mutually exclusive markets' },
+ category: { type: 'string', description: 'Event category' },
+ collateral_return_type: { type: 'string', description: 'Collateral return type' },
+ strike_date: { type: 'string', description: 'Strike date' },
+ strike_period: { type: 'string', description: 'Strike period' },
+ available_on_brokers: { type: 'boolean', description: 'Available on brokers' },
+ product_metadata: { type: 'object', description: 'Product metadata' },
+ markets: { type: 'array', description: 'Nested markets (if requested)' },
+ },
+ },
+ milestones: {
+ type: 'array',
+ description: 'Array of milestone objects (if requested)',
+ properties: {
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ milestone_type: { type: 'string', description: 'Milestone type' },
+ milestone_date: { type: 'string', description: 'Milestone date' },
+ milestone_title: { type: 'string', description: 'Milestone title' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+ }
diff --git a/apps/sim/tools/kalshi/get_exchange_status.ts b/apps/sim/tools/kalshi/get_exchange_status.ts
index 545a0ea7c..7348d6b29 100644
--- a/apps/sim/tools/kalshi/get_exchange_status.ts
+++ b/apps/sim/tools/kalshi/get_exchange_status.ts
@@ -59,3 +59,74 @@ export const kalshiGetExchangeStatusTool: ToolConfig<
},
},
}
+
+/**
+ * V2 Params for Get Exchange Status
+ */
+export type KalshiGetExchangeStatusV2Params = Record
+
+/**
+ * V2 Response matching Kalshi API exactly
+ */
+export interface KalshiGetExchangeStatusV2Response {
+ success: boolean
+ output: {
+ exchange_active: boolean
+ trading_active: boolean
+ exchange_estimated_resume_time: string | null
+ }
+}
+
+export const kalshiGetExchangeStatusV2Tool: ToolConfig<
+ KalshiGetExchangeStatusV2Params,
+ KalshiGetExchangeStatusV2Response
+> = {
+ id: 'kalshi_get_exchange_status_v2',
+ name: 'Get Exchange Status from Kalshi V2',
+ description: 'Retrieve the current status of the Kalshi exchange (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {},
+
+ request: {
+ url: () => {
+ return buildKalshiUrl('/exchange/status')
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_exchange_status_v2')
+ }
+
+ return {
+ success: true,
+ output: {
+ exchange_active: data.exchange_active ?? false,
+ trading_active: data.trading_active ?? false,
+ exchange_estimated_resume_time: data.exchange_estimated_resume_time ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ exchange_active: {
+ type: 'boolean',
+ description: 'Whether the exchange is active',
+ },
+ trading_active: {
+ type: 'boolean',
+ description: 'Whether trading is active',
+ },
+ exchange_estimated_resume_time: {
+ type: 'string',
+ description: 'Estimated time when exchange will resume (if inactive)',
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_fills.ts b/apps/sim/tools/kalshi/get_fills.ts
index 1ca8b8898..3ae429aab 100644
--- a/apps/sim/tools/kalshi/get_fills.ts
+++ b/apps/sim/tools/kalshi/get_fills.ts
@@ -131,3 +131,198 @@ export const kalshiGetFillsTool: ToolConfig
+ cursor: string | null
+ }
+}
+
+export const kalshiGetFillsV2Tool: ToolConfig = {
+ id: 'kalshi_get_fills_v2',
+ name: 'Get Fills from Kalshi V2',
+ description: "Retrieve your portfolio's fills/trades from Kalshi (V2 - exact API response)",
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ ticker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by market ticker',
+ },
+ orderId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by order ID',
+ },
+ minTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum timestamp (Unix milliseconds)',
+ },
+ maxTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum timestamp (Unix milliseconds)',
+ },
+ subaccount: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Subaccount to get fills for',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-200, default: 100)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.ticker) queryParams.append('ticker', params.ticker)
+ if (params.orderId) queryParams.append('order_id', params.orderId)
+ if (params.minTs !== undefined) queryParams.append('min_ts', params.minTs.toString())
+ if (params.maxTs !== undefined) queryParams.append('max_ts', params.maxTs.toString())
+ if (params.subaccount) queryParams.append('subaccount', params.subaccount)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/portfolio/fills')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: (params) => {
+ const path = '/trade-api/v2/portfolio/fills'
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_fills_v2')
+ }
+
+ const fills = (data.fills || []).map((f: Record) => ({
+ fill_id: f.fill_id ?? null,
+ trade_id: f.trade_id ?? null,
+ order_id: f.order_id ?? null,
+ client_order_id: f.client_order_id ?? null,
+ ticker: f.ticker ?? null,
+ market_ticker: f.market_ticker ?? null,
+ side: f.side ?? null,
+ action: f.action ?? null,
+ count: f.count ?? 0,
+ count_fp: f.count_fp ?? null,
+ price: f.price ?? null,
+ yes_price: f.yes_price ?? 0,
+ no_price: f.no_price ?? 0,
+ yes_price_fixed: f.yes_price_fixed ?? null,
+ no_price_fixed: f.no_price_fixed ?? null,
+ is_taker: f.is_taker ?? false,
+ created_time: f.created_time ?? null,
+ ts: f.ts ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ fills,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ fills: {
+ type: 'array',
+ description: 'Array of fill/trade objects with all API fields',
+ properties: {
+ fill_id: { type: 'string', description: 'Fill ID' },
+ trade_id: { type: 'string', description: 'Trade ID (same as fill_id)' },
+ order_id: { type: 'string', description: 'Order ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ market_ticker: { type: 'string', description: 'Market ticker (legacy)' },
+ side: { type: 'string', description: 'Side (yes/no)' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ count: { type: 'number', description: 'Number of contracts' },
+ count_fp: { type: 'string', description: 'Count (fixed-point)' },
+ price: { type: 'number', description: 'Price (deprecated)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ yes_price_fixed: { type: 'string', description: 'Yes price in dollars' },
+ no_price_fixed: { type: 'string', description: 'No price in dollars' },
+ is_taker: { type: 'boolean', description: 'Whether fill was taker' },
+ created_time: { type: 'string', description: 'Fill creation time' },
+ ts: { type: 'number', description: 'Unix timestamp (milliseconds)' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_market.ts b/apps/sim/tools/kalshi/get_market.ts
index b54971502..12d735e50 100644
--- a/apps/sim/tools/kalshi/get_market.ts
+++ b/apps/sim/tools/kalshi/get_market.ts
@@ -58,3 +58,219 @@ export const kalshiGetMarketTool: ToolConfig = {
+ id: 'kalshi_get_market_v2',
+ name: 'Get Market from Kalshi V2',
+ description:
+ 'Retrieve details of a specific prediction market by ticker (V2 - full API response)',
+ version: '2.0.0',
+
+ params: {
+ ticker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The market ticker (e.g., "KXBTC-24DEC31")',
+ },
+ },
+
+ request: {
+ url: (params) => buildKalshiUrl(`/markets/${params.ticker}`),
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_market_v2')
+ }
+
+ const m = data.market || {}
+
+ return {
+ success: true,
+ output: {
+ market: {
+ ticker: m.ticker ?? null,
+ event_ticker: m.event_ticker ?? null,
+ market_type: m.market_type ?? null,
+ title: m.title ?? null,
+ subtitle: m.subtitle ?? null,
+ yes_sub_title: m.yes_sub_title ?? null,
+ no_sub_title: m.no_sub_title ?? null,
+ open_time: m.open_time ?? null,
+ close_time: m.close_time ?? null,
+ expected_expiration_time: m.expected_expiration_time ?? null,
+ expiration_time: m.expiration_time ?? null,
+ latest_expiration_time: m.latest_expiration_time ?? null,
+ settlement_timer_seconds: m.settlement_timer_seconds ?? null,
+ status: m.status ?? null,
+ response_price_units: m.response_price_units ?? null,
+ notional_value: m.notional_value ?? null,
+ tick_size: m.tick_size ?? null,
+ yes_bid: m.yes_bid ?? null,
+ yes_ask: m.yes_ask ?? null,
+ no_bid: m.no_bid ?? null,
+ no_ask: m.no_ask ?? null,
+ last_price: m.last_price ?? null,
+ previous_yes_bid: m.previous_yes_bid ?? null,
+ previous_yes_ask: m.previous_yes_ask ?? null,
+ previous_price: m.previous_price ?? null,
+ volume: m.volume ?? null,
+ volume_24h: m.volume_24h ?? null,
+ liquidity: m.liquidity ?? null,
+ open_interest: m.open_interest ?? null,
+ result: m.result ?? null,
+ cap_strike: m.cap_strike ?? null,
+ floor_strike: m.floor_strike ?? null,
+ can_close_early: m.can_close_early ?? null,
+ expiration_value: m.expiration_value ?? null,
+ category: m.category ?? null,
+ risk_limit_cents: m.risk_limit_cents ?? null,
+ strike_type: m.strike_type ?? null,
+ rules_primary: m.rules_primary ?? null,
+ rules_secondary: m.rules_secondary ?? null,
+ settlement_source_url: m.settlement_source_url ?? null,
+ custom_strike: m.custom_strike ?? null,
+ underlying: m.underlying ?? null,
+ settlement_value: m.settlement_value ?? null,
+ cfd_contract_size: m.cfd_contract_size ?? null,
+ yes_fee_fp: m.yes_fee_fp ?? null,
+ no_fee_fp: m.no_fee_fp ?? null,
+ last_price_fp: m.last_price_fp ?? null,
+ yes_bid_fp: m.yes_bid_fp ?? null,
+ yes_ask_fp: m.yes_ask_fp ?? null,
+ no_bid_fp: m.no_bid_fp ?? null,
+ no_ask_fp: m.no_ask_fp ?? null,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ market: {
+ type: 'object',
+ description: 'Market object with all API fields',
+ properties: {
+ ticker: { type: 'string', description: 'Market ticker' },
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ market_type: { type: 'string', description: 'Market type' },
+ title: { type: 'string', description: 'Market title' },
+ subtitle: { type: 'string', description: 'Market subtitle' },
+ yes_sub_title: { type: 'string', description: 'Yes outcome subtitle' },
+ no_sub_title: { type: 'string', description: 'No outcome subtitle' },
+ open_time: { type: 'string', description: 'Market open time' },
+ close_time: { type: 'string', description: 'Market close time' },
+ expected_expiration_time: { type: 'string', description: 'Expected expiration time' },
+ expiration_time: { type: 'string', description: 'Expiration time' },
+ latest_expiration_time: { type: 'string', description: 'Latest expiration time' },
+ settlement_timer_seconds: { type: 'number', description: 'Settlement timer in seconds' },
+ status: { type: 'string', description: 'Market status' },
+ response_price_units: { type: 'string', description: 'Response price units' },
+ notional_value: { type: 'number', description: 'Notional value' },
+ tick_size: { type: 'number', description: 'Tick size' },
+ yes_bid: { type: 'number', description: 'Current yes bid price' },
+ yes_ask: { type: 'number', description: 'Current yes ask price' },
+ no_bid: { type: 'number', description: 'Current no bid price' },
+ no_ask: { type: 'number', description: 'Current no ask price' },
+ last_price: { type: 'number', description: 'Last trade price' },
+ previous_yes_bid: { type: 'number', description: 'Previous yes bid' },
+ previous_yes_ask: { type: 'number', description: 'Previous yes ask' },
+ previous_price: { type: 'number', description: 'Previous price' },
+ volume: { type: 'number', description: 'Total volume' },
+ volume_24h: { type: 'number', description: '24-hour volume' },
+ liquidity: { type: 'number', description: 'Market liquidity' },
+ open_interest: { type: 'number', description: 'Open interest' },
+ result: { type: 'string', description: 'Market result' },
+ cap_strike: { type: 'number', description: 'Cap strike' },
+ floor_strike: { type: 'number', description: 'Floor strike' },
+ can_close_early: { type: 'boolean', description: 'Can close early' },
+ expiration_value: { type: 'string', description: 'Expiration value' },
+ category: { type: 'string', description: 'Market category' },
+ risk_limit_cents: { type: 'number', description: 'Risk limit in cents' },
+ strike_type: { type: 'string', description: 'Strike type' },
+ rules_primary: { type: 'string', description: 'Primary rules' },
+ rules_secondary: { type: 'string', description: 'Secondary rules' },
+ settlement_source_url: { type: 'string', description: 'Settlement source URL' },
+ custom_strike: { type: 'object', description: 'Custom strike object' },
+ underlying: { type: 'string', description: 'Underlying asset' },
+ settlement_value: { type: 'number', description: 'Settlement value' },
+ cfd_contract_size: { type: 'number', description: 'CFD contract size' },
+ yes_fee_fp: { type: 'number', description: 'Yes fee (fixed-point)' },
+ no_fee_fp: { type: 'number', description: 'No fee (fixed-point)' },
+ last_price_fp: { type: 'number', description: 'Last price (fixed-point)' },
+ yes_bid_fp: { type: 'number', description: 'Yes bid (fixed-point)' },
+ yes_ask_fp: { type: 'number', description: 'Yes ask (fixed-point)' },
+ no_bid_fp: { type: 'number', description: 'No bid (fixed-point)' },
+ no_ask_fp: { type: 'number', description: 'No ask (fixed-point)' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_markets.ts b/apps/sim/tools/kalshi/get_markets.ts
index c8b743c28..5c2ea31eb 100644
--- a/apps/sim/tools/kalshi/get_markets.ts
+++ b/apps/sim/tools/kalshi/get_markets.ts
@@ -105,3 +105,341 @@ export const kalshiGetMarketsTool: ToolConfig
+ cursor: string | null
+ }
+}
+
+export const kalshiGetMarketsV2Tool: ToolConfig<
+ KalshiGetMarketsV2Params,
+ KalshiGetMarketsV2Response
+> = {
+ id: 'kalshi_get_markets_v2',
+ name: 'Get Markets from Kalshi V2',
+ description:
+ 'Retrieve a list of prediction markets from Kalshi with all filtering options (V2 - full API response)',
+ version: '2.0.0',
+
+ params: {
+ status: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by status (unopened, open, closed, settled)',
+ },
+ seriesTicker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by series ticker',
+ },
+ eventTicker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by event ticker',
+ },
+ minCreatedTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum created timestamp (Unix seconds)',
+ },
+ maxCreatedTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum created timestamp (Unix seconds)',
+ },
+ minUpdatedTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum updated timestamp (Unix seconds)',
+ },
+ minCloseTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum close timestamp (Unix seconds)',
+ },
+ maxCloseTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum close timestamp (Unix seconds)',
+ },
+ minSettledTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum settled timestamp (Unix seconds)',
+ },
+ maxSettledTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum settled timestamp (Unix seconds)',
+ },
+ tickers: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Comma-separated list of tickers to filter',
+ },
+ mveFilter: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'MVE filter (display or all)',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-1000, default: 100)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.status) queryParams.append('status', params.status)
+ if (params.seriesTicker) queryParams.append('series_ticker', params.seriesTicker)
+ if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker)
+ if (params.minCreatedTs) queryParams.append('min_created_ts', params.minCreatedTs.toString())
+ if (params.maxCreatedTs) queryParams.append('max_created_ts', params.maxCreatedTs.toString())
+ if (params.minUpdatedTs) queryParams.append('min_updated_ts', params.minUpdatedTs.toString())
+ if (params.minCloseTs) queryParams.append('min_close_ts', params.minCloseTs.toString())
+ if (params.maxCloseTs) queryParams.append('max_close_ts', params.maxCloseTs.toString())
+ if (params.minSettledTs) queryParams.append('min_settled_ts', params.minSettledTs.toString())
+ if (params.maxSettledTs) queryParams.append('max_settled_ts', params.maxSettledTs.toString())
+ if (params.tickers) queryParams.append('tickers', params.tickers)
+ if (params.mveFilter) queryParams.append('mve_filter', params.mveFilter)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/markets')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_markets_v2')
+ }
+
+ const markets = (data.markets || []).map((m: Record) => ({
+ ticker: m.ticker ?? null,
+ event_ticker: m.event_ticker ?? null,
+ market_type: m.market_type ?? null,
+ title: m.title ?? null,
+ subtitle: m.subtitle ?? null,
+ yes_sub_title: m.yes_sub_title ?? null,
+ no_sub_title: m.no_sub_title ?? null,
+ open_time: m.open_time ?? null,
+ close_time: m.close_time ?? null,
+ expected_expiration_time: m.expected_expiration_time ?? null,
+ expiration_time: m.expiration_time ?? null,
+ latest_expiration_time: m.latest_expiration_time ?? null,
+ settlement_timer_seconds: m.settlement_timer_seconds ?? null,
+ status: m.status ?? null,
+ response_price_units: m.response_price_units ?? null,
+ notional_value: m.notional_value ?? null,
+ tick_size: m.tick_size ?? null,
+ yes_bid: m.yes_bid ?? null,
+ yes_ask: m.yes_ask ?? null,
+ no_bid: m.no_bid ?? null,
+ no_ask: m.no_ask ?? null,
+ last_price: m.last_price ?? null,
+ previous_yes_bid: m.previous_yes_bid ?? null,
+ previous_yes_ask: m.previous_yes_ask ?? null,
+ previous_price: m.previous_price ?? null,
+ volume: m.volume ?? null,
+ volume_24h: m.volume_24h ?? null,
+ liquidity: m.liquidity ?? null,
+ open_interest: m.open_interest ?? null,
+ result: m.result ?? null,
+ cap_strike: m.cap_strike ?? null,
+ floor_strike: m.floor_strike ?? null,
+ can_close_early: m.can_close_early ?? null,
+ expiration_value: m.expiration_value ?? null,
+ category: m.category ?? null,
+ risk_limit_cents: m.risk_limit_cents ?? null,
+ strike_type: m.strike_type ?? null,
+ rules_primary: m.rules_primary ?? null,
+ rules_secondary: m.rules_secondary ?? null,
+ settlement_source_url: m.settlement_source_url ?? null,
+ custom_strike: m.custom_strike ?? null,
+ underlying: m.underlying ?? null,
+ settlement_value: m.settlement_value ?? null,
+ cfd_contract_size: m.cfd_contract_size ?? null,
+ yes_fee_fp: m.yes_fee_fp ?? null,
+ no_fee_fp: m.no_fee_fp ?? null,
+ last_price_fp: m.last_price_fp ?? null,
+ yes_bid_fp: m.yes_bid_fp ?? null,
+ yes_ask_fp: m.yes_ask_fp ?? null,
+ no_bid_fp: m.no_bid_fp ?? null,
+ no_ask_fp: m.no_ask_fp ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ markets,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ markets: {
+ type: 'array',
+ description: 'Array of market objects with all API fields',
+ properties: {
+ ticker: { type: 'string', description: 'Market ticker' },
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ market_type: { type: 'string', description: 'Market type' },
+ title: { type: 'string', description: 'Market title' },
+ subtitle: { type: 'string', description: 'Market subtitle' },
+ yes_sub_title: { type: 'string', description: 'Yes outcome subtitle' },
+ no_sub_title: { type: 'string', description: 'No outcome subtitle' },
+ open_time: { type: 'string', description: 'Market open time' },
+ close_time: { type: 'string', description: 'Market close time' },
+ expected_expiration_time: { type: 'string', description: 'Expected expiration time' },
+ expiration_time: { type: 'string', description: 'Expiration time' },
+ latest_expiration_time: { type: 'string', description: 'Latest expiration time' },
+ settlement_timer_seconds: { type: 'number', description: 'Settlement timer in seconds' },
+ status: { type: 'string', description: 'Market status' },
+ response_price_units: { type: 'string', description: 'Response price units' },
+ notional_value: { type: 'number', description: 'Notional value' },
+ tick_size: { type: 'number', description: 'Tick size' },
+ yes_bid: { type: 'number', description: 'Current yes bid price' },
+ yes_ask: { type: 'number', description: 'Current yes ask price' },
+ no_bid: { type: 'number', description: 'Current no bid price' },
+ no_ask: { type: 'number', description: 'Current no ask price' },
+ last_price: { type: 'number', description: 'Last trade price' },
+ previous_yes_bid: { type: 'number', description: 'Previous yes bid' },
+ previous_yes_ask: { type: 'number', description: 'Previous yes ask' },
+ previous_price: { type: 'number', description: 'Previous price' },
+ volume: { type: 'number', description: 'Total volume' },
+ volume_24h: { type: 'number', description: '24-hour volume' },
+ liquidity: { type: 'number', description: 'Market liquidity' },
+ open_interest: { type: 'number', description: 'Open interest' },
+ result: { type: 'string', description: 'Market result' },
+ cap_strike: { type: 'number', description: 'Cap strike' },
+ floor_strike: { type: 'number', description: 'Floor strike' },
+ can_close_early: { type: 'boolean', description: 'Can close early' },
+ expiration_value: { type: 'string', description: 'Expiration value' },
+ category: { type: 'string', description: 'Market category' },
+ risk_limit_cents: { type: 'number', description: 'Risk limit in cents' },
+ strike_type: { type: 'string', description: 'Strike type' },
+ rules_primary: { type: 'string', description: 'Primary rules' },
+ rules_secondary: { type: 'string', description: 'Secondary rules' },
+ settlement_source_url: { type: 'string', description: 'Settlement source URL' },
+ custom_strike: { type: 'object', description: 'Custom strike object' },
+ underlying: { type: 'string', description: 'Underlying asset' },
+ settlement_value: { type: 'number', description: 'Settlement value' },
+ cfd_contract_size: { type: 'number', description: 'CFD contract size' },
+ yes_fee_fp: { type: 'number', description: 'Yes fee (fixed-point)' },
+ no_fee_fp: { type: 'number', description: 'No fee (fixed-point)' },
+ last_price_fp: { type: 'number', description: 'Last price (fixed-point)' },
+ yes_bid_fp: { type: 'number', description: 'Yes bid (fixed-point)' },
+ yes_ask_fp: { type: 'number', description: 'Yes ask (fixed-point)' },
+ no_bid_fp: { type: 'number', description: 'No bid (fixed-point)' },
+ no_ask_fp: { type: 'number', description: 'No ask (fixed-point)' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_order.ts b/apps/sim/tools/kalshi/get_order.ts
index ed6a4c9f4..12411de6e 100644
--- a/apps/sim/tools/kalshi/get_order.ts
+++ b/apps/sim/tools/kalshi/get_order.ts
@@ -71,3 +71,178 @@ export const kalshiGetOrderTool: ToolConfig = {
+ id: 'kalshi_get_order_v2',
+ name: 'Get Order from Kalshi V2',
+ description: 'Retrieve details of a specific order by ID from Kalshi (V2 with full API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ orderId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'The order ID to retrieve',
+ },
+ },
+
+ request: {
+ url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}`),
+ method: 'GET',
+ headers: (params) => {
+ const path = `/trade-api/v2/portfolio/orders/${params.orderId}`
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_order_v2')
+ }
+
+ const order = data.order || {}
+
+ return {
+ success: true,
+ output: {
+ order: {
+ order_id: order.order_id ?? null,
+ user_id: order.user_id ?? null,
+ client_order_id: order.client_order_id ?? null,
+ ticker: order.ticker ?? null,
+ side: order.side ?? null,
+ action: order.action ?? null,
+ type: order.type ?? null,
+ status: order.status ?? null,
+ yes_price: order.yes_price ?? null,
+ no_price: order.no_price ?? null,
+ yes_price_dollars: order.yes_price_dollars ?? null,
+ no_price_dollars: order.no_price_dollars ?? null,
+ fill_count: order.fill_count ?? null,
+ fill_count_fp: order.fill_count_fp ?? null,
+ remaining_count: order.remaining_count ?? null,
+ remaining_count_fp: order.remaining_count_fp ?? null,
+ initial_count: order.initial_count ?? null,
+ initial_count_fp: order.initial_count_fp ?? null,
+ taker_fees: order.taker_fees ?? null,
+ maker_fees: order.maker_fees ?? null,
+ taker_fees_dollars: order.taker_fees_dollars ?? null,
+ maker_fees_dollars: order.maker_fees_dollars ?? null,
+ taker_fill_cost: order.taker_fill_cost ?? null,
+ maker_fill_cost: order.maker_fill_cost ?? null,
+ taker_fill_cost_dollars: order.taker_fill_cost_dollars ?? null,
+ maker_fill_cost_dollars: order.maker_fill_cost_dollars ?? null,
+ queue_position: order.queue_position ?? null,
+ expiration_time: order.expiration_time ?? null,
+ created_time: order.created_time ?? null,
+ last_update_time: order.last_update_time ?? null,
+ self_trade_prevention_type: order.self_trade_prevention_type ?? null,
+ order_group_id: order.order_group_id ?? null,
+ cancel_order_on_pause: order.cancel_order_on_pause ?? null,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ order: {
+ type: 'object',
+ description: 'Order object with full API response fields',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ status: { type: 'string', description: 'Order status (resting/canceled/executed)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ yes_price_dollars: { type: 'string', description: 'Yes price in dollars' },
+ no_price_dollars: { type: 'string', description: 'No price in dollars' },
+ fill_count: { type: 'number', description: 'Filled contract count' },
+ fill_count_fp: { type: 'string', description: 'Filled count (fixed-point)' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ remaining_count_fp: { type: 'string', description: 'Remaining count (fixed-point)' },
+ initial_count: { type: 'number', description: 'Initial contract count' },
+ initial_count_fp: { type: 'string', description: 'Initial count (fixed-point)' },
+ taker_fees: { type: 'number', description: 'Taker fees in cents' },
+ maker_fees: { type: 'number', description: 'Maker fees in cents' },
+ taker_fees_dollars: { type: 'string', description: 'Taker fees in dollars' },
+ maker_fees_dollars: { type: 'string', description: 'Maker fees in dollars' },
+ taker_fill_cost: { type: 'number', description: 'Taker fill cost in cents' },
+ maker_fill_cost: { type: 'number', description: 'Maker fill cost in cents' },
+ taker_fill_cost_dollars: { type: 'string', description: 'Taker fill cost in dollars' },
+ maker_fill_cost_dollars: { type: 'string', description: 'Maker fill cost in dollars' },
+ queue_position: { type: 'number', description: 'Queue position (deprecated)' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ self_trade_prevention_type: { type: 'string', description: 'Self-trade prevention type' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ cancel_order_on_pause: { type: 'boolean', description: 'Cancel on market pause' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_orderbook.ts b/apps/sim/tools/kalshi/get_orderbook.ts
index 4f6ff0303..642c67f2d 100644
--- a/apps/sim/tools/kalshi/get_orderbook.ts
+++ b/apps/sim/tools/kalshi/get_orderbook.ts
@@ -63,3 +63,135 @@ export const kalshiGetOrderbookTool: ToolConfig<
},
},
}
+
+/**
+ * V2 Get Orderbook Tool - Returns exact Kalshi API response structure with depth param
+ * API returns tuple arrays: [price, count] for orderbook, [dollars_string, count] for _dollars variants
+ */
+export interface KalshiGetOrderbookV2Params {
+ ticker: string
+ depth?: number // Number of price levels to return
+}
+
+export interface KalshiGetOrderbookV2Response {
+ success: boolean
+ output: {
+ orderbook: {
+ yes: Array<[number, number]> // [price_in_cents, count]
+ no: Array<[number, number]> // [price_in_cents, count]
+ yes_dollars: Array<[string, number]> // [dollars_string, count]
+ no_dollars: Array<[string, number]> // [dollars_string, count]
+ }
+ orderbook_fp: {
+ yes_dollars: Array<[string, string]> // [dollars_string, fp_count_string]
+ no_dollars: Array<[string, string]> // [dollars_string, fp_count_string]
+ }
+ }
+}
+
+export const kalshiGetOrderbookV2Tool: ToolConfig<
+ KalshiGetOrderbookV2Params,
+ KalshiGetOrderbookV2Response
+> = {
+ id: 'kalshi_get_orderbook_v2',
+ name: 'Get Market Orderbook from Kalshi V2',
+ description:
+ 'Retrieve the orderbook (yes and no bids) for a specific market (V2 - includes depth and fp fields)',
+ version: '2.0.0',
+
+ params: {
+ ticker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Market ticker (e.g., KXBTC-24DEC31)',
+ },
+ depth: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of price levels to return (default: all)',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.depth) queryParams.append('depth', params.depth.toString())
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl(`/markets/${params.ticker}/orderbook`)
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_orderbook_v2')
+ }
+
+ const orderbook = data.orderbook || {}
+ const orderbookFp = data.orderbook_fp || {}
+
+ return {
+ success: true,
+ output: {
+ orderbook: {
+ yes: orderbook.yes ?? [],
+ no: orderbook.no ?? [],
+ yes_dollars: orderbook.yes_dollars ?? [],
+ no_dollars: orderbook.no_dollars ?? [],
+ },
+ orderbook_fp: {
+ yes_dollars: orderbookFp.yes_dollars ?? [],
+ no_dollars: orderbookFp.no_dollars ?? [],
+ },
+ },
+ }
+ },
+
+ outputs: {
+ orderbook: {
+ type: 'object',
+ description: 'Orderbook with yes/no bids (legacy integer counts)',
+ properties: {
+ yes: {
+ type: 'array',
+ description: 'Yes side bids as tuples [price_cents, count]',
+ },
+ no: {
+ type: 'array',
+ description: 'No side bids as tuples [price_cents, count]',
+ },
+ yes_dollars: {
+ type: 'array',
+ description: 'Yes side bids as tuples [dollars_string, count]',
+ },
+ no_dollars: {
+ type: 'array',
+ description: 'No side bids as tuples [dollars_string, count]',
+ },
+ },
+ },
+ orderbook_fp: {
+ type: 'object',
+ description: 'Orderbook with fixed-point counts (preferred)',
+ properties: {
+ yes_dollars: {
+ type: 'array',
+ description: 'Yes side bids as tuples [dollars_string, fp_count_string]',
+ },
+ no_dollars: {
+ type: 'array',
+ description: 'No side bids as tuples [dollars_string, fp_count_string]',
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_orders.ts b/apps/sim/tools/kalshi/get_orders.ts
index 25f0a0243..e18d8f3fc 100644
--- a/apps/sim/tools/kalshi/get_orders.ts
+++ b/apps/sim/tools/kalshi/get_orders.ts
@@ -123,3 +123,250 @@ export const kalshiGetOrdersTool: ToolConfig =
+ {
+ id: 'kalshi_get_orders_v2',
+ name: 'Get Orders from Kalshi V2',
+ description:
+ 'Retrieve your orders from Kalshi with optional filtering (V2 with full API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ ticker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by market ticker',
+ },
+ eventTicker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by event ticker (max 10 comma-separated)',
+ },
+ status: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by status (resting, canceled, executed)',
+ },
+ minTs: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum timestamp filter (Unix timestamp)',
+ },
+ maxTs: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum timestamp filter (Unix timestamp)',
+ },
+ subaccount: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Subaccount to filter orders',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-200, default: 100)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.ticker) queryParams.append('ticker', params.ticker)
+ if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker)
+ if (params.status) queryParams.append('status', params.status)
+ if (params.minTs) queryParams.append('min_ts', params.minTs)
+ if (params.maxTs) queryParams.append('max_ts', params.maxTs)
+ if (params.subaccount) queryParams.append('subaccount', params.subaccount)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/portfolio/orders')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: (params) => {
+ const path = '/trade-api/v2/portfolio/orders'
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_orders_v2')
+ }
+
+ const rawOrders = data.orders || []
+ const orders: KalshiOrderV2[] = rawOrders.map((order: any) => ({
+ order_id: order.order_id ?? null,
+ user_id: order.user_id ?? null,
+ client_order_id: order.client_order_id ?? null,
+ ticker: order.ticker ?? null,
+ side: order.side ?? null,
+ action: order.action ?? null,
+ type: order.type ?? null,
+ status: order.status ?? null,
+ yes_price: order.yes_price ?? null,
+ no_price: order.no_price ?? null,
+ yes_price_dollars: order.yes_price_dollars ?? null,
+ no_price_dollars: order.no_price_dollars ?? null,
+ fill_count: order.fill_count ?? null,
+ fill_count_fp: order.fill_count_fp ?? null,
+ remaining_count: order.remaining_count ?? null,
+ remaining_count_fp: order.remaining_count_fp ?? null,
+ initial_count: order.initial_count ?? null,
+ initial_count_fp: order.initial_count_fp ?? null,
+ taker_fees: order.taker_fees ?? null,
+ maker_fees: order.maker_fees ?? null,
+ taker_fees_dollars: order.taker_fees_dollars ?? null,
+ maker_fees_dollars: order.maker_fees_dollars ?? null,
+ taker_fill_cost: order.taker_fill_cost ?? null,
+ maker_fill_cost: order.maker_fill_cost ?? null,
+ taker_fill_cost_dollars: order.taker_fill_cost_dollars ?? null,
+ maker_fill_cost_dollars: order.maker_fill_cost_dollars ?? null,
+ queue_position: order.queue_position ?? null,
+ expiration_time: order.expiration_time ?? null,
+ created_time: order.created_time ?? null,
+ last_update_time: order.last_update_time ?? null,
+ self_trade_prevention_type: order.self_trade_prevention_type ?? null,
+ order_group_id: order.order_group_id ?? null,
+ cancel_order_on_pause: order.cancel_order_on_pause ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ orders,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ orders: {
+ type: 'array',
+ description: 'Array of order objects with full API response fields',
+ properties: {
+ order_id: { type: 'string', description: 'Order ID' },
+ user_id: { type: 'string', description: 'User ID' },
+ client_order_id: { type: 'string', description: 'Client order ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ side: { type: 'string', description: 'Order side (yes/no)' },
+ action: { type: 'string', description: 'Action (buy/sell)' },
+ type: { type: 'string', description: 'Order type (limit/market)' },
+ status: { type: 'string', description: 'Order status (resting/canceled/executed)' },
+ yes_price: { type: 'number', description: 'Yes price in cents' },
+ no_price: { type: 'number', description: 'No price in cents' },
+ yes_price_dollars: { type: 'string', description: 'Yes price in dollars' },
+ no_price_dollars: { type: 'string', description: 'No price in dollars' },
+ fill_count: { type: 'number', description: 'Filled contract count' },
+ fill_count_fp: { type: 'string', description: 'Filled count (fixed-point)' },
+ remaining_count: { type: 'number', description: 'Remaining contracts' },
+ remaining_count_fp: { type: 'string', description: 'Remaining count (fixed-point)' },
+ initial_count: { type: 'number', description: 'Initial contract count' },
+ initial_count_fp: { type: 'string', description: 'Initial count (fixed-point)' },
+ taker_fees: { type: 'number', description: 'Taker fees in cents' },
+ maker_fees: { type: 'number', description: 'Maker fees in cents' },
+ taker_fees_dollars: { type: 'string', description: 'Taker fees in dollars' },
+ maker_fees_dollars: { type: 'string', description: 'Maker fees in dollars' },
+ taker_fill_cost: { type: 'number', description: 'Taker fill cost in cents' },
+ maker_fill_cost: { type: 'number', description: 'Maker fill cost in cents' },
+ taker_fill_cost_dollars: { type: 'string', description: 'Taker fill cost in dollars' },
+ maker_fill_cost_dollars: { type: 'string', description: 'Maker fill cost in dollars' },
+ queue_position: { type: 'number', description: 'Queue position (deprecated)' },
+ expiration_time: { type: 'string', description: 'Order expiration time' },
+ created_time: { type: 'string', description: 'Order creation time' },
+ last_update_time: { type: 'string', description: 'Last update time' },
+ self_trade_prevention_type: { type: 'string', description: 'Self-trade prevention type' },
+ order_group_id: { type: 'string', description: 'Order group ID' },
+ cancel_order_on_pause: { type: 'boolean', description: 'Cancel on market pause' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+ }
diff --git a/apps/sim/tools/kalshi/get_positions.ts b/apps/sim/tools/kalshi/get_positions.ts
index 4dfa1573a..3cae27ef6 100644
--- a/apps/sim/tools/kalshi/get_positions.ts
+++ b/apps/sim/tools/kalshi/get_positions.ts
@@ -126,3 +126,195 @@ export const kalshiGetPositionsTool: ToolConfig<
},
},
}
+
+/**
+ * V2 Params for Get Positions - removes invalid settlementStatus, adds countFilter and subaccount
+ */
+export interface KalshiGetPositionsV2Params extends KalshiAuthParams, KalshiPaginationParams {
+ ticker?: string
+ eventTicker?: string
+ countFilter?: string
+ subaccount?: string
+}
+
+/**
+ * V2 Response matching Kalshi API exactly
+ */
+export interface KalshiGetPositionsV2Response {
+ success: boolean
+ output: {
+ market_positions: Array<{
+ ticker: string
+ event_ticker: string
+ event_title: string | null
+ market_title: string | null
+ position: number
+ market_exposure: number | null
+ realized_pnl: number | null
+ total_traded: number | null
+ resting_orders_count: number | null
+ fees_paid: number | null
+ }>
+ event_positions: Array<{
+ event_ticker: string
+ event_exposure: number
+ realized_pnl: number | null
+ total_cost: number | null
+ }> | null
+ cursor: string | null
+ }
+}
+
+export const kalshiGetPositionsV2Tool: ToolConfig<
+ KalshiGetPositionsV2Params,
+ KalshiGetPositionsV2Response
+> = {
+ id: 'kalshi_get_positions_v2',
+ name: 'Get Positions from Kalshi V2',
+ description: 'Retrieve your open positions from Kalshi (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {
+ keyId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your Kalshi API Key ID',
+ },
+ privateKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Your RSA Private Key (PEM format)',
+ },
+ ticker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by market ticker',
+ },
+ eventTicker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by event ticker (max 10 comma-separated)',
+ },
+ countFilter: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by count (all, positive, negative). Default: all',
+ },
+ subaccount: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Subaccount to get positions for',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-1000, default: 100)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.ticker) queryParams.append('ticker', params.ticker)
+ if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker)
+ if (params.countFilter) queryParams.append('count_filter', params.countFilter)
+ if (params.subaccount) queryParams.append('subaccount', params.subaccount)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/portfolio/positions')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: (params) => {
+ const path = '/trade-api/v2/portfolio/positions'
+ return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
+ },
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_positions_v2')
+ }
+
+ const marketPositions = (data.market_positions || []).map((p: Record) => ({
+ ticker: p.ticker ?? null,
+ event_ticker: p.event_ticker ?? null,
+ event_title: p.event_title ?? null,
+ market_title: p.market_title ?? null,
+ position: p.position ?? 0,
+ market_exposure: p.market_exposure ?? null,
+ realized_pnl: p.realized_pnl ?? null,
+ total_traded: p.total_traded ?? null,
+ resting_orders_count: p.resting_orders_count ?? null,
+ fees_paid: p.fees_paid ?? null,
+ }))
+
+ const eventPositions = data.event_positions
+ ? (data.event_positions as Array>).map((p) => ({
+ event_ticker: (p.event_ticker as string) ?? '',
+ event_exposure: (p.event_exposure as number) ?? 0,
+ realized_pnl: (p.realized_pnl as number | null) ?? null,
+ total_cost: (p.total_cost as number | null) ?? null,
+ }))
+ : null
+
+ return {
+ success: true,
+ output: {
+ market_positions: marketPositions,
+ event_positions: eventPositions,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ market_positions: {
+ type: 'array',
+ description: 'Array of market position objects',
+ properties: {
+ ticker: { type: 'string', description: 'Market ticker' },
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ event_title: { type: 'string', description: 'Event title' },
+ market_title: { type: 'string', description: 'Market title' },
+ position: { type: 'number', description: 'Position size' },
+ market_exposure: { type: 'number', description: 'Market exposure' },
+ realized_pnl: { type: 'number', description: 'Realized P&L' },
+ total_traded: { type: 'number', description: 'Total traded' },
+ resting_orders_count: { type: 'number', description: 'Resting orders count' },
+ fees_paid: { type: 'number', description: 'Fees paid' },
+ },
+ },
+ event_positions: {
+ type: 'array',
+ description: 'Array of event position objects',
+ properties: {
+ event_ticker: { type: 'string', description: 'Event ticker' },
+ event_exposure: { type: 'number', description: 'Event exposure' },
+ realized_pnl: { type: 'number', description: 'Realized P&L' },
+ total_cost: { type: 'number', description: 'Total cost' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_series_by_ticker.ts b/apps/sim/tools/kalshi/get_series_by_ticker.ts
index 781a48116..bbf070374 100644
--- a/apps/sim/tools/kalshi/get_series_by_ticker.ts
+++ b/apps/sim/tools/kalshi/get_series_by_ticker.ts
@@ -65,3 +65,141 @@ export const kalshiGetSeriesByTickerTool: ToolConfig<
},
},
}
+
+/**
+ * V2 Params for Get Series by Ticker
+ */
+export interface KalshiGetSeriesByTickerV2Params {
+ seriesTicker: string
+ includeVolume?: string
+}
+
+/**
+ * V2 Response matching Kalshi API exactly
+ */
+export interface KalshiGetSeriesByTickerV2Response {
+ success: boolean
+ output: {
+ series: {
+ ticker: string
+ title: string
+ frequency: string
+ category: string
+ tags: string[] | null
+ settlement_sources: Array<{
+ name: string
+ url: string
+ }> | null
+ contract_url: string | null
+ contract_terms_url: string | null
+ fee_type: string | null
+ fee_multiplier: number | null
+ additional_prohibitions: string[] | null
+ product_metadata: Record | null
+ volume: number | null
+ volume_fp: number | null
+ }
+ }
+}
+
+export const kalshiGetSeriesByTickerV2Tool: ToolConfig<
+ KalshiGetSeriesByTickerV2Params,
+ KalshiGetSeriesByTickerV2Response
+> = {
+ id: 'kalshi_get_series_by_ticker_v2',
+ name: 'Get Series by Ticker from Kalshi V2',
+ description: 'Retrieve details of a specific market series by ticker (V2 - exact API response)',
+ version: '2.0.0',
+
+ params: {
+ seriesTicker: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Series ticker',
+ },
+ includeVolume: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Include volume data in response (true/false)',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.includeVolume) queryParams.append('include_volume', params.includeVolume)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl(`/series/${params.seriesTicker}`)
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_series_by_ticker_v2')
+ }
+
+ const series = data.series || data
+
+ const settlementSources = series.settlement_sources
+ ? (series.settlement_sources as Array>).map((s) => ({
+ name: (s.name as string) ?? null,
+ url: (s.url as string) ?? null,
+ }))
+ : null
+
+ return {
+ success: true,
+ output: {
+ series: {
+ ticker: series.ticker ?? null,
+ title: series.title ?? null,
+ frequency: series.frequency ?? null,
+ category: series.category ?? null,
+ tags: series.tags ?? null,
+ settlement_sources: settlementSources,
+ contract_url: series.contract_url ?? null,
+ contract_terms_url: series.contract_terms_url ?? null,
+ fee_type: series.fee_type ?? null,
+ fee_multiplier: series.fee_multiplier ?? null,
+ additional_prohibitions: series.additional_prohibitions ?? null,
+ product_metadata: series.product_metadata ?? null,
+ volume: series.volume ?? null,
+ volume_fp: series.volume_fp ?? null,
+ },
+ },
+ }
+ },
+
+ outputs: {
+ series: {
+ type: 'object',
+ description: 'Series object with full details matching Kalshi API response',
+ properties: {
+ ticker: { type: 'string', description: 'Series ticker' },
+ title: { type: 'string', description: 'Series title' },
+ frequency: { type: 'string', description: 'Event frequency' },
+ category: { type: 'string', description: 'Series category' },
+ tags: { type: 'array', description: 'Series tags' },
+ settlement_sources: { type: 'array', description: 'Settlement sources' },
+ contract_url: { type: 'string', description: 'Contract URL' },
+ contract_terms_url: { type: 'string', description: 'Contract terms URL' },
+ fee_type: { type: 'string', description: 'Fee type' },
+ fee_multiplier: { type: 'number', description: 'Fee multiplier' },
+ additional_prohibitions: { type: 'array', description: 'Additional prohibitions' },
+ product_metadata: { type: 'object', description: 'Product metadata' },
+ volume: { type: 'number', description: 'Series volume' },
+ volume_fp: { type: 'number', description: 'Volume (fixed-point)' },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/kalshi/get_trades.ts b/apps/sim/tools/kalshi/get_trades.ts
index 30fc268ae..5066e2d48 100644
--- a/apps/sim/tools/kalshi/get_trades.ts
+++ b/apps/sim/tools/kalshi/get_trades.ts
@@ -80,3 +80,138 @@ export const kalshiGetTradesTool: ToolConfig
+ cursor: string | null
+ }
+}
+
+export const kalshiGetTradesV2Tool: ToolConfig =
+ {
+ id: 'kalshi_get_trades_v2',
+ name: 'Get Trades from Kalshi V2',
+ description:
+ 'Retrieve recent trades with additional filtering options (V2 - includes trade_id and count_fp)',
+ version: '2.0.0',
+
+ params: {
+ ticker: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter by market ticker',
+ },
+ minTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Minimum timestamp (Unix seconds)',
+ },
+ maxTs: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum timestamp (Unix seconds)',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Number of results (1-1000, default: 100)',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor for next page',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.ticker) queryParams.append('ticker', params.ticker)
+ if (params.minTs) queryParams.append('min_ts', params.minTs.toString())
+ if (params.maxTs) queryParams.append('max_ts', params.maxTs.toString())
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.cursor) queryParams.append('cursor', params.cursor)
+
+ const query = queryParams.toString()
+ const url = buildKalshiUrl('/markets/trades')
+ return query ? `${url}?${query}` : url
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handleKalshiError(data, response.status, 'get_trades_v2')
+ }
+
+ const trades = (data.trades || []).map((t: Record) => ({
+ trade_id: t.trade_id ?? null,
+ ticker: t.ticker ?? null,
+ yes_price: t.yes_price ?? null,
+ no_price: t.no_price ?? null,
+ count: t.count ?? null,
+ count_fp: t.count_fp ?? null,
+ created_time: t.created_time ?? null,
+ taker_side: t.taker_side ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ trades,
+ cursor: data.cursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ trades: {
+ type: 'array',
+ description: 'Array of trade objects with trade_id and count_fp',
+ properties: {
+ trade_id: { type: 'string', description: 'Trade ID' },
+ ticker: { type: 'string', description: 'Market ticker' },
+ yes_price: { type: 'number', description: 'Yes price' },
+ no_price: { type: 'number', description: 'No price' },
+ count: { type: 'number', description: 'Number of contracts' },
+ count_fp: { type: 'number', description: 'Count (fixed-point)' },
+ created_time: { type: 'string', description: 'Trade creation time' },
+ taker_side: { type: 'string', description: 'Taker side (yes/no)' },
+ },
+ },
+ cursor: {
+ type: 'string',
+ description: 'Pagination cursor for fetching more results',
+ },
+ },
+ }
diff --git a/apps/sim/tools/kalshi/index.ts b/apps/sim/tools/kalshi/index.ts
index 3b6cc2a17..122cee6fd 100644
--- a/apps/sim/tools/kalshi/index.ts
+++ b/apps/sim/tools/kalshi/index.ts
@@ -1,17 +1,17 @@
-export { kalshiAmendOrderTool } from './amend_order'
-export { kalshiCancelOrderTool } from './cancel_order'
-export { kalshiCreateOrderTool } from './create_order'
-export { kalshiGetBalanceTool } from './get_balance'
-export { kalshiGetCandlesticksTool } from './get_candlesticks'
-export { kalshiGetEventTool } from './get_event'
-export { kalshiGetEventsTool } from './get_events'
-export { kalshiGetExchangeStatusTool } from './get_exchange_status'
-export { kalshiGetFillsTool } from './get_fills'
-export { kalshiGetMarketTool } from './get_market'
-export { kalshiGetMarketsTool } from './get_markets'
-export { kalshiGetOrderTool } from './get_order'
-export { kalshiGetOrderbookTool } from './get_orderbook'
-export { kalshiGetOrdersTool } from './get_orders'
-export { kalshiGetPositionsTool } from './get_positions'
-export { kalshiGetSeriesByTickerTool } from './get_series_by_ticker'
-export { kalshiGetTradesTool } from './get_trades'
+export { kalshiAmendOrderTool, kalshiAmendOrderV2Tool } from './amend_order'
+export { kalshiCancelOrderTool, kalshiCancelOrderV2Tool } from './cancel_order'
+export { kalshiCreateOrderTool, kalshiCreateOrderV2Tool } from './create_order'
+export { kalshiGetBalanceTool, kalshiGetBalanceV2Tool } from './get_balance'
+export { kalshiGetCandlesticksTool, kalshiGetCandlesticksV2Tool } from './get_candlesticks'
+export { kalshiGetEventTool, kalshiGetEventV2Tool } from './get_event'
+export { kalshiGetEventsTool, kalshiGetEventsV2Tool } from './get_events'
+export { kalshiGetExchangeStatusTool, kalshiGetExchangeStatusV2Tool } from './get_exchange_status'
+export { kalshiGetFillsTool, kalshiGetFillsV2Tool } from './get_fills'
+export { kalshiGetMarketTool, kalshiGetMarketV2Tool } from './get_market'
+export { kalshiGetMarketsTool, kalshiGetMarketsV2Tool } from './get_markets'
+export { kalshiGetOrderTool, kalshiGetOrderV2Tool } from './get_order'
+export { kalshiGetOrderbookTool, kalshiGetOrderbookV2Tool } from './get_orderbook'
+export { kalshiGetOrdersTool, kalshiGetOrdersV2Tool } from './get_orders'
+export { kalshiGetPositionsTool, kalshiGetPositionsV2Tool } from './get_positions'
+export { kalshiGetSeriesByTickerTool, kalshiGetSeriesByTickerV2Tool } from './get_series_by_ticker'
+export { kalshiGetTradesTool, kalshiGetTradesV2Tool } from './get_trades'
diff --git a/apps/sim/tools/polymarket/get_activity.ts b/apps/sim/tools/polymarket/get_activity.ts
new file mode 100644
index 000000000..5f04d5976
--- /dev/null
+++ b/apps/sim/tools/polymarket/get_activity.ts
@@ -0,0 +1,206 @@
+import type { PolymarketActivity } from '@/tools/polymarket/types'
+import { buildDataUrl, handlePolymarketError } from '@/tools/polymarket/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface PolymarketGetActivityParams {
+ user: string
+ limit?: string
+ offset?: string
+ market?: string
+ eventId?: string
+ type?: string
+ start?: number
+ end?: number
+ sortBy?: string
+ sortDirection?: string
+ side?: string
+}
+
+export interface PolymarketGetActivityResponse {
+ success: boolean
+ output: {
+ activity: PolymarketActivity[]
+ }
+}
+
+export const polymarketGetActivityTool: ToolConfig<
+ PolymarketGetActivityParams,
+ PolymarketGetActivityResponse
+> = {
+ id: 'polymarket_get_activity',
+ name: 'Get Activity from Polymarket',
+ description:
+ 'Retrieve on-chain activity for a user including trades, splits, merges, redemptions, rewards, and conversions',
+ version: '1.0.0',
+
+ params: {
+ user: {
+ type: 'string',
+ required: true,
+ description: 'User wallet address (0x-prefixed)',
+ visibility: 'user-or-llm',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ description: 'Maximum results (default: 100, max: 500)',
+ visibility: 'user-or-llm',
+ },
+ offset: {
+ type: 'string',
+ required: false,
+ description: 'Pagination offset (default: 0, max: 10000)',
+ visibility: 'user-or-llm',
+ },
+ market: {
+ type: 'string',
+ required: false,
+ description: 'Comma-separated condition IDs (mutually exclusive with eventId)',
+ visibility: 'user-or-llm',
+ },
+ eventId: {
+ type: 'string',
+ required: false,
+ description: 'Comma-separated event IDs (mutually exclusive with market)',
+ visibility: 'user-or-llm',
+ },
+ type: {
+ type: 'string',
+ required: false,
+ description:
+ 'Activity type filter: TRADE, SPLIT, MERGE, REDEEM, REWARD, CONVERSION, MAKER_REBATE',
+ visibility: 'user-or-llm',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ description: 'Start timestamp (Unix seconds)',
+ visibility: 'user-or-llm',
+ },
+ end: {
+ type: 'number',
+ required: false,
+ description: 'End timestamp (Unix seconds)',
+ visibility: 'user-or-llm',
+ },
+ sortBy: {
+ type: 'string',
+ required: false,
+ description: 'Sort field: TIMESTAMP, TOKENS, or CASH (default: TIMESTAMP)',
+ visibility: 'user-or-llm',
+ },
+ sortDirection: {
+ type: 'string',
+ required: false,
+ description: 'Sort direction: ASC or DESC (default: DESC)',
+ visibility: 'user-or-llm',
+ },
+ side: {
+ type: 'string',
+ required: false,
+ description: 'Trade side filter: BUY or SELL (only applies to trades)',
+ visibility: 'user-or-llm',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ queryParams.append('user', params.user)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.offset) queryParams.append('offset', params.offset)
+ if (params.market) queryParams.append('market', params.market)
+ if (params.eventId) queryParams.append('eventId', params.eventId)
+ if (params.type) queryParams.append('type', params.type)
+ if (params.start != null && !Number.isNaN(params.start))
+ queryParams.append('start', String(params.start))
+ if (params.end != null && !Number.isNaN(params.end))
+ queryParams.append('end', String(params.end))
+ if (params.sortBy) queryParams.append('sortBy', params.sortBy)
+ if (params.sortDirection) queryParams.append('sortDirection', params.sortDirection)
+ if (params.side) queryParams.append('side', params.side)
+ return `${buildDataUrl('/activity')}?${queryParams.toString()}`
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handlePolymarketError(data, response.status, 'get_activity')
+ }
+
+ const activityList = Array.isArray(data) ? data : []
+
+ const activity: PolymarketActivity[] = activityList.map((a: any) => ({
+ proxyWallet: a.proxyWallet ?? null,
+ timestamp: a.timestamp ?? 0,
+ conditionId: a.conditionId ?? '',
+ type: a.type ?? '',
+ size: a.size ?? 0,
+ usdcSize: a.usdcSize ?? 0,
+ transactionHash: a.transactionHash ?? null,
+ price: a.price ?? null,
+ asset: a.asset ?? null,
+ side: a.side ?? null,
+ outcomeIndex: a.outcomeIndex ?? null,
+ title: a.title ?? null,
+ slug: a.slug ?? null,
+ icon: a.icon ?? null,
+ eventSlug: a.eventSlug ?? null,
+ outcome: a.outcome ?? null,
+ name: a.name ?? null,
+ pseudonym: a.pseudonym ?? null,
+ bio: a.bio ?? null,
+ profileImage: a.profileImage ?? null,
+ profileImageOptimized: a.profileImageOptimized ?? null,
+ }))
+
+ return {
+ success: true,
+ output: {
+ activity,
+ },
+ }
+ },
+
+ outputs: {
+ activity: {
+ type: 'array',
+ description: 'Array of activity entries',
+ items: {
+ type: 'object',
+ properties: {
+ proxyWallet: { type: 'string', description: 'User proxy wallet address' },
+ timestamp: { type: 'number', description: 'Unix timestamp of activity' },
+ conditionId: { type: 'string', description: 'Market condition ID' },
+ type: {
+ type: 'string',
+ description: 'Activity type (TRADE, SPLIT, MERGE, REDEEM, REWARD, CONVERSION)',
+ },
+ size: { type: 'number', description: 'Size in tokens' },
+ usdcSize: { type: 'number', description: 'Size in USDC' },
+ transactionHash: { type: 'string', description: 'Blockchain transaction hash' },
+ price: { type: 'number', description: 'Price (for trades)' },
+ asset: { type: 'string', description: 'Asset/token ID' },
+ side: { type: 'string', description: 'Trade side (BUY/SELL)' },
+ outcomeIndex: { type: 'number', description: 'Outcome index' },
+ title: { type: 'string', description: 'Market title' },
+ slug: { type: 'string', description: 'Market slug' },
+ icon: { type: 'string', description: 'Market icon URL' },
+ eventSlug: { type: 'string', description: 'Event slug' },
+ outcome: { type: 'string', description: 'Outcome name' },
+ name: { type: 'string', description: 'User display name' },
+ pseudonym: { type: 'string', description: 'User pseudonym' },
+ bio: { type: 'string', description: 'User bio' },
+ profileImage: { type: 'string', description: 'User profile image URL' },
+ profileImageOptimized: { type: 'string', description: 'Optimized profile image URL' },
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/polymarket/get_event.ts b/apps/sim/tools/polymarket/get_event.ts
index c1dd86512..e957f6dd2 100644
--- a/apps/sim/tools/polymarket/get_event.ts
+++ b/apps/sim/tools/polymarket/get_event.ts
@@ -1,6 +1,6 @@
+import type { PolymarketEvent } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketEvent } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetEventParams {
eventId?: string // Event ID
@@ -71,6 +71,26 @@ export const polymarketGetEventTool: ToolConfig<
event: {
type: 'object',
description: 'Event object with details',
+ properties: {
+ id: { type: 'string', description: 'Event ID' },
+ ticker: { type: 'string', description: 'Event ticker' },
+ slug: { type: 'string', description: 'Event slug' },
+ title: { type: 'string', description: 'Event title' },
+ description: { type: 'string', description: 'Event description' },
+ startDate: { type: 'string', description: 'Start date' },
+ creationDate: { type: 'string', description: 'Creation date' },
+ endDate: { type: 'string', description: 'End date' },
+ image: { type: 'string', description: 'Event image URL' },
+ icon: { type: 'string', description: 'Event icon URL' },
+ active: { type: 'boolean', description: 'Whether event is active' },
+ closed: { type: 'boolean', description: 'Whether event is closed' },
+ archived: { type: 'boolean', description: 'Whether event is archived' },
+ liquidity: { type: 'number', description: 'Total liquidity' },
+ volume: { type: 'number', description: 'Total volume' },
+ openInterest: { type: 'number', description: 'Open interest' },
+ commentCount: { type: 'number', description: 'Comment count' },
+ markets: { type: 'array', description: 'Array of markets in this event' },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_events.ts b/apps/sim/tools/polymarket/get_events.ts
index b603948ff..8b030f46f 100644
--- a/apps/sim/tools/polymarket/get_events.ts
+++ b/apps/sim/tools/polymarket/get_events.ts
@@ -1,12 +1,12 @@
+import type { PolymarketEvent, PolymarketPaginationParams } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketEvent, PolymarketPaginationParams } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetEventsParams extends PolymarketPaginationParams {
- closed?: string // 'true' or 'false' - filter for closed/active events
- order?: string // sort field (e.g., 'volume', 'liquidity', 'startDate', 'endDate')
- ascending?: string // 'true' or 'false' - sort direction
- tagId?: string // filter by tag ID
+ closed?: string
+ order?: string
+ ascending?: string
+ tagId?: string
}
export interface PolymarketGetEventsResponse {
@@ -29,7 +29,7 @@ export const polymarketGetEventsTool: ToolConfig<
closed: {
type: 'string',
required: false,
- description: 'Filter by closed status (true/false). Use false for active events only.',
+ description: 'Filter by closed status (true/false). Use false for open events only.',
visibility: 'user-or-llm',
},
order: {
@@ -71,13 +71,11 @@ export const polymarketGetEventsTool: ToolConfig<
if (params.order) queryParams.append('order', params.order)
if (params.ascending) queryParams.append('ascending', params.ascending)
if (params.tagId) queryParams.append('tag_id', params.tagId)
- // Default limit to 50 to prevent browser crashes from large data sets
queryParams.append('limit', params.limit || '50')
if (params.offset) queryParams.append('offset', params.offset)
- const query = queryParams.toString()
const url = buildGammaUrl('/events')
- return `${url}?${query}`
+ return `${url}?${queryParams.toString()}`
},
method: 'GET',
headers: () => ({
@@ -92,7 +90,6 @@ export const polymarketGetEventsTool: ToolConfig<
handlePolymarketError(data, response.status, 'get_events')
}
- // Response is an array of events
const events = Array.isArray(data) ? data : []
return {
@@ -107,6 +104,26 @@ export const polymarketGetEventsTool: ToolConfig<
events: {
type: 'array',
description: 'Array of event objects',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Event ID' },
+ ticker: { type: 'string', description: 'Event ticker' },
+ slug: { type: 'string', description: 'Event slug' },
+ title: { type: 'string', description: 'Event title' },
+ description: { type: 'string', description: 'Event description' },
+ startDate: { type: 'string', description: 'Start date' },
+ endDate: { type: 'string', description: 'End date' },
+ image: { type: 'string', description: 'Event image URL' },
+ icon: { type: 'string', description: 'Event icon URL' },
+ active: { type: 'boolean', description: 'Whether event is active' },
+ closed: { type: 'boolean', description: 'Whether event is closed' },
+ archived: { type: 'boolean', description: 'Whether event is archived' },
+ liquidity: { type: 'number', description: 'Total liquidity' },
+ volume: { type: 'number', description: 'Total volume' },
+ markets: { type: 'array', description: 'Array of markets in this event' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_holders.ts b/apps/sim/tools/polymarket/get_holders.ts
new file mode 100644
index 000000000..8fe772a92
--- /dev/null
+++ b/apps/sim/tools/polymarket/get_holders.ts
@@ -0,0 +1,132 @@
+import type { PolymarketMarketHolders } from '@/tools/polymarket/types'
+import { buildDataUrl, handlePolymarketError } from '@/tools/polymarket/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface PolymarketGetHoldersParams {
+ market: string
+ limit?: string
+ minBalance?: string
+}
+
+export interface PolymarketGetHoldersResponse {
+ success: boolean
+ output: {
+ holders: PolymarketMarketHolders[]
+ }
+}
+
+export const polymarketGetHoldersTool: ToolConfig<
+ PolymarketGetHoldersParams,
+ PolymarketGetHoldersResponse
+> = {
+ id: 'polymarket_get_holders',
+ name: 'Get Market Holders from Polymarket',
+ description: 'Retrieve top holders of a specific market token',
+ version: '1.0.0',
+
+ params: {
+ market: {
+ type: 'string',
+ required: true,
+ description: 'Comma-separated list of condition IDs',
+ visibility: 'user-or-llm',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ description: 'Number of holders to return (0-20, default: 20)',
+ visibility: 'user-or-llm',
+ },
+ minBalance: {
+ type: 'string',
+ required: false,
+ description: 'Minimum balance threshold (default: 1)',
+ visibility: 'user-or-llm',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ queryParams.append('market', params.market)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.minBalance) queryParams.append('minBalance', params.minBalance)
+ return `${buildDataUrl('/holders')}?${queryParams.toString()}`
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handlePolymarketError(data, response.status, 'get_holders')
+ }
+
+ const marketHolders = Array.isArray(data) ? data : []
+
+ const holders: PolymarketMarketHolders[] = marketHolders.map((mh: any) => ({
+ token: mh.token ?? '',
+ holders: (mh.holders ?? []).map((h: any) => ({
+ proxyWallet: h.proxyWallet ?? '',
+ bio: h.bio ?? null,
+ asset: h.asset ?? '',
+ pseudonym: h.pseudonym ?? null,
+ amount: h.amount ?? 0,
+ displayUsernamePublic: h.displayUsernamePublic ?? false,
+ outcomeIndex: h.outcomeIndex ?? 0,
+ name: h.name ?? null,
+ profileImage: h.profileImage ?? null,
+ profileImageOptimized: h.profileImageOptimized ?? null,
+ })),
+ }))
+
+ return {
+ success: true,
+ output: {
+ holders,
+ },
+ }
+ },
+
+ outputs: {
+ holders: {
+ type: 'array',
+ description: 'Array of market holder groups by token',
+ items: {
+ type: 'object',
+ properties: {
+ token: { type: 'string', description: 'Token/asset ID' },
+ holders: {
+ type: 'array',
+ description: 'Array of holders for this token',
+ items: {
+ type: 'object',
+ properties: {
+ proxyWallet: { type: 'string', description: 'Holder wallet address' },
+ bio: { type: 'string', description: 'Holder bio' },
+ asset: { type: 'string', description: 'Asset ID' },
+ pseudonym: { type: 'string', description: 'Holder pseudonym' },
+ amount: { type: 'number', description: 'Amount held' },
+ displayUsernamePublic: {
+ type: 'boolean',
+ description: 'Whether username is publicly displayed',
+ },
+ outcomeIndex: { type: 'number', description: 'Outcome index' },
+ name: { type: 'string', description: 'Holder display name' },
+ profileImage: { type: 'string', description: 'Profile image URL' },
+ profileImageOptimized: {
+ type: 'string',
+ description: 'Optimized profile image URL',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/polymarket/get_last_trade_price.ts b/apps/sim/tools/polymarket/get_last_trade_price.ts
index 3280e0f03..92f4e52b0 100644
--- a/apps/sim/tools/polymarket/get_last_trade_price.ts
+++ b/apps/sim/tools/polymarket/get_last_trade_price.ts
@@ -1,5 +1,5 @@
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetLastTradePriceParams {
tokenId: string // The token ID (CLOB token ID from market)
@@ -9,6 +9,7 @@ export interface PolymarketGetLastTradePriceResponse {
success: boolean
output: {
price: string
+ side: string
}
}
@@ -52,7 +53,8 @@ export const polymarketGetLastTradePriceTool: ToolConfig<
return {
success: true,
output: {
- price: typeof data === 'string' ? data : data.price || '',
+ price: data.price ?? '',
+ side: data.side ?? '',
},
}
},
@@ -62,5 +64,9 @@ export const polymarketGetLastTradePriceTool: ToolConfig<
type: 'string',
description: 'Last trade price',
},
+ side: {
+ type: 'string',
+ description: 'Side of the last trade (BUY or SELL)',
+ },
},
}
diff --git a/apps/sim/tools/polymarket/get_leaderboard.ts b/apps/sim/tools/polymarket/get_leaderboard.ts
new file mode 100644
index 000000000..0fb0485e7
--- /dev/null
+++ b/apps/sim/tools/polymarket/get_leaderboard.ts
@@ -0,0 +1,143 @@
+import type { PolymarketLeaderboardEntry } from '@/tools/polymarket/types'
+import { buildDataUrl, handlePolymarketError } from '@/tools/polymarket/types'
+import type { ToolConfig } from '@/tools/types'
+
+export interface PolymarketGetLeaderboardParams {
+ category?: string
+ timePeriod?: string
+ orderBy?: string
+ limit?: string
+ offset?: string
+ user?: string
+ userName?: string
+}
+
+export interface PolymarketGetLeaderboardResponse {
+ success: boolean
+ output: {
+ leaderboard: PolymarketLeaderboardEntry[]
+ }
+}
+
+export const polymarketGetLeaderboardTool: ToolConfig<
+ PolymarketGetLeaderboardParams,
+ PolymarketGetLeaderboardResponse
+> = {
+ id: 'polymarket_get_leaderboard',
+ name: 'Get Leaderboard from Polymarket',
+ description: 'Retrieve trader leaderboard rankings by profit/loss or volume',
+ version: '1.0.0',
+
+ params: {
+ category: {
+ type: 'string',
+ required: false,
+ description:
+ 'Category filter: OVERALL, POLITICS, SPORTS, CRYPTO, CULTURE, MENTIONS, WEATHER, ECONOMICS, TECH, FINANCE (default: OVERALL)',
+ visibility: 'user-or-llm',
+ },
+ timePeriod: {
+ type: 'string',
+ required: false,
+ description: 'Time period: DAY, WEEK, MONTH, ALL (default: DAY)',
+ visibility: 'user-or-llm',
+ },
+ orderBy: {
+ type: 'string',
+ required: false,
+ description: 'Order by: PNL or VOL (default: PNL)',
+ visibility: 'user-or-llm',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ description: 'Number of results (1-50, default: 25)',
+ visibility: 'user-or-llm',
+ },
+ offset: {
+ type: 'string',
+ required: false,
+ description: 'Pagination offset (0-1000, default: 0)',
+ visibility: 'user-or-llm',
+ },
+ user: {
+ type: 'string',
+ required: false,
+ description: 'Filter by specific user wallet address',
+ visibility: 'user-or-llm',
+ },
+ userName: {
+ type: 'string',
+ required: false,
+ description: 'Filter by username',
+ visibility: 'user-or-llm',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const queryParams = new URLSearchParams()
+ if (params.category) queryParams.append('category', params.category)
+ if (params.timePeriod) queryParams.append('timePeriod', params.timePeriod)
+ if (params.orderBy) queryParams.append('orderBy', params.orderBy)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.offset) queryParams.append('offset', params.offset)
+ if (params.user) queryParams.append('user', params.user)
+ if (params.userName) queryParams.append('userName', params.userName)
+ const query = queryParams.toString()
+ return query ? `${buildDataUrl('/v1/leaderboard')}?${query}` : buildDataUrl('/v1/leaderboard')
+ },
+ method: 'GET',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ handlePolymarketError(data, response.status, 'get_leaderboard')
+ }
+
+ const entries = Array.isArray(data) ? data : []
+
+ const leaderboard: PolymarketLeaderboardEntry[] = entries.map((entry: any) => ({
+ rank: entry.rank ?? '',
+ proxyWallet: entry.proxyWallet ?? '',
+ userName: entry.userName ?? null,
+ vol: entry.vol ?? 0,
+ pnl: entry.pnl ?? 0,
+ profileImage: entry.profileImage ?? null,
+ xUsername: entry.xUsername ?? null,
+ verifiedBadge: entry.verifiedBadge ?? false,
+ }))
+
+ return {
+ success: true,
+ output: {
+ leaderboard,
+ },
+ }
+ },
+
+ outputs: {
+ leaderboard: {
+ type: 'array',
+ description: 'Array of leaderboard entries',
+ items: {
+ type: 'object',
+ properties: {
+ rank: { type: 'string', description: 'Leaderboard rank position' },
+ proxyWallet: { type: 'string', description: 'User proxy wallet address' },
+ userName: { type: 'string', description: 'User display name' },
+ vol: { type: 'number', description: 'Trading volume' },
+ pnl: { type: 'number', description: 'Profit and loss' },
+ profileImage: { type: 'string', description: 'User profile image URL' },
+ xUsername: { type: 'string', description: 'Twitter/X username' },
+ verifiedBadge: { type: 'boolean', description: 'Whether user has verified badge' },
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/polymarket/get_market.ts b/apps/sim/tools/polymarket/get_market.ts
index 1c4e9c27b..493a1bbec 100644
--- a/apps/sim/tools/polymarket/get_market.ts
+++ b/apps/sim/tools/polymarket/get_market.ts
@@ -1,6 +1,6 @@
+import type { PolymarketMarket } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketMarket } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetMarketParams {
marketId?: string // Market ID
@@ -71,6 +71,30 @@ export const polymarketGetMarketTool: ToolConfig<
market: {
type: 'object',
description: 'Market object with details',
+ properties: {
+ id: { type: 'string', description: 'Market ID' },
+ question: { type: 'string', description: 'Market question' },
+ conditionId: { type: 'string', description: 'Condition ID' },
+ slug: { type: 'string', description: 'Market slug' },
+ resolutionSource: { type: 'string', description: 'Resolution source' },
+ endDate: { type: 'string', description: 'End date' },
+ startDate: { type: 'string', description: 'Start date' },
+ image: { type: 'string', description: 'Market image URL' },
+ icon: { type: 'string', description: 'Market icon URL' },
+ description: { type: 'string', description: 'Market description' },
+ outcomes: { type: 'string', description: 'Outcomes JSON string' },
+ outcomePrices: { type: 'string', description: 'Outcome prices JSON string' },
+ volume: { type: 'string', description: 'Total volume' },
+ liquidity: { type: 'string', description: 'Total liquidity' },
+ active: { type: 'boolean', description: 'Whether market is active' },
+ closed: { type: 'boolean', description: 'Whether market is closed' },
+ archived: { type: 'boolean', description: 'Whether market is archived' },
+ volumeNum: { type: 'number', description: 'Volume as number' },
+ liquidityNum: { type: 'number', description: 'Liquidity as number' },
+ clobTokenIds: { type: 'array', description: 'CLOB token IDs' },
+ acceptingOrders: { type: 'boolean', description: 'Whether accepting orders' },
+ negRisk: { type: 'boolean', description: 'Whether negative risk' },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_markets.ts b/apps/sim/tools/polymarket/get_markets.ts
index 2183113ed..d4215bf50 100644
--- a/apps/sim/tools/polymarket/get_markets.ts
+++ b/apps/sim/tools/polymarket/get_markets.ts
@@ -1,12 +1,12 @@
+import type { PolymarketMarket, PolymarketPaginationParams } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketMarket, PolymarketPaginationParams } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetMarketsParams extends PolymarketPaginationParams {
- closed?: string // 'true' or 'false' - filter for closed/active markets
- order?: string // sort field - use camelCase (e.g., 'volumeNum', 'liquidityNum', 'startDate', 'endDate')
- ascending?: string // 'true' or 'false' - sort direction
- tagId?: string // filter by tag ID
+ closed?: string
+ order?: string
+ ascending?: string
+ tagId?: string
}
export interface PolymarketGetMarketsResponse {
@@ -29,7 +29,7 @@ export const polymarketGetMarketsTool: ToolConfig<
closed: {
type: 'string',
required: false,
- description: 'Filter by closed status (true/false). Use false for active markets only.',
+ description: 'Filter by closed status (true/false). Use false for open markets only.',
visibility: 'user-or-llm',
},
order: {
@@ -71,13 +71,11 @@ export const polymarketGetMarketsTool: ToolConfig<
if (params.order) queryParams.append('order', params.order)
if (params.ascending) queryParams.append('ascending', params.ascending)
if (params.tagId) queryParams.append('tag_id', params.tagId)
- // Default limit to 50 to prevent browser crashes from large data sets
queryParams.append('limit', params.limit || '50')
if (params.offset) queryParams.append('offset', params.offset)
- const query = queryParams.toString()
const url = buildGammaUrl('/markets')
- return `${url}?${query}`
+ return `${url}?${queryParams.toString()}`
},
method: 'GET',
headers: () => ({
@@ -107,6 +105,26 @@ export const polymarketGetMarketsTool: ToolConfig<
markets: {
type: 'array',
description: 'Array of market objects',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Market ID' },
+ question: { type: 'string', description: 'Market question' },
+ conditionId: { type: 'string', description: 'Condition ID' },
+ slug: { type: 'string', description: 'Market slug' },
+ endDate: { type: 'string', description: 'End date' },
+ image: { type: 'string', description: 'Market image URL' },
+ outcomes: { type: 'string', description: 'Outcomes JSON string' },
+ outcomePrices: { type: 'string', description: 'Outcome prices JSON string' },
+ volume: { type: 'string', description: 'Total volume' },
+ liquidity: { type: 'string', description: 'Total liquidity' },
+ active: { type: 'boolean', description: 'Whether market is active' },
+ closed: { type: 'boolean', description: 'Whether market is closed' },
+ volumeNum: { type: 'number', description: 'Volume as number' },
+ liquidityNum: { type: 'number', description: 'Liquidity as number' },
+ clobTokenIds: { type: 'array', description: 'CLOB token IDs' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_midpoint.ts b/apps/sim/tools/polymarket/get_midpoint.ts
index 7ea9abc1a..17723163e 100644
--- a/apps/sim/tools/polymarket/get_midpoint.ts
+++ b/apps/sim/tools/polymarket/get_midpoint.ts
@@ -1,5 +1,5 @@
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetMidpointParams {
tokenId: string // The token ID (CLOB token ID from market)
diff --git a/apps/sim/tools/polymarket/get_orderbook.ts b/apps/sim/tools/polymarket/get_orderbook.ts
index 90a95d9b6..0bd792443 100644
--- a/apps/sim/tools/polymarket/get_orderbook.ts
+++ b/apps/sim/tools/polymarket/get_orderbook.ts
@@ -1,6 +1,6 @@
+import type { PolymarketOrderBook } from '@/tools/polymarket/types'
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketOrderBook } from './types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetOrderbookParams {
tokenId: string // The token ID (CLOB token ID from market)
@@ -50,10 +50,22 @@ export const polymarketGetOrderbookTool: ToolConfig<
handlePolymarketError(data, response.status, 'get_orderbook')
}
+ const orderbook: PolymarketOrderBook = {
+ market: data.market ?? '',
+ asset_id: data.asset_id ?? '',
+ hash: data.hash ?? '',
+ timestamp: data.timestamp ?? '',
+ bids: data.bids ?? [],
+ asks: data.asks ?? [],
+ min_order_size: data.min_order_size ?? '0',
+ tick_size: data.tick_size ?? '0',
+ neg_risk: data.neg_risk ?? false,
+ }
+
return {
success: true,
output: {
- orderbook: data,
+ orderbook,
},
}
},
@@ -62,6 +74,37 @@ export const polymarketGetOrderbookTool: ToolConfig<
orderbook: {
type: 'object',
description: 'Order book with bids and asks arrays',
+ properties: {
+ market: { type: 'string', description: 'Market identifier' },
+ asset_id: { type: 'string', description: 'Asset token ID' },
+ hash: { type: 'string', description: 'Order book hash' },
+ timestamp: { type: 'string', description: 'Timestamp' },
+ bids: {
+ type: 'array',
+ description: 'Bid orders',
+ items: {
+ type: 'object',
+ properties: {
+ price: { type: 'string', description: 'Bid price' },
+ size: { type: 'string', description: 'Bid size' },
+ },
+ },
+ },
+ asks: {
+ type: 'array',
+ description: 'Ask orders',
+ items: {
+ type: 'object',
+ properties: {
+ price: { type: 'string', description: 'Ask price' },
+ size: { type: 'string', description: 'Ask size' },
+ },
+ },
+ },
+ min_order_size: { type: 'string', description: 'Minimum order size' },
+ tick_size: { type: 'string', description: 'Tick size' },
+ neg_risk: { type: 'boolean', description: 'Whether negative risk' },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_positions.ts b/apps/sim/tools/polymarket/get_positions.ts
index bbeda1909..cc2d66e7a 100644
--- a/apps/sim/tools/polymarket/get_positions.ts
+++ b/apps/sim/tools/polymarket/get_positions.ts
@@ -1,10 +1,19 @@
+import type { PolymarketPosition } from '@/tools/polymarket/types'
+import { buildDataUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketPosition } from './types'
-import { buildDataUrl, handlePolymarketError } from './types'
export interface PolymarketGetPositionsParams {
- user: string // Wallet address (required)
- market?: string // Optional market filter
+ user: string
+ market?: string
+ eventId?: string
+ sizeThreshold?: string
+ redeemable?: string
+ mergeable?: string
+ sortBy?: string
+ sortDirection?: string
+ title?: string
+ limit?: string
+ offset?: string
}
export interface PolymarketGetPositionsResponse {
@@ -33,7 +42,63 @@ export const polymarketGetPositionsTool: ToolConfig<
market: {
type: 'string',
required: false,
- description: 'Optional market ID to filter positions',
+ description:
+ 'Condition IDs to filter positions (comma-separated, mutually exclusive with eventId)',
+ visibility: 'user-or-llm',
+ },
+ eventId: {
+ type: 'string',
+ required: false,
+ description: 'Event ID to filter positions (mutually exclusive with market)',
+ visibility: 'user-or-llm',
+ },
+ sizeThreshold: {
+ type: 'string',
+ required: false,
+ description: 'Minimum position size threshold (default: 1)',
+ visibility: 'user-or-llm',
+ },
+ redeemable: {
+ type: 'string',
+ required: false,
+ description: 'Filter for redeemable positions only (true/false)',
+ visibility: 'user-or-llm',
+ },
+ mergeable: {
+ type: 'string',
+ required: false,
+ description: 'Filter for mergeable positions only (true/false)',
+ visibility: 'user-or-llm',
+ },
+ sortBy: {
+ type: 'string',
+ required: false,
+ description:
+ 'Sort field (TOKENS, CURRENT, INITIAL, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE, AVGPRICE)',
+ visibility: 'user-or-llm',
+ },
+ sortDirection: {
+ type: 'string',
+ required: false,
+ description: 'Sort direction (ASC or DESC)',
+ visibility: 'user-or-llm',
+ },
+ title: {
+ type: 'string',
+ required: false,
+ description: 'Search filter by title',
+ visibility: 'user-or-llm',
+ },
+ limit: {
+ type: 'string',
+ required: false,
+ description: 'Number of results per page',
+ visibility: 'user-or-llm',
+ },
+ offset: {
+ type: 'string',
+ required: false,
+ description: 'Pagination offset',
visibility: 'user-or-llm',
},
},
@@ -43,6 +108,15 @@ export const polymarketGetPositionsTool: ToolConfig<
const queryParams = new URLSearchParams()
queryParams.append('user', params.user)
if (params.market) queryParams.append('market', params.market)
+ if (params.eventId) queryParams.append('eventId', params.eventId)
+ if (params.sizeThreshold) queryParams.append('sizeThreshold', params.sizeThreshold)
+ if (params.redeemable) queryParams.append('redeemable', params.redeemable)
+ if (params.mergeable) queryParams.append('mergeable', params.mergeable)
+ if (params.sortBy) queryParams.append('sortBy', params.sortBy)
+ if (params.sortDirection) queryParams.append('sortDirection', params.sortDirection)
+ if (params.title) queryParams.append('title', params.title)
+ if (params.limit) queryParams.append('limit', params.limit)
+ if (params.offset) queryParams.append('offset', params.offset)
return `${buildDataUrl('/positions')}?${queryParams.toString()}`
},
@@ -59,8 +133,34 @@ export const polymarketGetPositionsTool: ToolConfig<
handlePolymarketError(data, response.status, 'get_positions')
}
- // Response is an array of positions
- const positions = Array.isArray(data) ? data : []
+ const rawPositions = Array.isArray(data) ? data : []
+ const positions: PolymarketPosition[] = rawPositions.map((p: Record) => ({
+ proxyWallet: (p.proxyWallet as string) ?? null,
+ asset: (p.asset as string) ?? '',
+ conditionId: (p.conditionId as string) ?? '',
+ size: (p.size as number) ?? 0,
+ avgPrice: (p.avgPrice as number) ?? 0,
+ initialValue: (p.initialValue as number) ?? 0,
+ currentValue: (p.currentValue as number) ?? 0,
+ cashPnl: (p.cashPnl as number) ?? 0,
+ percentPnl: (p.percentPnl as number) ?? 0,
+ totalBought: (p.totalBought as number) ?? 0,
+ realizedPnl: (p.realizedPnl as number) ?? 0,
+ percentRealizedPnl: (p.percentRealizedPnl as number) ?? 0,
+ curPrice: (p.curPrice as number) ?? 0,
+ redeemable: (p.redeemable as boolean) ?? false,
+ mergeable: (p.mergeable as boolean) ?? false,
+ title: (p.title as string) ?? null,
+ slug: (p.slug as string) ?? null,
+ icon: (p.icon as string) ?? null,
+ eventSlug: (p.eventSlug as string) ?? null,
+ outcome: (p.outcome as string) ?? null,
+ outcomeIndex: (p.outcomeIndex as number) ?? null,
+ oppositeOutcome: (p.oppositeOutcome as string) ?? null,
+ oppositeAsset: (p.oppositeAsset as string) ?? null,
+ endDate: (p.endDate as string) ?? null,
+ negativeRisk: (p.negativeRisk as boolean) ?? false,
+ }))
return {
success: true,
@@ -74,6 +174,36 @@ export const polymarketGetPositionsTool: ToolConfig<
positions: {
type: 'array',
description: 'Array of position objects',
+ items: {
+ type: 'object',
+ properties: {
+ proxyWallet: { type: 'string', description: 'Proxy wallet address' },
+ asset: { type: 'string', description: 'Asset token ID' },
+ conditionId: { type: 'string', description: 'Condition ID' },
+ size: { type: 'number', description: 'Position size' },
+ avgPrice: { type: 'number', description: 'Average price' },
+ initialValue: { type: 'number', description: 'Initial value' },
+ currentValue: { type: 'number', description: 'Current value' },
+ cashPnl: { type: 'number', description: 'Cash profit/loss' },
+ percentPnl: { type: 'number', description: 'Percent profit/loss' },
+ totalBought: { type: 'number', description: 'Total bought' },
+ realizedPnl: { type: 'number', description: 'Realized profit/loss' },
+ percentRealizedPnl: { type: 'number', description: 'Percent realized profit/loss' },
+ curPrice: { type: 'number', description: 'Current price' },
+ redeemable: { type: 'boolean', description: 'Whether position is redeemable' },
+ mergeable: { type: 'boolean', description: 'Whether position is mergeable' },
+ title: { type: 'string', description: 'Market title' },
+ slug: { type: 'string', description: 'Market slug' },
+ icon: { type: 'string', description: 'Market icon URL' },
+ eventSlug: { type: 'string', description: 'Event slug' },
+ outcome: { type: 'string', description: 'Outcome name' },
+ outcomeIndex: { type: 'number', description: 'Outcome index' },
+ oppositeOutcome: { type: 'string', description: 'Opposite outcome name' },
+ oppositeAsset: { type: 'string', description: 'Opposite asset token ID' },
+ endDate: { type: 'string', description: 'End date' },
+ negativeRisk: { type: 'boolean', description: 'Whether negative risk' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_price.ts b/apps/sim/tools/polymarket/get_price.ts
index 8506c5101..4bae1f67f 100644
--- a/apps/sim/tools/polymarket/get_price.ts
+++ b/apps/sim/tools/polymarket/get_price.ts
@@ -1,5 +1,5 @@
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetPriceParams {
tokenId: string // The token ID (CLOB token ID from market)
diff --git a/apps/sim/tools/polymarket/get_price_history.ts b/apps/sim/tools/polymarket/get_price_history.ts
index e7ed1b330..deb4f362c 100644
--- a/apps/sim/tools/polymarket/get_price_history.ts
+++ b/apps/sim/tools/polymarket/get_price_history.ts
@@ -1,6 +1,6 @@
+import type { PolymarketPriceHistoryEntry } from '@/tools/polymarket/types'
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketPriceHistoryEntry } from './types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetPriceHistoryParams {
tokenId: string
@@ -99,7 +99,14 @@ export const polymarketGetPriceHistoryTool: ToolConfig<
outputs: {
history: {
type: 'array',
- description: 'Array of price history entries with timestamp (t) and price (p)',
+ description: 'Array of price history entries',
+ items: {
+ type: 'object',
+ properties: {
+ t: { type: 'number', description: 'Unix timestamp' },
+ p: { type: 'number', description: 'Price at timestamp' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_series.ts b/apps/sim/tools/polymarket/get_series.ts
index a3d528ecc..c209dfbb5 100644
--- a/apps/sim/tools/polymarket/get_series.ts
+++ b/apps/sim/tools/polymarket/get_series.ts
@@ -1,6 +1,6 @@
+import type { PolymarketPaginationParams, PolymarketSeries } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketPaginationParams, PolymarketSeries } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetSeriesParams extends PolymarketPaginationParams {}
@@ -97,6 +97,26 @@ export const polymarketGetSeriesTool: ToolConfig<
series: {
type: 'array',
description: 'Array of series objects',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Series ID' },
+ ticker: { type: 'string', description: 'Series ticker' },
+ slug: { type: 'string', description: 'Series slug' },
+ title: { type: 'string', description: 'Series title' },
+ seriesType: { type: 'string', description: 'Series type' },
+ recurrence: { type: 'string', description: 'Recurrence pattern' },
+ image: { type: 'string', description: 'Series image URL' },
+ icon: { type: 'string', description: 'Series icon URL' },
+ active: { type: 'boolean', description: 'Whether series is active' },
+ closed: { type: 'boolean', description: 'Whether series is closed' },
+ archived: { type: 'boolean', description: 'Whether series is archived' },
+ featured: { type: 'boolean', description: 'Whether series is featured' },
+ volume: { type: 'number', description: 'Total volume' },
+ liquidity: { type: 'number', description: 'Total liquidity' },
+ eventCount: { type: 'number', description: 'Number of events in series' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_series_by_id.ts b/apps/sim/tools/polymarket/get_series_by_id.ts
index 4904a46f5..790d7ab4e 100644
--- a/apps/sim/tools/polymarket/get_series_by_id.ts
+++ b/apps/sim/tools/polymarket/get_series_by_id.ts
@@ -1,6 +1,6 @@
+import type { PolymarketSeries } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketSeries } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetSeriesByIdParams {
seriesId: string // Series ID (required)
@@ -58,6 +58,25 @@ export const polymarketGetSeriesByIdTool: ToolConfig<
series: {
type: 'object',
description: 'Series object with details',
+ properties: {
+ id: { type: 'string', description: 'Series ID' },
+ ticker: { type: 'string', description: 'Series ticker' },
+ slug: { type: 'string', description: 'Series slug' },
+ title: { type: 'string', description: 'Series title' },
+ seriesType: { type: 'string', description: 'Series type' },
+ recurrence: { type: 'string', description: 'Recurrence pattern' },
+ image: { type: 'string', description: 'Series image URL' },
+ icon: { type: 'string', description: 'Series icon URL' },
+ active: { type: 'boolean', description: 'Whether series is active' },
+ closed: { type: 'boolean', description: 'Whether series is closed' },
+ archived: { type: 'boolean', description: 'Whether series is archived' },
+ featured: { type: 'boolean', description: 'Whether series is featured' },
+ volume: { type: 'number', description: 'Total volume' },
+ liquidity: { type: 'number', description: 'Total liquidity' },
+ commentCount: { type: 'number', description: 'Comment count' },
+ eventCount: { type: 'number', description: 'Number of events in series' },
+ events: { type: 'array', description: 'Array of events in this series' },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_spread.ts b/apps/sim/tools/polymarket/get_spread.ts
index 80c2c5519..0eff8c44c 100644
--- a/apps/sim/tools/polymarket/get_spread.ts
+++ b/apps/sim/tools/polymarket/get_spread.ts
@@ -1,6 +1,6 @@
+import type { PolymarketSpread } from '@/tools/polymarket/types'
+import { buildClobUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketSpread } from './types'
-import { buildClobUrl, handlePolymarketError } from './types'
export interface PolymarketGetSpreadParams {
tokenId: string // The token ID (CLOB token ID from market)
@@ -53,7 +53,9 @@ export const polymarketGetSpreadTool: ToolConfig<
return {
success: true,
output: {
- spread: data,
+ spread: {
+ spread: data.spread ?? '',
+ },
},
}
},
@@ -61,7 +63,10 @@ export const polymarketGetSpreadTool: ToolConfig<
outputs: {
spread: {
type: 'object',
- description: 'Bid-ask spread with bid and ask prices',
+ description: 'Spread value between bid and ask',
+ properties: {
+ spread: { type: 'string', description: 'The spread value' },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/get_tags.ts b/apps/sim/tools/polymarket/get_tags.ts
index 1b492eaba..8dba40c10 100644
--- a/apps/sim/tools/polymarket/get_tags.ts
+++ b/apps/sim/tools/polymarket/get_tags.ts
@@ -1,6 +1,6 @@
+import type { PolymarketPaginationParams, PolymarketTag } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketPaginationParams, PolymarketTag } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetTagsParams extends PolymarketPaginationParams {}
@@ -71,7 +71,17 @@ export const polymarketGetTagsTool: ToolConfig ({
@@ -76,8 +116,28 @@ export const polymarketGetTradesTool: ToolConfig<
handlePolymarketError(data, response.status, 'get_trades')
}
- // Response is an array of trades
- const trades = Array.isArray(data) ? data : []
+ const rawTrades = Array.isArray(data) ? data : []
+ const trades: PolymarketTrade[] = rawTrades.map((t: Record) => ({
+ proxyWallet: (t.proxyWallet as string) ?? null,
+ side: (t.side as string) ?? '',
+ asset: (t.asset as string) ?? '',
+ conditionId: (t.conditionId as string) ?? '',
+ size: (t.size as number) ?? 0,
+ price: (t.price as number) ?? 0,
+ timestamp: (t.timestamp as number) ?? 0,
+ title: (t.title as string) ?? null,
+ slug: (t.slug as string) ?? null,
+ icon: (t.icon as string) ?? null,
+ eventSlug: (t.eventSlug as string) ?? null,
+ outcome: (t.outcome as string) ?? null,
+ outcomeIndex: (t.outcomeIndex as number) ?? null,
+ name: (t.name as string) ?? null,
+ pseudonym: (t.pseudonym as string) ?? null,
+ bio: (t.bio as string) ?? null,
+ profileImage: (t.profileImage as string) ?? null,
+ profileImageOptimized: (t.profileImageOptimized as string) ?? null,
+ transactionHash: (t.transactionHash as string) ?? null,
+ }))
return {
success: true,
@@ -91,6 +151,30 @@ export const polymarketGetTradesTool: ToolConfig<
trades: {
type: 'array',
description: 'Array of trade objects',
+ items: {
+ type: 'object',
+ properties: {
+ proxyWallet: { type: 'string', description: 'Proxy wallet address' },
+ side: { type: 'string', description: 'Trade side (BUY or SELL)' },
+ asset: { type: 'string', description: 'Asset token ID' },
+ conditionId: { type: 'string', description: 'Condition ID' },
+ size: { type: 'number', description: 'Trade size' },
+ price: { type: 'number', description: 'Trade price' },
+ timestamp: { type: 'number', description: 'Unix timestamp' },
+ title: { type: 'string', description: 'Market title' },
+ slug: { type: 'string', description: 'Market slug' },
+ icon: { type: 'string', description: 'Market icon URL' },
+ eventSlug: { type: 'string', description: 'Event slug' },
+ outcome: { type: 'string', description: 'Outcome name' },
+ outcomeIndex: { type: 'number', description: 'Outcome index' },
+ name: { type: 'string', description: 'Trader name' },
+ pseudonym: { type: 'string', description: 'Trader pseudonym' },
+ bio: { type: 'string', description: 'Trader bio' },
+ profileImage: { type: 'string', description: 'Profile image URL' },
+ profileImageOptimized: { type: 'string', description: 'Optimized profile image URL' },
+ transactionHash: { type: 'string', description: 'Transaction hash' },
+ },
+ },
},
},
}
diff --git a/apps/sim/tools/polymarket/index.ts b/apps/sim/tools/polymarket/index.ts
index a52e9ac37..bc85dc348 100644
--- a/apps/sim/tools/polymarket/index.ts
+++ b/apps/sim/tools/polymarket/index.ts
@@ -1,18 +1,20 @@
-export * from './get_event'
-export * from './get_events'
-export * from './get_last_trade_price'
-export * from './get_market'
-export * from './get_markets'
-export * from './get_midpoint'
-export * from './get_orderbook'
-export * from './get_positions'
-export * from './get_price'
-export * from './get_price_history'
-export * from './get_series'
-export * from './get_series_by_id'
-export * from './get_spread'
-export * from './get_tags'
-export * from './get_tick_size'
-export * from './get_trades'
-export * from './search'
-export * from './types'
+export { polymarketGetActivityTool } from './get_activity'
+export { polymarketGetEventTool } from './get_event'
+export { polymarketGetEventsTool } from './get_events'
+export { polymarketGetHoldersTool } from './get_holders'
+export { polymarketGetLastTradePriceTool } from './get_last_trade_price'
+export { polymarketGetLeaderboardTool } from './get_leaderboard'
+export { polymarketGetMarketTool } from './get_market'
+export { polymarketGetMarketsTool } from './get_markets'
+export { polymarketGetMidpointTool } from './get_midpoint'
+export { polymarketGetOrderbookTool } from './get_orderbook'
+export { polymarketGetPositionsTool } from './get_positions'
+export { polymarketGetPriceTool } from './get_price'
+export { polymarketGetPriceHistoryTool } from './get_price_history'
+export { polymarketGetSeriesTool } from './get_series'
+export { polymarketGetSeriesByIdTool } from './get_series_by_id'
+export { polymarketGetSpreadTool } from './get_spread'
+export { polymarketGetTagsTool } from './get_tags'
+export { polymarketGetTickSizeTool } from './get_tick_size'
+export { polymarketGetTradesTool } from './get_trades'
+export { polymarketSearchTool } from './search'
diff --git a/apps/sim/tools/polymarket/search.ts b/apps/sim/tools/polymarket/search.ts
index 87a03ca41..342185824 100644
--- a/apps/sim/tools/polymarket/search.ts
+++ b/apps/sim/tools/polymarket/search.ts
@@ -1,9 +1,22 @@
+import type { PolymarketSearchResult } from '@/tools/polymarket/types'
+import { buildGammaUrl, handlePolymarketError } from '@/tools/polymarket/types'
import type { ToolConfig } from '@/tools/types'
-import type { PolymarketPaginationParams, PolymarketSearchResult } from './types'
-import { buildGammaUrl, handlePolymarketError } from './types'
-export interface PolymarketSearchParams extends PolymarketPaginationParams {
- query: string // Search term (required)
+export interface PolymarketSearchParams {
+ query: string
+ limit?: string
+ page?: string
+ cache?: string
+ eventsStatus?: string
+ limitPerType?: string
+ eventsTag?: string
+ sort?: string
+ ascending?: string
+ searchTags?: string
+ searchProfiles?: string
+ recurrence?: string
+ excludeTagId?: string
+ keepClosedMarkets?: string
}
export interface PolymarketSearchResponse {
@@ -32,10 +45,76 @@ export const polymarketSearchTool: ToolConfig {
const queryParams = new URLSearchParams()
queryParams.append('q', params.query)
- // Default limit to 50 to prevent browser crashes from large data sets
queryParams.append('limit', params.limit || '50')
- if (params.offset) queryParams.append('offset', params.offset)
+ if (params.page) queryParams.append('page', params.page)
+ if (params.cache) queryParams.append('cache', params.cache)
+ if (params.eventsStatus) queryParams.append('events_status', params.eventsStatus)
+ if (params.limitPerType) queryParams.append('limit_per_type', params.limitPerType)
+ if (params.eventsTag) queryParams.append('events_tag', params.eventsTag)
+ if (params.sort) queryParams.append('sort', params.sort)
+ if (params.ascending) queryParams.append('ascending', params.ascending)
+ if (params.searchTags) queryParams.append('search_tags', params.searchTags)
+ if (params.searchProfiles) queryParams.append('search_profiles', params.searchProfiles)
+ if (params.recurrence) queryParams.append('recurrence', params.recurrence)
+ if (params.excludeTagId) queryParams.append('exclude_tag_id', params.excludeTagId)
+ if (params.keepClosedMarkets)
+ queryParams.append('keep_closed_markets', params.keepClosedMarkets)
return `${buildGammaUrl('/public-search')}?${queryParams.toString()}`
},
@@ -63,11 +153,11 @@ export const polymarketSearchTool: ToolConfig = {
jsm_get_approvals: jsmGetApprovalsTool,
jsm_answer_approval: jsmAnswerApprovalTool,
kalshi_get_markets: kalshiGetMarketsTool,
+ kalshi_get_markets_v2: kalshiGetMarketsV2Tool,
kalshi_get_market: kalshiGetMarketTool,
+ kalshi_get_market_v2: kalshiGetMarketV2Tool,
kalshi_get_events: kalshiGetEventsTool,
+ kalshi_get_events_v2: kalshiGetEventsV2Tool,
kalshi_get_event: kalshiGetEventTool,
+ kalshi_get_event_v2: kalshiGetEventV2Tool,
kalshi_get_balance: kalshiGetBalanceTool,
+ kalshi_get_balance_v2: kalshiGetBalanceV2Tool,
kalshi_get_positions: kalshiGetPositionsTool,
+ kalshi_get_positions_v2: kalshiGetPositionsV2Tool,
kalshi_get_orders: kalshiGetOrdersTool,
+ kalshi_get_orders_v2: kalshiGetOrdersV2Tool,
kalshi_get_order: kalshiGetOrderTool,
+ kalshi_get_order_v2: kalshiGetOrderV2Tool,
kalshi_get_orderbook: kalshiGetOrderbookTool,
+ kalshi_get_orderbook_v2: kalshiGetOrderbookV2Tool,
kalshi_get_trades: kalshiGetTradesTool,
+ kalshi_get_trades_v2: kalshiGetTradesV2Tool,
kalshi_get_candlesticks: kalshiGetCandlesticksTool,
+ kalshi_get_candlesticks_v2: kalshiGetCandlesticksV2Tool,
kalshi_get_fills: kalshiGetFillsTool,
+ kalshi_get_fills_v2: kalshiGetFillsV2Tool,
kalshi_get_series_by_ticker: kalshiGetSeriesByTickerTool,
+ kalshi_get_series_by_ticker_v2: kalshiGetSeriesByTickerV2Tool,
kalshi_get_exchange_status: kalshiGetExchangeStatusTool,
+ kalshi_get_exchange_status_v2: kalshiGetExchangeStatusV2Tool,
kalshi_create_order: kalshiCreateOrderTool,
+ kalshi_create_order_v2: kalshiCreateOrderV2Tool,
kalshi_cancel_order: kalshiCancelOrderTool,
+ kalshi_cancel_order_v2: kalshiCancelOrderV2Tool,
kalshi_amend_order: kalshiAmendOrderTool,
+ kalshi_amend_order_v2: kalshiAmendOrderV2Tool,
polymarket_get_markets: polymarketGetMarketsTool,
polymarket_get_market: polymarketGetMarketTool,
polymarket_get_events: polymarketGetEventsTool,
@@ -1848,6 +1900,9 @@ export const tools: Record = {
polymarket_get_tick_size: polymarketGetTickSizeTool,
polymarket_get_positions: polymarketGetPositionsTool,
polymarket_get_trades: polymarketGetTradesTool,
+ polymarket_get_activity: polymarketGetActivityTool,
+ polymarket_get_leaderboard: polymarketGetLeaderboardTool,
+ polymarket_get_holders: polymarketGetHoldersTool,
slack_message: slackMessageTool,
slack_message_reader: slackMessageReaderTool,
slack_list_channels: slackListChannelsTool,
@@ -3086,8 +3141,23 @@ export const tools: Record = {
intercom_create_ticket_v2: intercomCreateTicketV2Tool,
intercom_get_ticket: intercomGetTicketTool,
intercom_get_ticket_v2: intercomGetTicketV2Tool,
+ intercom_update_ticket_v2: intercomUpdateTicketV2Tool,
intercom_create_message: intercomCreateMessageTool,
intercom_create_message_v2: intercomCreateMessageV2Tool,
+ intercom_list_admins_v2: intercomListAdminsV2Tool,
+ intercom_close_conversation_v2: intercomCloseConversationV2Tool,
+ intercom_open_conversation_v2: intercomOpenConversationV2Tool,
+ intercom_snooze_conversation_v2: intercomSnoozeConversationV2Tool,
+ intercom_assign_conversation_v2: intercomAssignConversationV2Tool,
+ intercom_list_tags_v2: intercomListTagsV2Tool,
+ intercom_create_tag_v2: intercomCreateTagV2Tool,
+ intercom_tag_contact_v2: intercomTagContactV2Tool,
+ intercom_untag_contact_v2: intercomUntagContactV2Tool,
+ intercom_tag_conversation_v2: intercomTagConversationV2Tool,
+ intercom_create_note_v2: intercomCreateNoteV2Tool,
+ intercom_create_event_v2: intercomCreateEventV2Tool,
+ intercom_attach_contact_to_company_v2: intercomAttachContactToCompanyV2Tool,
+ intercom_detach_contact_from_company_v2: intercomDetachContactFromCompanyV2Tool,
sentry_issues_list: listIssuesTool,
sentry_issues_get: getIssueTool,
sentry_issues_update: updateIssueTool,
diff --git a/bun.lock b/bun.lock
index f1df7669e..c76b5b453 100644
--- a/bun.lock
+++ b/bun.lock
@@ -291,7 +291,7 @@
},
"packages/ts-sdk": {
"name": "simstudio-ts-sdk",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"node-fetch": "^3.3.2",
},
diff --git a/helm/sim/templates/configmap-branding.yaml b/helm/sim/templates/configmap-branding.yaml
new file mode 100644
index 000000000..4e22d3a2b
--- /dev/null
+++ b/helm/sim/templates/configmap-branding.yaml
@@ -0,0 +1,25 @@
+{{- if .Values.branding.enabled }}
+---
+# Branding ConfigMap
+# Mounts custom branding assets (logos, CSS, etc.) into the application
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "sim.fullname" . }}-branding
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "sim.labels" . | nindent 4 }}
+ app.kubernetes.io/component: branding
+{{- if .Values.branding.files }}
+data:
+ {{- range $key, $value := .Values.branding.files }}
+ {{ $key }}: {{ $value | quote }}
+ {{- end }}
+{{- end }}
+{{- if .Values.branding.binaryFiles }}
+binaryData:
+ {{- range $key, $value := .Values.branding.binaryFiles }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+{{- end }}
+{{- end }}
diff --git a/helm/sim/templates/deployment-app.yaml b/helm/sim/templates/deployment-app.yaml
index 6b1d63289..5362dd43e 100644
--- a/helm/sim/templates/deployment-app.yaml
+++ b/helm/sim/templates/deployment-app.yaml
@@ -110,8 +110,13 @@ spec:
{{- end }}
{{- include "sim.resources" .Values.app | nindent 10 }}
{{- include "sim.securityContext" .Values.app | nindent 10 }}
- {{- if or .Values.extraVolumeMounts .Values.app.extraVolumeMounts }}
+ {{- if or .Values.branding.enabled .Values.extraVolumeMounts .Values.app.extraVolumeMounts }}
volumeMounts:
+ {{- if .Values.branding.enabled }}
+ - name: branding
+ mountPath: {{ .Values.branding.mountPath | default "/app/public/branding" }}
+ readOnly: true
+ {{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -119,8 +124,13 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
- {{- if or .Values.extraVolumes .Values.app.extraVolumes }}
+ {{- if or .Values.branding.enabled .Values.extraVolumes .Values.app.extraVolumes }}
volumes:
+ {{- if .Values.branding.enabled }}
+ - name: branding
+ configMap:
+ name: {{ include "sim.fullname" . }}-branding
+ {{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml
index f0def91cf..dc09a9ce2 100644
--- a/helm/sim/values.yaml
+++ b/helm/sim/values.yaml
@@ -738,6 +738,32 @@ sharedStorage:
extraVolumes: []
extraVolumeMounts: []
+# Branding configuration
+# Use this to inject custom branding assets (logos, CSS, etc.) into the application
+branding:
+ # Enable/disable branding ConfigMap
+ enabled: false
+
+ # Mount path in the container where branding files will be available
+ mountPath: "/app/public/branding"
+
+ # Text files (CSS, JSON, HTML, etc.) - values are plain text
+ # Example:
+ # files:
+ # custom.css: |
+ # .logo { background-color: #ff0000; }
+ # config.json: |
+ # {"theme": "dark"}
+ files: {}
+
+ # Binary files (PNG, JPG, ICO, etc.) - values must be base64 encoded
+ # Generate base64 with: base64 -i logo.png | tr -d '\n'
+ # Example:
+ # binaryFiles:
+ # logo.png: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk..."
+ # favicon.ico: "AAABAAEAEBAAAAEAIABoBAAAFgAAAA..."
+ binaryFiles: {}
+
# Additional environment variables for custom integrations
extraEnvVars: []
diff --git a/scripts/create-single-release.ts b/scripts/create-single-release.ts
index b49777cec..bb6f79df1 100755
--- a/scripts/create-single-release.ts
+++ b/scripts/create-single-release.ts
@@ -197,7 +197,7 @@ async function getCommitsBetweenVersions(
const commitEntries = gitLog.split('\n').filter((line) => line.trim())
const nonVersionCommits = commitEntries.filter((line) => {
- const [hash, message] = line.split('|')
+ const [, message] = line.split('|')
const isVersionCommit = message.match(/^v\d+\.\d+/)
if (isVersionCommit) {
console.log(`⏭️ Skipping version commit: ${message.substring(0, 50)}...`)
@@ -369,6 +369,25 @@ async function main() {
console.log(`ℹ️ No previous version found (this might be the first release)`)
}
+ try {
+ const existingRelease = await octokit.rest.repos.getReleaseByTag({
+ owner: REPO_OWNER,
+ repo: REPO_NAME,
+ tag: targetVersion,
+ })
+ if (existingRelease.data) {
+ console.log(`ℹ️ Release ${targetVersion} already exists, skipping creation`)
+ console.log(
+ `🔗 View release: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/tag/${targetVersion}`
+ )
+ return
+ }
+ } catch (error: any) {
+ if (error.status !== 404) {
+ throw error
+ }
+ }
+
const releaseBody = await generateReleaseBody(versionCommit, previousCommit || undefined)
console.log(`🚀 Creating GitHub release for ${targetVersion}...`)