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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 6 additions & 35 deletions apps/sim/lib/custom-tools/operations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { customTools } from '@sim/db/schema'
import { and, desc, eq, isNull } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'

Expand All @@ -23,35 +24,18 @@ export async function upsertCustomTools(params: {
}) {
const { tools, workspaceId, userId, requestId = generateRequestId() } = params

// Use a transaction for multi-step database operations
return await db.transaction(async (tx) => {
// Process each tool: either update existing or create new
for (const tool of tools) {
const nowTime = new Date()

if (tool.id) {
// First, check if tool exists in the workspace
const existingWorkspaceTool = await tx
.select()
.from(customTools)
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
.limit(1)

if (existingWorkspaceTool.length > 0) {
// Tool exists in workspace
const newFunctionName = tool.schema?.function?.name
if (!newFunctionName) {
throw new Error('Tool schema must include a function name')
}

// Check if function name has changed
if (tool.id !== newFunctionName) {
throw new Error(
`Cannot change function name from "${tool.id}" to "${newFunctionName}". Please create a new tool instead.`
)
}

// Update existing workspace tool
await tx
.update(customTools)
.set({
Expand All @@ -64,7 +48,6 @@ export async function upsertCustomTools(params: {
continue
}

// Check if this is a legacy tool (no workspaceId, belongs to user)
const existingLegacyTool = await tx
.select()
.from(customTools)
Expand All @@ -78,7 +61,6 @@ export async function upsertCustomTools(params: {
.limit(1)

if (existingLegacyTool.length > 0) {
// Legacy tool found - update it without migrating to workspace
await tx
.update(customTools)
.set({
Expand All @@ -94,28 +76,18 @@ export async function upsertCustomTools(params: {
}
}

// Creating new tool - use function name as ID for consistency
const functionName = tool.schema?.function?.name
if (!functionName) {
throw new Error('Tool schema must include a function name')
}

// Check for duplicate function names in workspace
const duplicateFunction = await tx
const duplicateTitle = await tx
.select()
.from(customTools)
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.id, functionName)))
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.title, tool.title)))
.limit(1)

if (duplicateFunction.length > 0) {
throw new Error(
`A tool with the function name "${functionName}" already exists in this workspace`
)
if (duplicateTitle.length > 0) {
throw new Error(`A tool with the title "${tool.title}" already exists in this workspace`)
}

// Create new tool using function name as ID
await tx.insert(customTools).values({
id: functionName,
id: nanoid(),
workspaceId,
userId,
title: tool.title,
Expand All @@ -126,7 +98,6 @@ export async function upsertCustomTools(params: {
})
}

// Fetch and return the created/updated tools
const resultTools = await tx
.select()
.from(customTools)
Expand Down
13 changes: 2 additions & 11 deletions apps/sim/lib/workflows/custom-tools-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
try {
const blockData = block as any

// Only process agent blocks
if (!blockData || blockData.type !== 'agent') {
continue
}
Expand All @@ -47,7 +46,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT

let tools = toolsSubBlock.value

// Parse if it's a string
if (typeof tools === 'string') {
try {
tools = JSON.parse(tools)
Expand All @@ -61,7 +59,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
continue
}

// Extract custom tools
for (const tool of tools) {
if (
tool &&
Expand All @@ -71,10 +68,8 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
tool.schema?.function &&
tool.code
) {
// Use toolId if available, otherwise generate one from title
const toolKey = tool.toolId || tool.title

// Deduplicate by toolKey (if same tool appears in multiple blocks)
if (!customToolsMap.has(toolKey)) {
customToolsMap.set(toolKey, tool as CustomTool)
}
Expand All @@ -101,8 +96,6 @@ export async function persistCustomToolsToDatabase(
return { saved: 0, errors: [] }
}

// Only persist if workspaceId is provided (new workspace-scoped tools)
// Skip persistence for existing user-scoped tools to maintain backward compatibility
if (!workspaceId) {
logger.debug('Skipping custom tools persistence - no workspaceId provided (user-scoped tools)')
return { saved: 0, errors: [] }
Expand All @@ -111,7 +104,6 @@ export async function persistCustomToolsToDatabase(
const errors: string[] = []
let saved = 0

// Filter out tools without function names
const validTools = customToolsList.filter((tool) => {
if (!tool.schema?.function?.name) {
logger.warn(`Skipping custom tool without function name: ${tool.title}`)
Expand All @@ -125,10 +117,9 @@ export async function persistCustomToolsToDatabase(
}

try {
// Call the upsert function from lib
await upsertCustomTools({
tools: validTools.map((tool) => ({
id: tool.schema.function.name, // Use function name as ID for updates
id: tool.toolId,
title: tool.title,
schema: tool.schema,
code: tool.code,
Expand All @@ -149,7 +140,7 @@ export async function persistCustomToolsToDatabase(
}

/**
* Extract and persist custom tools from workflow state in one operation
* Extract and persist custom tools from workflow state
*/
export async function extractAndPersistCustomTools(
workflowState: any,
Expand Down
10 changes: 10 additions & 0 deletions packages/db/migrations/0113_calm_tiger_shark.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Step 1: Convert non-UUID IDs to UUIDs (preserve existing UUIDs)
-- This allows same title in different workspaces by removing function-name-based IDs
UPDATE "custom_tools"
SET "id" = gen_random_uuid()::text
WHERE workspace_id IS NOT NULL -- Only update workspace-scoped tools
AND "id" !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; -- Skip if already UUID

-- Step 2: Add composite unique constraint on (workspace_id, title)
-- This enforces uniqueness per workspace, not globally
CREATE UNIQUE INDEX "custom_tools_workspace_title_unique" ON "custom_tools" USING btree ("workspace_id","title");
Loading