
Yelp Search
- 148 installs
- 134 repo stars
- Updated July 3, 2026
- letta-ai/skills
Let agents query Yelp for businesses, ratings, and reviews to power local discovery, trip planning, lead research, and conversational recommendation experiences.
About
Integrates Yelp business search into Letta agents: authenticate, query by location and category, normalize ratings and reviews, and return structured results for local discovery and recommendation flows.
- Yelp Fusion search patterns
- Location and category filters
- Review and rating normalization
- Rate-limit aware tool design
- Structured outputs for agents
Yelp Search by the numbers
- 148 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #2,545 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/letta-ai/skills --skill yelp-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 148 |
|---|---|
| repo stars | ★ 134 |
| Last updated | July 3, 2026 |
| Repository | letta-ai/skills ↗ |
What it does
Let agents query Yelp for businesses, ratings, and reviews to power local discovery, trip planning, lead research, and conversational recommendation experiences.
Files
Yelp Search Integration
Search for local businesses on Yelp to find services, get contact information, check ratings, and retrieve hours of operation.
Setup
1. Yelp API Key (Required)
1. Go to https://www.yelp.com/developers 2. Create an account or sign in 3. Click "Create App" and fill out the form 4. Copy your API Key
Add to your .env file:
YELP_API_KEY=your_api_key_here2. Browser-Use for Reviews (Optional)
Only needed if you want to extract review text (slow, ~30-60s per request).
Install dependencies:
uv add browser-use playwright langchain-openai
uv run playwright install chromiumAdd to `.env`:
OPENAI_API_KEY=your_openai_key_hereNote: Review extraction uses browser-use to search DuckDuckGo (since Yelp blocks direct scraping). For most use cases, the rating + review_count from the API is sufficient.
Scripts
All scripts are in tools/yelp-search/scripts/ and should be run with uv run python.
search.py - Find Businesses (Primary Tool)
uv run python tools/yelp-search/scripts/search.py "search term" --location "City, State"Options:
| Flag | Description | Example |
|---|---|---|
--location, -l | City, address, or zip | "San Francisco" or "94123" |
--latitude/--longitude | GPS coordinates | --latitude 37.78 --longitude -122.41 |
--limit, -n | Number of results (default: 5) | -n 10 |
--sort-by | Sort order | rating, distance, review_count, best_match |
--price | Price filter (1-4) | --price 1,2 for $ and $$ only |
--json | Output raw JSON |
Examples:
# Find top-rated dog groomers
uv run python tools/yelp-search/scripts/search.py "dog groomer" -l "San Francisco" --sort-by rating
# Find cheap restaurants nearby
uv run python tools/yelp-search/scripts/search.py "restaurants" -l "94123" --price 1,2 --sort-by distance
# Search near a specific address
uv run python tools/yelp-search/scripts/search.py "laundry pickup" -l "123 Main St, San Francisco"details.py - Get Business Hours & Info
uv run python tools/yelp-search/scripts/details.py "business-alias"The business alias is in the Yelp URL (e.g., the-laundry-corner-san-francisco).
phone_search.py - Reverse Lookup
uv run python tools/yelp-search/scripts/phone_search.py "+14155551234"get_reviews.py - Extract Review Text (Slow)
uv run python tools/yelp-search/scripts/get_reviews.py "Business Name" -l "City" -n 3Note: Uses browser-use which is slow (~30-60s). Yelp blocks direct scraping, so it searches DuckDuckGo for cached reviews as a workaround.
scrape_reviews.py - Direct Yelp Scraping (Alternative)
uv run python tools/yelp-search/scripts/scrape_reviews.py "https://www.yelp.com/biz/business-alias" -n 5Requires Browserbase credentials:
BROWSERBASE_API_KEY=your_key_here
BROWSERBASE_PROJECT_ID=your_project_idNote: Uses Browserbase with proxies to bypass Yelp's CAPTCHA. More reliable than get_reviews.py but requires a Browserbase account.
Best Practices
Evaluating Quality Without Review Text
The API provides rating + review_count which is usually sufficient:
| Rating | Review Count | Interpretation |
|---|---|---|
| 4.5+ | 50+ | Excellent, reliable data |
| 4.5+ | <20 | Promising but limited data |
| 4.0-4.4 | 100+ | Good, well-established |
| <4.0 | any | Proceed with caution |
Finding Services with Specific Needs
When looking for services with specific requirements (weekend hours, pickup/delivery, etc.):
1. Search with --sort-by rating to get best options 2. Get details on top candidates to check hours 3. Filter for businesses open when you need them 4. Contact directly to confirm specific services (pickup, delivery, etc.) since Yelp doesn't always have this info
Search Tips
- Use specific terms:
"laundry pickup"not just"laundry" - Search near an address for accurate distance:
-l "123 Main St, City" - Sort by
ratingfirst, then checkdistanceon results - Check
review_count- high ratings with few reviews may be unreliable
Response Data
Each business result includes:
- name - Business name
- phone - Phone number (use for texting/calling)
- rating - Yelp rating (1-5 stars)
- review_count - Number of reviews
- price - Price level ($ to $$$$)
- location - Full address
- hours - Operating hours by day (in details)
- distance - Distance from search location
- categories - Business categories
- is_open_now - Current open/closed status
Limitations
| Feature | Status | Notes |
|---|---|---|
| Business search | ✅ Works | Fast, reliable |
| Business details | ✅ Works | Includes hours |
| Phone lookup | ✅ Works | Reverse search |
| Review text (API) | ❌ Paid only | Requires enterprise tier |
| Review text (scraping) | ⚠️ Slow | browser-use workaround via DuckDuckGo |
- Free API tier: 500 calls/day
- Results limited to 50 per request
- Some business info (pickup/delivery) not in API - contact directly
MIT License
Copyright (c) 2026 Letta, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#!/usr/bin/env python3
"""Get detailed information about a Yelp business."""
import argparse
import json
import os
import sys
import urllib.request
def load_api_key():
"""Load API key from .env file."""
env_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')
env_path = os.path.abspath(env_path)
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
if line.startswith('YELP_API_KEY='):
return line.strip().split('=', 1)[1]
return os.environ.get('YELP_API_KEY')
def get_business_details(business_id):
"""Get detailed business information."""
api_key = load_api_key()
if not api_key:
print("Error: YELP_API_KEY not found", file=sys.stderr)
sys.exit(1)
url = f"https://api.yelp.com/v3/businesses/{business_id}"
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {api_key}')
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
except urllib.error.HTTPError as e:
print(f"API Error: {e.code} - {e.read().decode()}", file=sys.stderr)
sys.exit(1)
def format_hours(hours_data):
"""Format business hours."""
if not hours_data:
return []
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
hours_list = hours_data[0].get('open', [])
# Group by day
day_hours = {i: [] for i in range(7)}
for entry in hours_list:
day_hours[entry['day']].append(entry)
formatted = []
for day_num, entries in day_hours.items():
if entries:
times = []
for e in entries:
start = f"{e['start'][:2]}:{e['start'][2:]}"
end = f"{e['end'][:2]}:{e['end'][2:]}"
times.append(f"{start}-{end}")
formatted.append(f"{days[day_num]}: {', '.join(times)}")
else:
formatted.append(f"{days[day_num]}: Closed")
return formatted
def main():
parser = argparse.ArgumentParser(description='Get Yelp business details')
parser.add_argument('business_id', help='Business ID or alias')
parser.add_argument('--json', action='store_true', help='Output raw JSON')
args = parser.parse_args()
data = get_business_details(args.business_id)
if args.json:
print(json.dumps(data, indent=2))
return
print(f"=== {data.get('name', 'Unknown')} ===\n")
print(f"⭐ Rating: {data.get('rating', 'N/A')} ({data.get('review_count', 0)} reviews)")
print(f"💰 Price: {data.get('price', 'N/A')}")
print(f"📞 Phone: {data.get('display_phone', data.get('phone', 'N/A'))}")
location = data.get('location', {})
address = '\n '.join(location.get('display_address', ['N/A']))
print(f"📍 Address: {address}")
if location.get('cross_streets'):
print(f" Cross streets: {location['cross_streets']}")
categories = ', '.join([c['title'] for c in data.get('categories', [])])
print(f"📂 Categories: {categories}")
hours = data.get('hours', [])
if hours:
is_open = hours[0].get('is_open_now', False)
print(f"\n🕐 Currently: {'Open' if is_open else 'Closed'}")
print("\nHours:")
for line in format_hours(hours):
print(f" {line}")
print(f"\n🔗 Yelp URL: {data.get('url', 'N/A')}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Fetch Yelp reviews using browser automation."""
import asyncio
import argparse
import os
import sys
# Set up environment for browser-use
os.environ.setdefault("ANONYMIZED_TELEMETRY", "false")
async def get_reviews(business_name: str, location: str, num_reviews: int = 5):
"""Get reviews for a business using browser automation."""
from browser_use import Agent, ChatOpenAI
# Check for API key
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
# Try loading from .env
env_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
if line.startswith('OPENAI_API_KEY='):
api_key = line.strip().split('=', 1)[1]
os.environ["OPENAI_API_KEY"] = api_key
break
if not api_key:
print("Error: OPENAI_API_KEY not found. Set it in environment or .env file.", file=sys.stderr)
sys.exit(1)
# Use browser-use's ChatOpenAI wrapper
llm = ChatOpenAI(model="gpt-4o-mini")
task = f"""
Go to Yelp.com and search for "{business_name}" in "{location}".
Click on the business in the search results.
Find and read the reviews section.
Extract the first {num_reviews} reviews, including:
- Reviewer name
- Star rating
- Date
- Review text (full text if possible)
Return the reviews in a structured format.
"""
agent = Agent(
task=task,
llm=llm,
)
result = await agent.run()
return result
def main():
parser = argparse.ArgumentParser(description='Get Yelp reviews using browser automation')
parser.add_argument('business', help='Business name to search for')
parser.add_argument('--location', '-l', default='San Francisco', help='Location (default: San Francisco)')
parser.add_argument('--num-reviews', '-n', type=int, default=5, help='Number of reviews to fetch (default: 5)')
args = parser.parse_args()
print(f"Fetching reviews for '{args.business}' in {args.location}...")
print("(This may take a minute as the browser navigates Yelp)\n")
result = asyncio.run(get_reviews(args.business, args.location, args.num_reviews))
print("\n=== Reviews ===\n")
print(result)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Search Yelp for a business by phone number."""
import argparse
import json
import os
import sys
import urllib.request
def load_api_key():
"""Load API key from .env file."""
env_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')
env_path = os.path.abspath(env_path)
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
if line.startswith('YELP_API_KEY='):
return line.strip().split('=', 1)[1]
return os.environ.get('YELP_API_KEY')
def search_by_phone(phone):
"""Search for a business by phone number."""
api_key = load_api_key()
if not api_key:
print("Error: YELP_API_KEY not found", file=sys.stderr)
sys.exit(1)
# Ensure phone is in E.164 format
phone = phone.replace(' ', '').replace('-', '').replace('(', '').replace(')', '')
if not phone.startswith('+'):
phone = '+1' + phone # Assume US if no country code
url = f"https://api.yelp.com/v3/businesses/search/phone?phone={phone}"
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {api_key}')
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
except urllib.error.HTTPError as e:
print(f"API Error: {e.code} - {e.read().decode()}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description='Search Yelp by phone number')
parser.add_argument('phone', help='Phone number (e.g., +14155551234)')
parser.add_argument('--json', action='store_true', help='Output raw JSON')
args = parser.parse_args()
data = search_by_phone(args.phone)
if args.json:
print(json.dumps(data, indent=2))
return
businesses = data.get('businesses', [])
if not businesses:
print(f"No business found for phone: {args.phone}")
return
print(f"Found {len(businesses)} business(es) for {args.phone}:\n")
for biz in businesses:
name = biz.get('name', 'Unknown')
rating = biz.get('rating', 'N/A')
review_count = biz.get('review_count', 0)
location = biz.get('location', {})
address = ', '.join(location.get('display_address', ['N/A']))
categories = ', '.join([c['title'] for c in biz.get('categories', [])])
print(f"📍 {name}")
print(f" ⭐ {rating} ({review_count} reviews)")
print(f" 📍 {address}")
print(f" 📂 {categories}")
print(f" 🔗 {biz.get('url', 'N/A')}")
print()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Scrape Yelp reviews using Browserbase (handles CAPTCHA with proxies)."""
import argparse
import json
import os
import sys
def load_env():
"""Load environment variables from .env file."""
# Try multiple paths
for env_path in [
os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env'),
os.path.join(os.getcwd(), '.env'),
]:
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key] = value
return True
return False
def scrape_reviews(url: str, num_reviews: int = 5):
"""Scrape reviews from a Yelp business page using Browserbase."""
from playwright.sync_api import sync_playwright
from browserbase import Browserbase
load_env()
api_key = os.environ.get("BROWSERBASE_API_KEY")
project_id = os.environ.get("BROWSERBASE_PROJECT_ID")
if not api_key or not project_id:
print("Error: BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID required", file=sys.stderr)
return None
bb = Browserbase(api_key=api_key)
with sync_playwright() as p:
print("Creating Browserbase session with proxies...", file=sys.stderr)
session = bb.sessions.create(
project_id=project_id,
proxies=True
)
browser = p.chromium.connect_over_cdp(session.connect_url)
context = browser.contexts[0]
page = context.pages[0]
try:
print(f"Loading {url}...", file=sys.stderr)
page.goto(url, timeout=45000)
page.wait_for_timeout(3000)
# Extract business name
name = "Unknown"
h1 = page.locator("h1")
if h1.count() > 0:
name = h1.first.text_content() or "Unknown"
# Extract rating
rating = None
rating_el = page.locator("[aria-label*='star rating']")
if rating_el.count() > 0:
rating = rating_el.first.get_attribute("aria-label")
# Extract reviews
reviews = []
seen_texts = set()
# Try p[class*='comment'] first
review_els = page.locator("p[class*='comment']")
count = review_els.count()
for i in range(count):
if len(reviews) >= num_reviews:
break
try:
text = review_els.nth(i).text_content()
if text and len(text) > 50:
text_key = text[:100]
if text_key not in seen_texts:
seen_texts.add(text_key)
reviews.append({"text": text.strip()})
except:
continue
print(f"Extracted {len(reviews)} reviews", file=sys.stderr)
finally:
browser.close()
print(f"Session: https://browserbase.com/sessions/{session.id}", file=sys.stderr)
return {"business": name.strip(), "rating": rating, "reviews": reviews}
def main():
parser = argparse.ArgumentParser(description='Scrape Yelp reviews via Browserbase')
parser.add_argument('url', help='Yelp business URL')
parser.add_argument('--num-reviews', '-n', type=int, default=5)
parser.add_argument('--json', action='store_true', help='Output as JSON')
args = parser.parse_args()
result = scrape_reviews(args.url, args.num_reviews)
if not result:
sys.exit(1)
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"\n=== {result['business']} ===")
if result['rating']:
print(f"Rating: {result['rating']}")
print()
for i, review in enumerate(result['reviews'], 1):
text = review['text']
if len(text) > 300:
text = text[:300] + "..."
print(f"[{i}] {text}\n")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""Search Yelp for businesses by term and location."""
import argparse
import json
import os
import sys
from urllib.parse import urlencode
import urllib.request
def load_api_key():
"""Load API key from .env file."""
env_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')
env_path = os.path.abspath(env_path)
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
if line.startswith('YELP_API_KEY='):
return line.strip().split('=', 1)[1]
# Fall back to environment variable
return os.environ.get('YELP_API_KEY')
def search_businesses(term, location=None, latitude=None, longitude=None,
limit=5, sort_by='best_match', price=None):
"""Search for businesses on Yelp."""
api_key = load_api_key()
if not api_key:
print("Error: YELP_API_KEY not found in .env or environment", file=sys.stderr)
sys.exit(1)
params = {'term': term, 'limit': limit, 'sort_by': sort_by}
if location:
params['location'] = location
elif latitude and longitude:
params['latitude'] = latitude
params['longitude'] = longitude
else:
print("Error: Must provide --location or --latitude/--longitude", file=sys.stderr)
sys.exit(1)
if price:
params['price'] = price
url = f"https://api.yelp.com/v3/businesses/search?{urlencode(params)}"
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {api_key}')
try:
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
except urllib.error.HTTPError as e:
print(f"API Error: {e.code} - {e.read().decode()}", file=sys.stderr)
sys.exit(1)
return data
def format_hours(hours_data):
"""Format business hours for display."""
if not hours_data:
return "Hours not available"
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
hours_list = hours_data[0].get('open', [])
formatted = []
for entry in hours_list:
day = days[entry['day']]
start = f"{entry['start'][:2]}:{entry['start'][2:]}"
end = f"{entry['end'][:2]}:{entry['end'][2:]}"
formatted.append(f"{day}: {start}-{end}")
return ', '.join(formatted) if formatted else "Hours not available"
def main():
parser = argparse.ArgumentParser(description='Search Yelp for businesses')
parser.add_argument('term', help='Search term (e.g., "dog groomer", "pizza")')
parser.add_argument('--location', '-l', help='Location (e.g., "San Francisco, CA")')
parser.add_argument('--latitude', type=float, help='Latitude for location-based search')
parser.add_argument('--longitude', type=float, help='Longitude for location-based search')
parser.add_argument('--limit', '-n', type=int, default=5, help='Number of results (default: 5)')
parser.add_argument('--sort-by', choices=['best_match', 'rating', 'review_count', 'distance'],
default='best_match', help='Sort order')
parser.add_argument('--price', help='Price filter (1,2,3,4 for $-$$$$)')
parser.add_argument('--json', action='store_true', help='Output raw JSON')
args = parser.parse_args()
data = search_businesses(
term=args.term,
location=args.location,
latitude=args.latitude,
longitude=args.longitude,
limit=args.limit,
sort_by=args.sort_by,
price=args.price
)
if args.json:
print(json.dumps(data, indent=2))
return
businesses = data.get('businesses', [])
total = data.get('total', 0)
print(f"Found {total} results for \"{args.term}\"")
print(f"Showing top {len(businesses)}:\n")
for i, biz in enumerate(businesses, 1):
name = biz.get('name', 'Unknown')
rating = biz.get('rating', 'N/A')
review_count = biz.get('review_count', 0)
price = biz.get('price', 'N/A')
phone = biz.get('phone', 'N/A')
display_phone = biz.get('display_phone', phone)
location = biz.get('location', {})
address = ', '.join(location.get('display_address', ['Address not available']))
distance = biz.get('distance')
distance_str = f"{distance/1609.34:.1f} mi" if distance else "N/A"
hours = biz.get('business_hours', [])
is_open = hours[0].get('is_open_now', False) if hours else None
open_status = "Open now" if is_open else ("Closed" if is_open is False else "")
categories = ', '.join([c['title'] for c in biz.get('categories', [])])
print(f"[{i}] {name}")
print(f" ⭐ {rating} ({review_count} reviews) | {price} | {distance_str}")
print(f" 📍 {address}")
print(f" 📞 {display_phone}")
if open_status:
print(f" 🕐 {open_status}")
if categories:
print(f" 📂 {categories}")
print()
if __name__ == '__main__':
main()