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
52 changes: 41 additions & 11 deletions apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -836,17 +836,18 @@ export function DiscordIcon(props: SVGProps<SVGSVGElement>) {

export function LinkedInIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} height='72' viewBox='0 0 72 72' width='72' xmlns='http://www.w3.org/2000/svg'>
<g fill='none' fillRule='evenodd'>
<path
d='M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z'
fill='#0072B1'
/>
<path
d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z'
fill='#FFF'
/>
</g>
<svg
{...props}
fill='currentColor'
height='72'
viewBox='0 0 72 72'
width='72'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z'
fill='currentColor'
/>
</svg>
)
}
Expand Down Expand Up @@ -3833,3 +3834,32 @@ export function ApifyIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}

interface StatusDotIconProps extends SVGProps<SVGSVGElement> {
status: 'operational' | 'degraded' | 'outage' | 'maintenance' | 'loading' | 'error'
}

export function StatusDotIcon({ status, className, ...props }: StatusDotIconProps) {
const colors = {
operational: '#10B981',
degraded: '#F59E0B',
outage: '#EF4444',
maintenance: '#3B82F6',
loading: '#9CA3AF',
error: '#9CA3AF',
}

return (
<svg
xmlns='http://www.w3.org/2000/svg'
width={6}
height={6}
viewBox='0 0 6 6'
fill='none'
className={className}
{...props}
>
<circle cx={3} cy={3} r={3} fill={colors[status]} />
</svg>
)
}
2 changes: 2 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
ResendIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
Expand Down Expand Up @@ -128,6 +129,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
serper: SerperIcon,
sentry: SentryIcon,
sendgrid: SendgridIcon,
search: SearchIcon,
salesforce: SalesforceIcon,
s3: S3Icon,
resend: ResendIcon,
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/en/tools/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"resend",
"s3",
"salesforce",
"search",
"sendgrid",
"sentry",
"serper",
Expand Down
59 changes: 59 additions & 0 deletions apps/docs/content/docs/en/tools/search.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Search
description: Search the web ($0.01 per search)
---

import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="search"
color="#3B82F6"
/>

{/* MANUAL-CONTENT-START:intro */}
The **Search** tool lets you search the web from within your Sim workflows using state-of-the-art search engines. Use it to pull in the latest information, news, facts, and web content directly into your agents, automations, or conversations.

- **General web search**: Find up-to-date information from the internet to supplement your workflows.
- **Automated queries**: Let agents or program logic submit search queries and handle the results automatically.
- **Structured results**: Returns the most relevant web results, including title, link, snippet, and date for each result.

> **Note:** Each search costs **$0.01** per query.

This tool is ideal for any workflow where your agents need access to live web data or must reference current events, perform research, or fetch supplemental content.
{/* MANUAL-CONTENT-END */}


## Usage Instructions

Search the web using the Search tool. Each search costs $0.01 per query.



## Tools

### `search_tool`

Search the web. Returns the most relevant web results, including title, link, snippet, and date for each result.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | The search query |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `query` | string | The search query |
| `totalResults` | number | Total number of results |
| `source` | string | Search source \(exa\) |
| `cost` | json | Cost information \($0.01\) |



## Notes

- Category: `tools`
- Type: `search`
130 changes: 130 additions & 0 deletions apps/sim/app/api/tools/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { executeTool } from '@/tools'

const logger = createLogger('search')

const SearchRequestSchema = z.object({
query: z.string().min(1),
})

export const maxDuration = 60
export const dynamic = 'force-dynamic'

export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID()

try {
const { searchParams: urlParams } = new URL(request.url)
const workflowId = urlParams.get('workflowId') || undefined

const authResult = await checkHybridAuth(request, { requireWorkflowId: false })

if (!authResult.success || !authResult.userId) {
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'
const statusCode = workflowId ? 404 : 401
return NextResponse.json({ success: false, error: errorMessage }, { status: statusCode })
}

const userId = authResult.userId

logger.info(`[${requestId}] Authenticated search request via ${authResult.authType}`, {
userId,
})

const body = await request.json()
const validated = SearchRequestSchema.parse(body)

if (!env.EXA_API_KEY) {
logger.error(`[${requestId}] EXA_API_KEY not configured`)
return NextResponse.json(
{ success: false, error: 'Search service not configured' },
{ status: 503 }
)
}

logger.info(`[${requestId}] Executing search`, {
userId,
query: validated.query,
})

const result = await executeTool('exa_search', {
query: validated.query,
type: 'auto',
useAutoprompt: true,
text: true,
apiKey: env.EXA_API_KEY,
})

if (!result.success) {
logger.error(`[${requestId}] Search failed`, {
userId,
error: result.error,
})
return NextResponse.json(
{
success: false,
error: result.error || 'Search failed',
},
{ status: 500 }
)
}

const results = (result.output.results || []).map((r: any, index: number) => ({
title: r.title || '',
link: r.url || '',
snippet: r.text || '',
date: r.publishedDate || undefined,
position: index + 1,
}))

const cost = {
input: 0,
output: 0,
total: SEARCH_TOOL_COST,
tokens: {
prompt: 0,
completion: 0,
total: 0,
},
model: 'search-exa',
pricing: {
input: 0,
cachedInput: 0,
output: 0,
updatedAt: new Date().toISOString(),
},
}

logger.info(`[${requestId}] Search completed`, {
userId,
resultCount: results.length,
cost: cost.total,
})

return NextResponse.json({
results,
query: validated.query,
totalResults: results.length,
source: 'exa',
cost,
})
} catch (error: any) {
logger.error(`[${requestId}] Search failed`, {
error: error.message,
stack: error.stack,
})

return NextResponse.json(
{
success: false,
error: error.message || 'Search failed',
},
{ status: 500 }
)
}
}
Loading