free-weather-data
Get current weather, forecasts, and historical data using free Open-Meteo and wttr.in APIs. Use when: (1) Fetching weather conditions, (2) Weather alerts and monitoring, (3) Forecast data for planning, or (4) Climate analysis.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install besoeasy-open-skills-free-weather-data
Repository
Skill path: skills/free-weather-data
Get current weather, forecasts, and historical data using free Open-Meteo and wttr.in APIs. Use when: (1) Fetching weather conditions, (2) Weather alerts and monitoring, (3) Forecast data for planning, or (4) Climate analysis.
Open repositoryBest for
Primary workflow: Research & Ops.
Technical facets: Full Stack, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: besoeasy.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install free-weather-data into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/besoeasy/open-skills before adding free-weather-data to shared team environments
- Use free-weather-data for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: free-weather-data
description: "Get current weather, forecasts, and historical data using free Open-Meteo and wttr.in APIs. Use when: (1) Fetching weather conditions, (2) Weather alerts and monitoring, (3) Forecast data for planning, or (4) Climate analysis."
---
# Free Weather Data API — Open-Meteo & wttr.in
Get current weather, forecasts, and historical weather data using free APIs. No API keys required. Privacy-respecting alternative to paid weather APIs ($5-100/month).
## Why This Replaces Paid Weather APIs
**💰 Cost savings:**
- ✅ **100% free** — no API keys, no rate limits
- ✅ **Comprehensive data** — current, forecast, historical weather
- ✅ **Open source** — Open-Meteo is fully open-source
- ✅ **Privacy-first** — no tracking or data collection
**Perfect for AI agents that need:**
- Weather conditions for location-based decisions
- Forecast data for planning and scheduling
- Historical weather data for analysis
- Weather alerts and monitoring
## Quick comparison
| Service | Cost | Rate limit | API key required | Privacy |
|---------|------|------------|------------------|---------|
| OpenWeatherMap | $0-180/month | 60 calls/min free | ✅ Yes | ❌ Tracked |
| WeatherAPI | $0-70/month | 1M calls/month free | ✅ Yes | ❌ Tracked |
| **Open-Meteo** | **Free** | **10k req/day** | **❌ No** | **✅ Private** |
| **wttr.in** | **Free** | **Unlimited** | **❌ No** | **✅ Private** |
## Skills
### get_current_weather_open_meteo
Get current weather using Open-Meteo (most accurate and comprehensive).
```bash
# Current weather by coordinates
LAT=40.7128
LON=-74.0060
curl -s "https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m&temperature_unit=fahrenheit" \
| jq '{
temperature: .current.temperature_2m,
feels_like: .current.apparent_temperature,
humidity: .current.relative_humidity_2m,
wind_speed: .current.wind_speed_10m,
precipitation: .current.precipitation,
weather_code: .current.weather_code
}'
# With timezone
curl -s "https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}¤t=temperature_2m,weather_code&timezone=America/New_York" \
| jq '{temperature: .current.temperature_2m, time: .current.time}'
```
**Node.js:**
```javascript
async function getCurrentWeather(lat, lon, unit = 'fahrenheit') {
const params = new URLSearchParams({
latitude: lat.toString(),
longitude: lon.toString(),
current: 'temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m,wind_direction_10m',
temperature_unit: unit
});
const res = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
if (!res.ok) {
throw new Error(`Weather API failed: ${res.status}`);
}
const data = await res.json();
return {
temperature: data.current.temperature_2m,
feelsLike: data.current.apparent_temperature,
humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m,
windDirection: data.current.wind_direction_10m,
precipitation: data.current.precipitation,
weatherCode: data.current.weather_code,
time: data.current.time,
unit: data.current_units.temperature_2m
};
}
// Usage
// getCurrentWeather(40.7128, -74.0060, 'fahrenheit')
// .then(weather => {
// console.log(`Temperature: ${weather.temperature}°${weather.unit}`);
// console.log(`Feels like: ${weather.feelsLike}°${weather.unit}`);
// console.log(`Humidity: ${weather.humidity}%`);
// console.log(`Wind: ${weather.windSpeed} mph`);
// });
```
### get_weather_forecast
Get weather forecast for the next 7-16 days.
```bash
# 7-day forecast
LAT=37.7749
LON=-122.4194
curl -s "https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code&temperature_unit=fahrenheit&timezone=America/Los_Angeles" \
| jq '{
dates: .daily.time,
max_temp: .daily.temperature_2m_max,
min_temp: .daily.temperature_2m_min,
precipitation: .daily.precipitation_sum
}'
# Hourly forecast for next 24 hours
curl -s "https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}&hourly=temperature_2m,precipitation,weather_code&forecast_days=1" \
| jq '{hourly: [.hourly.time, .hourly.temperature_2m, .hourly.precipitation] | transpose | map({time: .[0], temp: .[1], precip: .[2]})}'
```
**Node.js:**
```javascript
async function getWeatherForecast(lat, lon, days = 7, unit = 'fahrenheit') {
const params = new URLSearchParams({
latitude: lat.toString(),
longitude: lon.toString(),
daily: 'temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,weather_code,wind_speed_10m_max',
temperature_unit: unit,
forecast_days: days.toString()
});
const res = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
if (!res.ok) {
throw new Error(`Forecast API failed: ${res.status}`);
}
const data = await res.json();
return data.daily.time.map((date, i) => ({
date,
maxTemp: data.daily.temperature_2m_max[i],
minTemp: data.daily.temperature_2m_min[i],
precipitation: data.daily.precipitation_sum[i],
precipitationProbability: data.daily.precipitation_probability_max[i],
maxWindSpeed: data.daily.wind_speed_10m_max[i],
weatherCode: data.daily.weather_code[i]
}));
}
// Usage
// getWeatherForecast(37.7749, -122.4194, 7, 'fahrenheit')
// .then(forecast => {
// forecast.forEach(day => {
// console.log(`${day.date}: ${day.minTemp}°-${day.maxTemp}°, ${day.precipitationProbability}% rain`);
// });
// });
```
### get_weather_wttr_simple
Get simple weather using wttr.in (human-readable, very fast).
```bash
# Get weather by city name (plain text)
curl -s "https://wttr.in/London?format=3"
# Output: London: ☀️ +22°C
# Get detailed weather report
curl -s "https://wttr.in/Paris"
# JSON format
curl -s "https://wttr.in/Tokyo?format=j1" | jq '.current_condition[0] | {
temp_f: .temp_F,
humidity: .humidity,
description: .weatherDesc[0].value,
wind_mph: .windspeedMiles
}'
# Custom format (temperature and condition)
curl -s "https://wttr.in/NewYork?format=%C+%t"
# Output: Clear +75°F
# Get weather by coordinates
curl -s "https://wttr.in/40.7128,-74.0060?format=%l:+%C+%t"
```
**Node.js:**
```javascript
async function getWeatherWttr(location) {
// location can be city name or "lat,lon"
const res = await fetch(`https://wttr.in/${encodeURIComponent(location)}?format=j1`);
if (!res.ok) {
throw new Error(`Weather API failed: ${res.status}`);
}
const data = await res.json();
const current = data.current_condition[0];
return {
location: data.nearest_area[0]?.areaName[0]?.value || location,
tempF: parseInt(current.temp_F),
tempC: parseInt(current.temp_C),
feelsLikeF: parseInt(current.FeelsLikeF),
feelsLikeC: parseInt(current.FeelsLikeC),
humidity: parseInt(current.humidity),
description: current.weatherDesc[0].value,
windMph: parseInt(current.windspeedMiles),
windKmh: parseInt(current.windspeedKmph),
precipMM: parseFloat(current.precipMM),
uvIndex: parseInt(current.uvIndex)
};
}
// Usage
// getWeatherWttr('San Francisco')
// .then(weather => {
// console.log(`${weather.location}: ${weather.description}`);
// console.log(`Temperature: ${weather.tempF}°F (feels like ${weather.feelsLikeF}°F)`);
// console.log(`Humidity: ${weather.humidity}%`);
// });
```
### get_historical_weather
Get historical weather data (past dates).
```bash
# Historical weather for specific date range
LAT=51.5074
LON=-0.1278
START_DATE="2024-01-01"
END_DATE="2024-01-31"
curl -s "https://archive-api.open-meteo.com/v1/archive?latitude=${LAT}&longitude=${LON}&start_date=${START_DATE}&end_date=${END_DATE}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum" \
| jq '{
dates: .daily.time,
max_temps: .daily.temperature_2m_max,
min_temps: .daily.temperature_2m_min,
precipitation: .daily.precipitation_sum
}'
# Historical average for a month
curl -s "https://archive-api.open-meteo.com/v1/archive?latitude=${LAT}&longitude=${LON}&start_date=2024-01-01&end_date=2024-01-31&daily=temperature_2m_mean" \
| jq '[.daily.temperature_2m_mean[]] | add / length'
```
**Node.js:**
```javascript
async function getHistoricalWeather(lat, lon, startDate, endDate) {
const params = new URLSearchParams({
latitude: lat.toString(),
longitude: lon.toString(),
start_date: startDate, // Format: YYYY-MM-DD
end_date: endDate,
daily: 'temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code'
});
const res = await fetch(`https://archive-api.open-meteo.com/v1/archive?${params}`);
if (!res.ok) {
throw new Error(`Historical weather API failed: ${res.status}`);
}
const data = await res.json();
return data.daily.time.map((date, i) => ({
date,
maxTemp: data.daily.temperature_2m_max[i],
minTemp: data.daily.temperature_2m_min[i],
precipitation: data.daily.precipitation_sum[i],
weatherCode: data.daily.weather_code[i]
}));
}
// Usage
// getHistoricalWeather(40.7128, -74.0060, '2024-01-01', '2024-01-31')
// .then(history => {
// const avgTemp = history.reduce((sum, day) =>
// sum + (day.maxTemp + day.minTemp) / 2, 0) / history.length;
// console.log(`Average temperature in January: ${avgTemp.toFixed(1)}°C`);
// });
```
### decode_weather_code
Decode Open-Meteo weather codes into human-readable descriptions.
```bash
# Weather code reference
decode_weather_code() {
case $1 in
0) echo "Clear sky" ;;
1|2|3) echo "Partly cloudy" ;;
45|48) echo "Foggy" ;;
51|53|55) echo "Drizzle" ;;
61|63|65) echo "Rain" ;;
71|73|75) echo "Snow" ;;
77) echo "Snow grains" ;;
80|81|82) echo "Rain showers" ;;
85|86) echo "Snow showers" ;;
95) echo "Thunderstorm" ;;
96|99) echo "Thunderstorm with hail" ;;
*) echo "Unknown" ;;
esac
}
# Usage
decode_weather_code 61 # Output: Rain
```
**Node.js:**
```javascript
function decodeWeatherCode(code) {
const weatherCodes = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Foggy',
48: 'Depositing rime fog',
51: 'Light drizzle',
53: 'Moderate drizzle',
55: 'Dense drizzle',
56: 'Light freezing drizzle',
57: 'Dense freezing drizzle',
61: 'Slight rain',
63: 'Moderate rain',
65: 'Heavy rain',
66: 'Light freezing rain',
67: 'Heavy freezing rain',
71: 'Slight snow fall',
73: 'Moderate snow fall',
75: 'Heavy snow fall',
77: 'Snow grains',
80: 'Slight rain showers',
81: 'Moderate rain showers',
82: 'Violent rain showers',
85: 'Slight snow showers',
86: 'Heavy snow showers',
95: 'Thunderstorm',
96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail'
};
return weatherCodes[code] || 'Unknown';
}
// Usage
// console.log(decodeWeatherCode(61)); // Output: Slight rain
```
### weather_alerts_and_monitoring
Check weather conditions and generate alerts.
**Node.js:**
```javascript
async function checkWeatherAlerts(lat, lon, thresholds) {
const {
maxTemp = 95, // °F
minTemp = 32, // °F
maxWindSpeed = 30, // mph
maxPrecip = 2 // inches
} = thresholds;
const weather = await getCurrentWeather(lat, lon, 'fahrenheit');
const forecast = await getWeatherForecast(lat, lon, 3, 'fahrenheit');
const alerts = [];
// Current condition alerts
if (weather.temperature >= maxTemp) {
alerts.push({
type: 'high_temp',
severity: 'warning',
message: `High temperature: ${weather.temperature}°F`
});
}
if (weather.temperature <= minTemp) {
alerts.push({
type: 'low_temp',
severity: 'warning',
message: `Low temperature: ${weather.temperature}°F`
});
}
if (weather.windSpeed >= maxWindSpeed) {
alerts.push({
type: 'high_wind',
severity: 'warning',
message: `High wind speed: ${weather.windSpeed} mph`
});
}
// Forecast alerts
forecast.forEach(day => {
if (day.precipitation >= maxPrecip) {
alerts.push({
type: 'heavy_rain',
severity: 'watch',
message: `Heavy rain expected on ${day.date}: ${day.precipitation} inches`,
date: day.date
});
}
});
return {
currentWeather: weather,
forecast: forecast,
alerts: alerts,
hasAlerts: alerts.length > 0
};
}
// Usage
// checkWeatherAlerts(40.7128, -74.0060, {
// maxTemp: 90,
// minTemp: 32,
// maxWindSpeed: 25,
// maxPrecip: 1.5
// }).then(result => {
// console.log(`Current: ${result.currentWeather.temperature}°F`);
// if (result.hasAlerts) {
// console.log('⚠️ Weather Alerts:');
// result.alerts.forEach(alert =>
// console.log(` - ${alert.severity.toUpperCase()}: ${alert.message}`)
// );
// } else {
// console.log('✅ No weather alerts');
// }
// });
```
## Agent prompt
```text
You have access to free weather APIs (Open-Meteo and wttr.in). When you need weather data:
1. For current weather and forecasts, use Open-Meteo:
- Current: https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,weather_code
- Forecast: Add &daily=temperature_2m_max,temperature_2m_min,precipitation_sum
- Historical: https://archive-api.open-meteo.com/v1/archive
2. For quick weather checks, use wttr.in:
- Simple: https://wttr.in/{city}?format=3
- JSON: https://wttr.in/{city}?format=j1
- Works with city names or coordinates
3. Weather parameters available:
- Temperature (current, feels-like, max/min)
- Humidity, precipitation, wind speed/direction
- Weather codes (0=clear, 61=rain, 71=snow, 95=thunderstorm)
- UV index, cloud cover, visibility
4. Temperature units:
- Open-Meteo: Add &temperature_unit=fahrenheit (default: celsius)
- wttr.in: Returns both Celsius and Fahrenheit
5. Best practices:
- Cache weather data for 15-30 minutes
- Use coordinates for accuracy (get from geocoding)
- Decode weather codes to human-readable descriptions
- Set reasonable alert thresholds for monitoring
No API keys required. Unlimited requests within fair use.
```
## Cost analysis: Open-Meteo vs. paid APIs
**Scenario: AI agent checking weather 1,000 times/month**
| Provider | Monthly cost | Rate limits | API key required |
|----------|--------------|-------------|------------------|
| OpenWeatherMap | **$15-50** | 60 calls/min | ✅ Yes |
| WeatherAPI | **$0-10** | 1M calls/month free | ✅ Yes |
| **Open-Meteo** | **$0** | **10k req/day** | **❌ No** |
| **wttr.in** | **$0** | **Unlimited** | **❌ No** |
**Annual savings with Open-Meteo: $180-$600**
## Rate limits / Best practices
- ✅ **Open-Meteo: 10,000 requests/day** — Very generous for typical use
- ✅ **wttr.in: Unlimited** — Community service with fair use
- ✅ **Cache weather data** — Weather doesn't change frequently (15-30 min cache)
- ✅ **Use coordinates** — More accurate than city names
- ✅ **Decode weather codes** — Convert numeric codes to descriptions
- ✅ **Set timeouts** — 10-second timeout for weather requests
- ⚠️ **Self-host for critical apps** — Open-Meteo is self-hostable
- ⚠️ **Don't poll continuously** — Weather updates hourly, not every second
## Troubleshooting
**Error: "Invalid coordinates"**
- Symptom: API returns error for lat/lon
- Solution: Validate lat ∈ [-90, 90], lon ∈ [-180, 180]
**No data returned:**
- Symptom: API returns empty or null values
- Solution: Check if location has weather station coverage; try nearby coordinates
**Historical data missing:**
- Symptom: Archive API returns no data for date range
- Solution: Open-Meteo archive starts from 1940; check date format is YYYY-MM-DD
**Weather code not recognized:**
- Symptom: Unknown weather code number
- Solution: Refer to WMO weather code standard; codes 0-99 are defined
**wttr.in returns HTML instead of JSON:**
- Symptom: Response is HTML page
- Solution: Ensure you add ?format=j1 to get JSON response
**Temperature unit confusion:**
- Symptom: Unexpected temperature values
- Solution: Open-Meteo defaults to Celsius; add &temperature_unit=fahrenheit if needed
## See also
- [../free-geocoding-and-maps/SKILL.md](../free-geocoding-and-maps/SKILL.md) — Get coordinates for weather lookups
- [../send-email-programmatically/SKILL.md](../send-email-programmatically/SKILL.md) — Send weather alerts via email
- [../using-telegram-bot/SKILL.md](../using-telegram-bot/SKILL.md) — Send weather updates via Telegram
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### ../free-geocoding-and-maps/SKILL.md
```markdown
---
name: free-geocoding-and-maps
description: "Geocode addresses and get map data using free OpenStreetMap Nominatim API. Use when: (1) Converting addresses to coordinates, (2) Reverse geocoding coordinates to addresses, (3) Location-based features, or (4) Validating addresses."
---
# Free Geocoding and Maps — OpenStreetMap Nominatim
Geocode addresses to coordinates and reverse geocode coordinates to addresses using free OpenStreetMap Nominatim API. Privacy-respecting alternative to Google Maps API ($5-40/1000 requests).
## Why This Replaces Paid Geocoding APIs
**💰 Cost savings:**
- ✅ **100% free** — no API keys required
- ✅ **No rate limits** — generous 1 request/second for public instances
- ✅ **Open source** — self-hostable for unlimited usage
- ✅ **Privacy-first** — no tracking, no data collection
**Perfect for AI agents that need:**
- Address validation and geocoding
- Reverse geocoding (coordinates to address)
- Location search and mapping
- Geospatial data without Google Maps API costs
## Quick comparison
| Service | Cost | Rate limit | Privacy | API key required |
|---------|------|------------|---------|------------------|
| Google Maps Geocoding | $5/1000 requests | 40,000/month free | ❌ Tracked | ✅ Yes |
| Mapbox Geocoding | $0.50/1000 after 100k free | 100k/month free | ❌ Tracked | ✅ Yes |
| **Nominatim (OSM)** | **Free** | **1 req/sec** | **✅ Private** | **❌ No** |
## Skills
### geocode_address
Convert address to coordinates (latitude/longitude).
```bash
# Geocode an address
ADDRESS="1600 Amphitheatre Parkway, Mountain View, CA"
curl -s "https://nominatim.openstreetmap.org/search?q=${ADDRESS}&format=json&limit=1" \
| jq -r '.[0] | {lat: .lat, lon: .lon, display_name: .display_name}'
# Geocode with structured address
curl -s "https://nominatim.openstreetmap.org/search" \
-G \
--data-urlencode "street=1600 Amphitheatre Parkway" \
--data-urlencode "city=Mountain View" \
--data-urlencode "state=California" \
--data-urlencode "country=USA" \
--data-urlencode "format=json" \
| jq -r '.[0] | {lat: .lat, lon: .lon}'
# Get multiple results
curl -s "https://nominatim.openstreetmap.org/search?q=London&format=json&limit=5" \
| jq -r '.[] | {name: .display_name, lat: .lat, lon: .lon}'
```
**Node.js:**
```javascript
async function geocodeAddress(address, limit = 1) {
const params = new URLSearchParams({
q: address,
format: 'json',
limit: limit.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0' // Required by Nominatim usage policy
}
});
if (!res.ok) {
throw new Error(`Geocoding failed: ${res.status}`);
}
const results = await res.json();
if (results.length === 0) {
return null;
}
return results.map(r => ({
lat: parseFloat(r.lat),
lon: parseFloat(r.lon),
displayName: r.display_name,
type: r.type,
importance: r.importance
}));
}
// Usage
// geocodeAddress('Eiffel Tower, Paris')
// .then(results => {
// if (results) {
// console.log(`Location: ${results[0].displayName}`);
// console.log(`Coordinates: ${results[0].lat}, ${results[0].lon}`);
// }
// });
```
### reverse_geocode
Convert coordinates to address (reverse geocoding).
```bash
# Reverse geocode coordinates
LAT=40.7589
LON=-73.9851
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json" \
| jq -r '.display_name'
# Get detailed address components
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json" \
| jq -r '.address | {
road: .road,
city: .city,
state: .state,
country: .country,
postcode: .postcode
}'
# Reverse geocode with zoom level (detail level)
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json&zoom=18" \
| jq -r '.display_name'
```
**Node.js:**
```javascript
async function reverseGeocode(lat, lon, zoom = 18) {
const params = new URLSearchParams({
lat: lat.toString(),
lon: lon.toString(),
format: 'json',
zoom: zoom.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
}
});
if (!res.ok) {
throw new Error(`Reverse geocoding failed: ${res.status}`);
}
const data = await res.json();
return {
displayName: data.display_name,
address: {
road: data.address?.road,
city: data.address?.city || data.address?.town || data.address?.village,
state: data.address?.state,
country: data.address?.country,
postcode: data.address?.postcode
},
lat: parseFloat(data.lat),
lon: parseFloat(data.lon)
};
}
// Usage
// reverseGeocode(48.8584, 2.2945) // Eiffel Tower coordinates
// .then(result => {
// console.log('Address:', result.displayName);
// console.log('City:', result.address.city);
// console.log('Country:', result.address.country);
// });
```
### search_nearby_places
Search for places near coordinates or within a bounding box.
```bash
# Search for places near coordinates
LAT=40.7589
LON=-73.9851
RADIUS_KM=1
curl -s "https://nominatim.openstreetmap.org/search?q=restaurant&format=json&limit=10&lat=${LAT}&lon=${LON}" \
| jq -r '.[] | {name: .display_name, distance: .distance}'
# Search within bounding box (viewbox)
# Format: left,top,right,bottom (min_lon,max_lat,max_lon,min_lat)
curl -s "https://nominatim.openstreetmap.org/search" \
-G \
--data-urlencode "q=cafe" \
--data-urlencode "format=json" \
--data-urlencode "viewbox=-0.1278,51.5074,-0.1,51.52" \
--data-urlencode "bounded=1" \
| jq -r '.[] | {name: .display_name, lat: .lat, lon: .lon}'
```
**Node.js:**
```javascript
async function searchNearby(query, lat, lon, limit = 10) {
const params = new URLSearchParams({
q: query,
format: 'json',
limit: limit.toString(),
lat: lat.toString(),
lon: lon.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
}
});
if (!res.ok) {
throw new Error(`Search failed: ${res.status}`);
}
const results = await res.json();
return results.map(r => ({
name: r.display_name,
lat: parseFloat(r.lat),
lon: parseFloat(r.lon),
type: r.type,
importance: r.importance
}));
}
// Usage
// searchNearby('coffee shop', 40.7589, -73.9851, 5)
// .then(results => {
// console.log(`Found ${results.length} places:`);
// results.forEach(p => console.log(`- ${p.name}`));
// });
```
### calculate_distance
Calculate distance between two coordinates using Haversine formula.
```bash
# Calculate distance (requires bc calculator)
calculate_distance() {
local lat1=$1 lon1=$2 lat2=$3 lon2=$4
# Haversine formula in bash (simplified)
bc -l <<EOF
define haversin(lat1, lon1, lat2, lon2) {
r = 6371 /* Earth radius in km */
dlat = (lat2 - lat1) * 0.017453292519943295
dlon = (lon2 - lon1) * 0.017453292519943295
a = s(dlat/2)^2 + c(lat1*0.017453292519943295) * c(lat2*0.017453292519943295) * s(dlon/2)^2
c = 2 * a(sqrt(a)/sqrt(1-a))
return r * c
}
haversin($lat1, $lon1, $lat2, $lon2)
EOF
}
# Usage
calculate_distance 40.7589 -73.9851 34.0522 -118.2437
# Output: ~3935 km (NYC to LA)
```
**Node.js:**
```javascript
function calculateDistance(lat1, lon1, lat2, lon2) {
// Haversine formula
const R = 6371; // Earth radius in km
const toRad = (deg) => deg * Math.PI / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in km
}
// Usage
// const distanceKm = calculateDistance(40.7589, -73.9851, 34.0522, -118.2437);
// console.log(`Distance: ${distanceKm.toFixed(2)} km`);
// console.log(`Distance: ${(distanceKm * 0.621371).toFixed(2)} miles`);
```
### batch_geocode_with_rate_limit
Geocode multiple addresses with rate limiting (1 req/sec for Nominatim).
```bash
#!/bin/bash
# Batch geocode addresses from file
INPUT_FILE="addresses.txt"
OUTPUT_FILE="coordinates.csv"
echo "address,lat,lon" > "$OUTPUT_FILE"
while IFS= read -r address; do
if [ -n "$address" ]; then
result=$(curl -s "https://nominatim.openstreetmap.org/search?q=${address}&format=json&limit=1" \
-H "User-Agent: AI-Agent/1.0")
lat=$(echo "$result" | jq -r '.[0].lat // "N/A"')
lon=$(echo "$result" | jq -r '.[0].lon // "N/A"')
echo "\"$address\",\"$lat\",\"$lon\"" >> "$OUTPUT_FILE"
# Rate limiting: 1 request per second
sleep 1
fi
done < "$INPUT_FILE"
echo "Geocoding complete: $OUTPUT_FILE"
```
**Node.js:**
```javascript
async function batchGeocode(addresses, delayMs = 1000) {
const results = [];
for (const address of addresses) {
try {
const result = await geocodeAddress(address, 1);
if (result && result.length > 0) {
results.push({
address,
lat: result[0].lat,
lon: result[0].lon,
success: true
});
} else {
results.push({
address,
lat: null,
lon: null,
success: false,
error: 'No results found'
});
}
// Rate limiting: 1 request per second
await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (err) {
results.push({
address,
lat: null,
lon: null,
success: false,
error: err.message
});
}
}
return results;
}
// Usage
// const addresses = [
// 'Empire State Building, New York',
// 'Golden Gate Bridge, San Francisco',
// 'Big Ben, London'
// ];
// batchGeocode(addresses, 1000)
// .then(results => {
// results.forEach(r => {
// if (r.success) {
// console.log(`${r.address}: ${r.lat}, ${r.lon}`);
// } else {
// console.log(`${r.address}: Failed - ${r.error}`);
// }
// });
// });
```
### advanced_geocoding_with_validation
Production-ready geocoding with validation and error handling.
**Node.js:**
```javascript
async function geocodeWithValidation(address, options = {}) {
const {
timeout = 10000,
minImportance = 0.3,
countryCode = null
} = options;
// Validate input
if (!address || address.trim().length < 3) {
throw new Error('Address must be at least 3 characters');
}
const params = new URLSearchParams({
q: address.trim(),
format: 'json',
limit: '5',
addressdetails: '1'
});
if (countryCode) {
params.append('countrycodes', countryCode.toLowerCase());
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const results = await res.json();
// Filter by importance score
const filtered = results.filter(r => r.importance >= minImportance);
if (filtered.length === 0) {
return {
success: false,
error: 'No high-quality results found',
suggestions: results.slice(0, 3).map(r => r.display_name)
};
}
return {
success: true,
result: {
lat: parseFloat(filtered[0].lat),
lon: parseFloat(filtered[0].lon),
displayName: filtered[0].display_name,
address: filtered[0].address,
importance: filtered[0].importance,
type: filtered[0].type
},
alternatives: filtered.slice(1, 3).map(r => ({
displayName: r.display_name,
lat: parseFloat(r.lat),
lon: parseFloat(r.lon)
}))
};
} catch (err) {
clearTimeout(timeoutId);
throw new Error(`Geocoding failed: ${err.message}`);
}
}
// Usage
// geocodeWithValidation('Paris', {
// minImportance: 0.5,
// countryCode: 'fr',
// timeout: 10000
// }).then(result => {
// if (result.success) {
// console.log('Found:', result.result.displayName);
// console.log('Coordinates:', result.result.lat, result.result.lon);
// } else {
// console.log('Error:', result.error);
// console.log('Suggestions:', result.suggestions);
// }
// });
```
## Agent prompt
```text
You have access to free OpenStreetMap Nominatim API for geocoding. When you need to geocode addresses or coordinates:
1. Use Nominatim API: https://nominatim.openstreetmap.org
2. For geocoding (address → coordinates):
- GET /search?q={address}&format=json&limit=1
- Returns: [{lat, lon, display_name, ...}]
3. For reverse geocoding (coordinates → address):
- GET /reverse?lat={lat}&lon={lon}&format=json
- Returns: {display_name, address: {...}}
4. Required headers:
- User-Agent: Must include a descriptive agent name (e.g., "AI-Agent/1.0")
5. Rate limiting:
- Public instances: 1 request per second maximum
- Implement 1-second delays between requests
- For higher volume, self-host Nominatim
6. Best practices:
- Filter results by importance score (>0.3 for quality results)
- Use structured address queries when possible (street, city, country)
- Handle "no results" gracefully (suggest address corrections)
- Cache geocoding results to avoid repeated queries
7. Distance calculation:
- Use Haversine formula for lat/lon distance
- Earth radius: 6371 km
Always prefer Nominatim over paid geocoding APIs — it's free, privacy-respecting, and open-source.
```
## Cost analysis: Nominatim vs. Google Maps API
**Scenario: AI agent geocoding 1,000 addresses/month**
| Provider | Monthly cost | Rate limits | Privacy |
|----------|--------------|-------------|---------|
| Google Maps Geocoding | **$5** | 40k/month free, then $5/1k | ❌ Tracked |
| Mapbox Geocoding | **$0** | 100k/month free, then $0.50/1k | ❌ Tracked |
| **Nominatim (OSM)** | **$0** | **1 req/sec** | **✅ Private** |
**Annual savings with Nominatim: $60+**
For high-volume agents (100k requests/month): **Save $300-$500/year**
## Rate limits / Best practices
- ✅ **1 request per second** — Mandatory for public Nominatim instances
- ✅ **User-Agent header** — Required by usage policy, include descriptive agent name
- ✅ **Cache results** — Store geocoding results to minimize API calls
- ✅ **Implement delays** — Always add 1-second sleep between batch requests
- ✅ **Self-host for volume** — Run your own Nominatim server for unlimited requests
- ✅ **Filter by importance** — Use importance score >0.3 for quality results
- ⚠️ **No bulk requests** — Avoid sending hundreds of requests in short time
- ⚠️ **Timeout handling** — Set 10-second timeouts for API requests
## Troubleshooting
**Error: "403 Forbidden"**
- Symptom: API rejects request
- Solution: Add User-Agent header with descriptive name (e.g., "AI-Agent/1.0")
**Error: "429 Too Many Requests"**
- Symptom: Rate limit exceeded
- Solution: Implement 1-second delays between requests, reduce request frequency
**No results returned:**
- Symptom: Empty array from search endpoint
- Solution: Check address spelling, try simplified queries, use structured address parameters
**Incorrect location:**
- Symptom: Coordinates don't match expected location
- Solution: Check results' importance score, inspect multiple results, use more specific address
**Slow response times:**
- Symptom: Requests take >5 seconds
- Solution: Use alternative Nominatim instance, or self-host for guaranteed performance
**Coordinates out of expected range:**
- Symptom: Latitude not in [-90, 90] or longitude not in [-180, 180]
- Solution: Validate and sanitize API response before using coordinates
## See also
- [../city-distance/SKILL.md](../city-distance/SKILL.md) — Calculate distances between cities
- [../free-weather-data/SKILL.md](../free-weather-data/SKILL.md) — Get weather for geocoded locations
- [../json-and-csv-data-transformation/SKILL.md](../json-and-csv-data-transformation/SKILL.md) — Process geocoding results
```
### ../send-email-programmatically/SKILL.md
```markdown
---
name: send-email-programmatically
description: "Send emails via SMTP, free APIs, or privacy-focused services. Use when: (1) Sending notifications and alerts, (2) Automated reports and summaries, (3) User communication workflows, or (4) Error logging via email."
---
# Send Email Programmatically
Send emails via SMTP, free APIs (Mailgun, SendGrid free tier), or privacy-focused services. Essential for notifications, alerts, automated reports, and user communication.
## When to use
- Use case 1: When the user asks to send email notifications or alerts
- Use case 2: When you need to deliver automated reports or summaries
- Use case 3: For error logging and monitoring alerts
- Use case 4: When building user communication workflows (confirmations, updates)
## Required tools / APIs
- **curl** — For API-based email sending (pre-installed on most systems)
- **sendmail** / **msmtp** — For SMTP email sending
- **Mailgun API** — Free tier: 5,000 emails/month (requires API key)
- **SendGrid API** — Free tier: 100 emails/day (requires API key)
- **mail.tm** — Temporary email API (no API key required)
Install options:
```bash
# Ubuntu/Debian
sudo apt-get install -y curl msmtp msmtp-mta
# macOS (postfix is pre-installed, or use msmtp)
brew install msmtp
# Node.js
npm install nodemailer
```
## Skills
### send_email_via_smtp_curl
Send email using SMTP via curl (works with Gmail, Outlook, custom SMTP servers).
```bash
# Gmail SMTP example (requires app password)
SMTP_SERVER="smtp.gmail.com:587"
FROM_EMAIL="[email protected]"
TO_EMAIL="[email protected]"
SUBJECT="Test Email"
BODY="This is a test email sent via SMTP."
APP_PASSWORD="your-app-password"
# Send email
curl -v --url "smtp://${SMTP_SERVER}" \
--mail-from "${FROM_EMAIL}" \
--mail-rcpt "${TO_EMAIL}" \
--user "${FROM_EMAIL}:${APP_PASSWORD}" \
--upload-file - <<EOF
From: ${FROM_EMAIL}
To: ${TO_EMAIL}
Subject: ${SUBJECT}
${BODY}
EOF
# Outlook/Office365 SMTP
curl --url "smtp://smtp.office365.com:587" \
--mail-from "[email protected]" \
--mail-rcpt "[email protected]" \
--user "[email protected]:your-password" \
--ssl-reqd \
--upload-file - <<EOF
From: [email protected]
To: [email protected]
Subject: Hello from Outlook
This is an automated email.
EOF
```
### send_email_via_mailgun_api
Send email using Mailgun free tier (5,000 emails/month, no credit card required for sandbox).
```bash
# Set your Mailgun credentials
MAILGUN_API_KEY="your-mailgun-api-key"
MAILGUN_DOMAIN="sandbox123.mailgun.org" # or your verified domain
# Send email
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="Sender Name <mailgun@${MAILGUN_DOMAIN}>" \
-F to="[email protected]" \
-F subject="Hello from Mailgun" \
-F text="This is the plain text body" \
-F html="<h1>HTML Email</h1><p>This is the HTML body</p>"
# Send with attachment
curl -s --user "api:${MAILGUN_API_KEY}" \
"https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages" \
-F from="notifications@${MAILGUN_DOMAIN}" \
-F to="[email protected]" \
-F subject="Report Attached" \
-F text="Please find the report attached." \
-F attachment=@./report.pdf
```
**Node.js:**
```javascript
async function sendEmailMailgun(options) {
const { apiKey, domain, from, to, subject, text, html } = options;
const formData = new URLSearchParams({
from,
to,
subject,
text: text || '',
html: html || ''
});
const auth = 'Basic ' + Buffer.from(`api:${apiKey}`).toString('base64');
const res = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, {
method: 'POST',
headers: {
'Authorization': auth,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Mailgun API error: ${error}`);
}
return await res.json();
}
// Usage
// sendEmailMailgun({
// apiKey: 'your-mailgun-api-key',
// domain: 'sandbox123.mailgun.org',
// from: 'Sender <[email protected]>',
// to: '[email protected]',
// subject: 'Test Email',
// text: 'Plain text body',
// html: '<h1>HTML body</h1>'
// }).then(result => console.log('Email sent:', result));
```
### send_email_via_sendgrid_api
Send email using SendGrid free tier (100 emails/day).
```bash
# Set your SendGrid API key
SENDGRID_API_KEY="your-sendgrid-api-key"
# Send email
curl -s --request POST \
--url "https://api.sendgrid.com/v3/mail/send" \
--header "Authorization: Bearer ${SENDGRID_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"personalizations": [{
"to": [{"email": "[email protected]"}],
"subject": "Hello from SendGrid"
}],
"from": {"email": "[email protected]", "name": "Sender Name"},
"content": [{
"type": "text/plain",
"value": "This is the email body."
}]
}'
# Send HTML email with attachment
curl -s --request POST \
--url "https://api.sendgrid.com/v3/mail/send" \
--header "Authorization: Bearer ${SENDGRID_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"personalizations": [{
"to": [{"email": "[email protected]"}]
}],
"from": {"email": "[email protected]"},
"subject": "Weekly Report",
"content": [{
"type": "text/html",
"value": "<h1>Weekly Report</h1><p>Attached is your report.</p>"
}],
"attachments": [{
"content": "'"$(base64 -w 0 report.pdf)"'",
"filename": "report.pdf",
"type": "application/pdf"
}]
}'
```
**Node.js:**
```javascript
async function sendEmailSendGrid(options) {
const { apiKey, from, to, subject, text, html, attachments = [] } = options;
const payload = {
personalizations: [{
to: [{ email: to }],
subject
}],
from: { email: from },
content: [{
type: html ? 'text/html' : 'text/plain',
value: html || text
}]
};
if (attachments.length > 0) {
payload.attachments = attachments.map(att => ({
content: att.content, // base64 string
filename: att.filename,
type: att.type || 'application/octet-stream'
}));
}
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const error = await res.text();
throw new Error(`SendGrid API error: ${error}`);
}
return { success: true, status: res.status };
}
// Usage
// sendEmailSendGrid({
// apiKey: 'your-sendgrid-api-key',
// from: '[email protected]',
// to: '[email protected]',
// subject: 'Test Email',
// html: '<h1>Hello</h1><p>This is a test.</p>'
// }).then(result => console.log('Email sent:', result));
```
### send_email_via_msmtp
Send email using msmtp (lightweight SMTP client, good for automation).
```bash
# Configure msmtp (one-time setup)
cat > ~/.msmtprc <<EOF
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.msmtp.log
# Gmail account
account gmail
host smtp.gmail.com
port 587
from [email protected]
user [email protected]
password your-app-password
# Set default account
account default : gmail
EOF
chmod 600 ~/.msmtprc
# Send email
echo -e "Subject: Test Email\nFrom: [email protected]\nTo: [email protected]\n\nThis is the email body." | msmtp [email protected]
# Send email with file content
cat report.txt | msmtp -t <<EOF
To: [email protected]
From: [email protected]
Subject: Daily Report
$(cat report.txt)
EOF
# Send HTML email
msmtp [email protected] <<EOF
To: [email protected]
From: [email protected]
Subject: HTML Email
Content-Type: text/html
<html>
<body>
<h1>Hello</h1>
<p>This is an HTML email.</p>
</body>
</html>
EOF
```
### send_email_nodejs_nodemailer
Send email using Node.js nodemailer library (supports all SMTP servers).
**Node.js:**
```javascript
const nodemailer = require('nodemailer');
async function sendEmail(config) {
const {
smtpHost,
smtpPort,
smtpUser,
smtpPassword,
from,
to,
subject,
text,
html,
attachments = []
} = config;
// Create transporter
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465, // true for 465, false for other ports
auth: {
user: smtpUser,
pass: smtpPassword
},
connectionTimeout: 10000
});
// Send email
const info = await transporter.sendMail({
from,
to,
subject,
text,
html,
attachments: attachments.map(att => ({
filename: att.filename,
path: att.path || undefined,
content: att.content || undefined
}))
});
return {
success: true,
messageId: info.messageId,
response: info.response
};
}
// Usage - Gmail
// sendEmail({
// smtpHost: 'smtp.gmail.com',
// smtpPort: 587,
// smtpUser: '[email protected]',
// smtpPassword: 'your-app-password',
// from: '"Sender Name" <[email protected]>',
// to: '[email protected]',
// subject: 'Hello',
// text: 'Plain text body',
// html: '<b>HTML body</b>',
// attachments: [
// { filename: 'report.pdf', path: './report.pdf' }
// ]
// }).then(result => console.log('Email sent:', result));
// Usage - Outlook
// sendEmail({
// smtpHost: 'smtp.office365.com',
// smtpPort: 587,
// smtpUser: '[email protected]',
// smtpPassword: 'your-password',
// from: '[email protected]',
// to: '[email protected]',
// subject: 'Test',
// text: 'This is a test email'
// });
```
### advanced_email_with_retry
Production-ready email sending with retry logic and error handling.
```bash
#!/bin/bash
send_email_with_retry() {
local TO="$1"
local SUBJECT="$2"
local BODY="$3"
local MAX_RETRIES=3
local RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do
if curl -fsS --max-time 30 \
--url "smtp://smtp.gmail.com:587" \
--mail-from "[email protected]" \
--mail-rcpt "$TO" \
--user "[email protected]:app-password" \
--upload-file - <<EOF
From: [email protected]
To: $TO
Subject: $SUBJECT
$BODY
EOF
then
echo "Email sent successfully to $TO"
return 0
else
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..." >&2
sleep $RETRY_DELAY
fi
done
echo "Failed to send email after $MAX_RETRIES attempts" >&2
return 1
}
# Usage
send_email_with_retry "[email protected]" "Alert" "System CPU usage is high"
```
**Node.js:**
```javascript
async function sendEmailWithRetry(config, maxRetries = 3) {
const { provider, ...emailConfig } = config;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
let result;
if (provider === 'mailgun') {
result = await sendEmailMailgun(emailConfig);
} else if (provider === 'sendgrid') {
result = await sendEmailSendGrid(emailConfig);
} else {
throw new Error(`Unknown provider: ${provider}`);
}
return { success: true, attempt, result };
} catch (err) {
console.error(`Attempt ${attempt} failed:`, err.message);
if (attempt === maxRetries) {
throw new Error(`Failed to send email after ${maxRetries} attempts: ${err.message}`);
}
// Exponential backoff
const delayMs = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
// Usage
// sendEmailWithRetry({
// provider: 'mailgun',
// apiKey: 'your-api-key',
// domain: 'sandbox123.mailgun.org',
// from: '[email protected]',
// to: '[email protected]',
// subject: 'Critical Alert',
// text: 'System requires attention'
// }, 3).then(result => console.log('Email sent:', result));
```
## Rate limits / Best practices
- ✅ **Use app passwords** — For Gmail/Outlook, create app-specific passwords instead of account passwords
- ✅ **Free tier limits** — Mailgun: 5,000/month, SendGrid: 100/day, Gmail: 500/day
- ✅ **Retry logic** — Implement exponential backoff for failed sends
- ✅ **Error handling** — Catch and log errors, don't expose credentials in logs
- ✅ **Validate emails** — Check recipient email format before sending
- ✅ **SPF/DKIM** — Configure DNS records for custom domains to avoid spam
- ⚠️ **Never hardcode credentials** — Use environment variables or secure vaults
- ⚠️ **Rate limiting** — Add delays between bulk sends (1-2 seconds)
- ⚠️ **Bounce handling** — Monitor bounces and remove invalid addresses
## Agent prompt
```text
You have email sending capability via SMTP and free APIs. When a user asks to send an email:
1. Choose the best method based on requirements:
- **curl + SMTP** — For simple emails with Gmail/Outlook (requires app password)
- **Mailgun API** — For higher volume (5,000/month free, requires API key)
- **SendGrid API** — For moderate volume (100/day free, requires API key)
- **msmtp** — For automated scripts and cron jobs
- **nodemailer** — For Node.js applications with full SMTP support
2. For Gmail/Outlook SMTP:
- Gmail: smtp.gmail.com:587 (requires app password from Google Account settings)
- Outlook: smtp.office365.com:587
- Always use app passwords, never account passwords
3. For Mailgun:
- Free sandbox domain: 5,000 emails/month to authorized recipients
- Verified domain: Unlimited recipients (within free tier limits)
- No credit card required for sandbox
4. For SendGrid:
- Free tier: 100 emails/day
- Requires account signup and API key
5. Always:
- Validate recipient email format
- Use environment variables for credentials
- Implement retry logic (3 attempts with exponential backoff)
- Handle errors gracefully with clear messages
- Never log credentials or sensitive data
6. Security:
- Store SMTP passwords in ~/.msmtprc (chmod 600) or environment variables
- Use TLS/SSL for all SMTP connections
- Validate and sanitize email content to prevent injection
```
## Troubleshooting
**Error: "authentication failed" (Gmail)**
- Symptom: SMTP auth fails with Gmail account password
- Solution: Generate an app password at https://myaccount.google.com/apppasswords (requires 2FA enabled)
**Error: "530 5.7.0 Must issue a STARTTLS command first"**
- Symptom: Server requires TLS encryption
- Solution: Add `--ssl-reqd` to curl command or use port 587/465
**Error: "554 5.7.1 Relay access denied"**
- Symptom: SMTP server rejects email relay
- Solution: Authenticate with correct credentials, ensure FROM email matches authenticated account
**Mailgun: "Free accounts are for test purposes only"**
- Symptom: Mailgun sandbox restricts recipients
- Solution: Add authorized recipients in Mailgun dashboard, or verify a custom domain
**SendGrid: "403 Forbidden"**
- Symptom: API key lacks permissions
- Solution: Create new API key with "Mail Send" permissions in SendGrid dashboard
**Emails going to spam:**
- Symptom: Recipients don't receive emails or they're in spam folder
- Solution: Configure SPF, DKIM, DMARC DNS records for custom domains; use verified sender emails
**Timeout errors:**
- Symptom: SMTP connection hangs or times out
- Solution: Check firewall allows outbound connections on ports 587/465; increase timeout to 30 seconds
## See also
- [../using-telegram-bot/SKILL.md](../using-telegram-bot/SKILL.md) — Alternative notification method via Telegram
- [../nostr-logging-system/SKILL.md](../nostr-logging-system/SKILL.md) — Decentralized logging alternative
- [../user-aks-for-report/SKILL.md](../user-aks-for-report/SKILL.md) — Generate reports that can be emailed
```
### ../using-telegram-bot/SKILL.md
```markdown
---
name: using-telegram-bot
description: Build and run Telegram bots in Node.js using Telegraf with practical command patterns.
---
# Telegram (Telegraf) Skill — Node.js
Short guide to build Telegram bots with `telegraf` (Node.js).
## Overview
- Library: https://github.com/telegraf/telegraf
- Install: `npm install telegraf`
- Get a bot token from BotFather and store it in `BOT_TOKEN`.
## Minimal polling bot
```javascript
// bot.js
const { Telegraf, Markup } = require('telegraf');
const bot = new Telegraf(process.env.BOT_TOKEN);
bot.start(ctx => ctx.reply('Welcome! I can help with commands.'));
bot.command('echo', ctx => {
const text = ctx.message.text.split(' ').slice(1).join(' ');
ctx.reply(text || 'usage: /echo your message');
});
bot.on('text', ctx => ctx.reply(`You said: ${ctx.message.text}`));
bot.launch();
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));
```
Run:
```bash
BOT_TOKEN=123:ABC node bot.js
```
## Send media and files
```javascript
// send photo
await ctx.replyWithPhoto('https://example.com/image.jpg', { caption: 'Nice pic' });
// send document
await ctx.replyWithDocument('https://example.com/file.pdf');
```
## Inline keyboards and callbacks
```javascript
// show inline buttons
await ctx.reply('Choose:', Markup.inlineKeyboard([
Markup.button.callback('OK', 'ok'),
Markup.button.callback('Cancel', 'cancel')
]));
bot.action('ok', ctx => ctx.reply('You pressed OK'));
bot.action('cancel', ctx => ctx.reply('Cancelled'));
```
## Webhook (Express) example
```javascript
const express = require('express');
const { Telegraf } = require('telegraf');
const bot = new Telegraf(process.env.BOT_TOKEN);
const app = express();
app.use(bot.webhookCallback('/telegraf'));
bot.telegram.setWebhook(`${process.env.PUBLIC_URL}/telegraf`);
app.listen(process.env.PORT || 3000);
```
Use webhooks for production deployments (faster, lower resource use).
## Error handling
```javascript
bot.catch((err, ctx) => {
console.error('Bot error', err);
});
```
## Tips
- Use environment variables for tokens and URLs.
- Respect Telegram rate limits (avoid flooding large groups).
- For local testing, use polling; for deployment use webhooks behind HTTPS.
- Add `NODE_ENV=production` and graceful shutdown hooks for reliability.
---
This doc shows the most common Telegraf patterns: start/command handlers, text handlers, media, inline buttons, webhook setup, and error handling.
```