diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
index fd601b7ea..1468d750a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
@@ -1412,10 +1412,13 @@ function RunSkipButtons({
setIsProcessing(true)
setButtonsHidden(true)
try {
- // Add to auto-allowed list first
+ // Add to auto-allowed list - this also executes all pending integration tools of this type
await addAutoAllowedTool(toolCall.name)
- // Then execute
- await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
+ // For client tools with interrupts (not integration tools), we still need to call handleRun
+ // since executeIntegrationTool only works for server-side tools
+ if (!isIntegrationTool(toolCall.name)) {
+ await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
+ }
} finally {
setIsProcessing(false)
actionInProgressRef.current = false
@@ -1438,10 +1441,10 @@ function RunSkipButtons({
if (buttonsHidden) return null
- // Hide "Always Allow" for integration tools (only show for client tools with interrupts)
- const showAlwaysAllow = !isIntegrationTool(toolCall.name)
+ // Show "Always Allow" for all tools that require confirmation
+ const showAlwaysAllow = true
- // Standardized buttons for all interrupt tools: Allow, (Always Allow for client tools only), Skip
+ // Standardized buttons for all interrupt tools: Allow, Always Allow, Skip
return (
diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
index 60136d1a8..48e5f74d5 100644
--- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
+++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
@@ -2468,16 +2468,17 @@ async function validateWorkflowSelectorIds(
const result = await validateSelectorIds(selector.selectorType, selector.value, context)
if (result.invalid.length > 0) {
+ // Include warning info (like available credentials) in the error message for better LLM feedback
+ const warningInfo = result.warning ? `. ${result.warning}` : ''
errors.push({
blockId: selector.blockId,
blockType: selector.blockType,
field: selector.fieldName,
value: selector.value,
- error: `Invalid ${selector.selectorType} ID(s): ${result.invalid.join(', ')} - ID(s) do not exist`,
+ error: `Invalid ${selector.selectorType} ID(s): ${result.invalid.join(', ')} - ID(s) do not exist or user doesn't have access${warningInfo}`,
})
- }
-
- if (result.warning) {
+ } else if (result.warning) {
+ // Log warnings that don't have errors (shouldn't happen for credentials but may for other selectors)
logger.warn(result.warning, {
blockId: selector.blockId,
fieldName: selector.fieldName,
diff --git a/apps/sim/lib/copilot/validation/selector-validator.ts b/apps/sim/lib/copilot/validation/selector-validator.ts
index 98466e71d..13ef8f416 100644
--- a/apps/sim/lib/copilot/validation/selector-validator.ts
+++ b/apps/sim/lib/copilot/validation/selector-validator.ts
@@ -39,6 +39,31 @@ export async function validateSelectorIds(
.from(account)
.where(and(inArray(account.id, idsArray), eq(account.userId, context.userId)))
existingIds = results.map((r) => r.id)
+
+ // If any IDs are invalid, fetch user's available credentials to include in error message
+ const existingSet = new Set(existingIds)
+ const invalidIds = idsArray.filter((id) => !existingSet.has(id))
+ if (invalidIds.length > 0) {
+ // Fetch all of the user's credentials to provide helpful feedback
+ const allUserCredentials = await db
+ .select({ id: account.id, providerId: account.providerId })
+ .from(account)
+ .where(eq(account.userId, context.userId))
+
+ const availableCredentials = allUserCredentials
+ .map((c) => `${c.id} (${c.providerId})`)
+ .join(', ')
+ const noCredentialsMessage = 'User has no credentials configured.'
+
+ return {
+ valid: existingIds,
+ invalid: invalidIds,
+ warning:
+ allUserCredentials.length > 0
+ ? `Available credentials for this user: ${availableCredentials}`
+ : noCredentialsMessage,
+ }
+ }
break
}
diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts
index 697265df8..cab752a63 100644
--- a/apps/sim/stores/panel/copilot/store.ts
+++ b/apps/sim/stores/panel/copilot/store.ts
@@ -1320,7 +1320,16 @@ const sseHandlers: Record = {
typeof def.hasInterrupt === 'function'
? !!def.hasInterrupt(args || {})
: !!def.hasInterrupt
- if (!hasInterrupt && typeof def.execute === 'function') {
+ // Check if tool is auto-allowed - if so, execute even if it has an interrupt
+ const { autoAllowedTools } = get()
+ const isAutoAllowed = name ? autoAllowedTools.includes(name) : false
+ if ((!hasInterrupt || isAutoAllowed) && typeof def.execute === 'function') {
+ if (isAutoAllowed && hasInterrupt) {
+ logger.info('[toolCallsById] Auto-executing tool with interrupt (auto-allowed)', {
+ id,
+ name,
+ })
+ }
const ctx = createExecutionContext({ toolCallId: id, toolName: name || 'unknown_tool' })
// Defer executing transition by a tick to let pending render
setTimeout(() => {
@@ -1426,11 +1435,20 @@ const sseHandlers: Record = {
logger.warn('tool_call registry auto-exec check failed', { id, name, error: e })
}
- // Class-based auto-exec for non-interrupt tools
+ // Class-based auto-exec for non-interrupt tools or auto-allowed tools
try {
const inst = getClientTool(id) as any
const hasInterrupt = !!inst?.getInterruptDisplays?.()
- if (!hasInterrupt && typeof inst?.execute === 'function') {
+ // Check if tool is auto-allowed - if so, execute even if it has an interrupt
+ const { autoAllowedTools: classAutoAllowed } = get()
+ const isClassAutoAllowed = name ? classAutoAllowed.includes(name) : false
+ if ((!hasInterrupt || isClassAutoAllowed) && (typeof inst?.execute === 'function' || typeof inst?.handleAccept === 'function')) {
+ if (isClassAutoAllowed && hasInterrupt) {
+ logger.info('[toolCallsById] Auto-executing class tool with interrupt (auto-allowed)', {
+ id,
+ name,
+ })
+ }
setTimeout(() => {
// Guard against duplicate execution - check if already executing or terminal
const currentState = get().toolCallsById[id]?.state
@@ -1449,7 +1467,12 @@ const sseHandlers: Record = {
Promise.resolve()
.then(async () => {
- await inst.execute(args || {})
+ // Use handleAccept for tools with interrupts, execute for others
+ if (hasInterrupt && typeof inst?.handleAccept === 'function') {
+ await inst.handleAccept(args || {})
+ } else {
+ await inst.execute(args || {})
+ }
// Success/error will be synced via registerToolStateSync
})
.catch(() => {
@@ -1474,20 +1497,35 @@ const sseHandlers: Record = {
}
} catch {}
- // Integration tools: Stay in pending state until user confirms via buttons
+ // Integration tools: Check auto-allowed or stay in pending state until user confirms
// This handles tools like google_calendar_*, exa_*, gmail_read, etc. that aren't in the client registry
// Only relevant if mode is 'build' (agent)
- const { mode, workflowId } = get()
+ const { mode, workflowId, autoAllowedTools, executeIntegrationTool } = get()
if (mode === 'build' && workflowId) {
// Check if tool was NOT found in client registry
const def = name ? getTool(name) : undefined
const inst = getClientTool(id) as any
if (!def && !inst && name) {
- // Integration tools stay in pending state until user confirms
- logger.info('[build mode] Integration tool awaiting user confirmation', {
- id,
- name,
- })
+ // Check if this integration tool is auto-allowed - if so, execute it immediately
+ if (autoAllowedTools.includes(name)) {
+ logger.info('[build mode] Auto-executing integration tool (auto-allowed)', { id, name })
+ // Defer to allow pending state to render briefly
+ setTimeout(() => {
+ executeIntegrationTool(id).catch((err) => {
+ logger.error('[build mode] Auto-execute integration tool failed', {
+ id,
+ name,
+ error: err,
+ })
+ })
+ }, 0)
+ } else {
+ // Integration tools stay in pending state until user confirms
+ logger.info('[build mode] Integration tool awaiting user confirmation', {
+ id,
+ name,
+ })
+ }
}
}
},
@@ -1976,6 +2014,10 @@ const subAgentSSEHandlers: Record = {
}
// Execute client tools in parallel (non-blocking) - same pattern as main tool_call handler
+ // Check if tool is auto-allowed
+ const { autoAllowedTools: subAgentAutoAllowed } = get()
+ const isSubAgentAutoAllowed = name ? subAgentAutoAllowed.includes(name) : false
+
try {
const def = getTool(name)
if (def) {
@@ -1983,8 +2025,15 @@ const subAgentSSEHandlers: Record = {
typeof def.hasInterrupt === 'function'
? !!def.hasInterrupt(args || {})
: !!def.hasInterrupt
- if (!hasInterrupt) {
- // Auto-execute tools without interrupts - non-blocking
+ // Auto-execute if no interrupt OR if auto-allowed
+ if (!hasInterrupt || isSubAgentAutoAllowed) {
+ if (isSubAgentAutoAllowed && hasInterrupt) {
+ logger.info('[SubAgent] Auto-executing tool with interrupt (auto-allowed)', {
+ id,
+ name,
+ })
+ }
+ // Auto-execute tools - non-blocking
const ctx = createExecutionContext({ toolCallId: id, toolName: name })
Promise.resolve()
.then(() => def.execute(ctx, args || {}))
@@ -2001,9 +2050,22 @@ const subAgentSSEHandlers: Record = {
const instance = getClientTool(id)
if (instance) {
const hasInterruptDisplays = !!instance.getInterruptDisplays?.()
- if (!hasInterruptDisplays) {
+ // Auto-execute if no interrupt OR if auto-allowed
+ if (!hasInterruptDisplays || isSubAgentAutoAllowed) {
+ if (isSubAgentAutoAllowed && hasInterruptDisplays) {
+ logger.info('[SubAgent] Auto-executing class tool with interrupt (auto-allowed)', {
+ id,
+ name,
+ })
+ }
Promise.resolve()
- .then(() => instance.execute(args || {}))
+ .then(() => {
+ // Use handleAccept for tools with interrupts, execute for others
+ if (hasInterruptDisplays && typeof instance.handleAccept === 'function') {
+ return instance.handleAccept(args || {})
+ }
+ return instance.execute(args || {})
+ })
.catch((execErr: any) => {
logger.error('[SubAgent] Class tool execution failed', {
id,
@@ -3676,6 +3738,19 @@ export const useCopilotStore = create()(
const { id, name, params } = toolCall
+ // Guard against double execution - skip if already executing or in terminal state
+ if (
+ toolCall.state === ClientToolCallState.executing ||
+ isTerminalState(toolCall.state)
+ ) {
+ logger.info('[executeIntegrationTool] Skipping - already executing or terminal', {
+ id,
+ name,
+ state: toolCall.state,
+ })
+ return
+ }
+
// Set to executing state
const executingMap = { ...get().toolCallsById }
executingMap[id] = {
@@ -3824,6 +3899,46 @@ export const useCopilotStore = create()(
const data = await res.json()
set({ autoAllowedTools: data.autoAllowedTools || [] })
logger.info('[AutoAllowedTools] Added tool', { toolId })
+
+ // Auto-execute all pending tools of the same type
+ const { toolCallsById, executeIntegrationTool } = get()
+ const pendingToolCalls = Object.values(toolCallsById).filter(
+ (tc) => tc.name === toolId && tc.state === ClientToolCallState.pending
+ )
+ if (pendingToolCalls.length > 0) {
+ const isIntegrationTool = !CLASS_TOOL_METADATA[toolId]
+ logger.info('[AutoAllowedTools] Auto-executing pending tools', {
+ toolId,
+ count: pendingToolCalls.length,
+ isIntegrationTool,
+ })
+ for (const tc of pendingToolCalls) {
+ if (isIntegrationTool) {
+ // Integration tools use executeIntegrationTool
+ executeIntegrationTool(tc.id).catch((err) => {
+ logger.error('[AutoAllowedTools] Auto-execute pending integration tool failed', {
+ toolCallId: tc.id,
+ toolId,
+ error: err,
+ })
+ })
+ } else {
+ // Client tools with interrupts use handleAccept
+ const inst = getClientTool(tc.id) as any
+ if (inst && typeof inst.handleAccept === 'function') {
+ Promise.resolve()
+ .then(() => inst.handleAccept(tc.params || {}))
+ .catch((err: any) => {
+ logger.error('[AutoAllowedTools] Auto-execute pending client tool failed', {
+ toolCallId: tc.id,
+ toolId,
+ error: err,
+ })
+ })
+ }
+ }
+ }
+ }
}
} catch (err) {
logger.error('[AutoAllowedTools] Failed to add tool', { toolId, error: err })