Build domain-specific tools with custom business logic, calculations, validations, and data transformations.
Rent calculations, ROI analysis, market comparisons with your specific formulas
Tenant screening rules, compliance checks, document verification
Format conversion, data enrichment, report generation
Connect to proprietary databases, legacy systems, internal APIs
Every custom tool follows the same pattern: schema definition, execution logic, and error handling.
// lib/tools/custom/tool-template.ts
import { z } from "zod"
import { tool } from "ai"
export const myCustomTool = tool({
// 1. Clear, descriptive name for the AI to understand
description: "What this tool does and when to use it",
// 2. Strongly typed parameters with Zod
parameters: z.object({
requiredParam: z.string().describe("Explanation for AI"),
optionalParam: z.number().optional().describe("Optional value"),
enumParam: z.enum(["option1", "option2"]).describe("Choices"),
}),
// 3. Execution logic with proper error handling
execute: async (params, context) => {
try {
// Your business logic here
const result = await performOperation(params)
// Return structured result
return {
success: true,
data: result,
metadata: {
executedAt: new Date().toISOString(),
duration: Date.now() - startTime,
}
}
} catch (error) {
// Proper error response
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
}
})// lib/tools/custom/rent-calculator.ts
import { z } from "zod"
import { tool } from "ai"
import { neon } from "@neondatabase/serverless"
const sql = neon(process.env.DATABASE_URL!)
export const calculateRentTool = tool({
description: `Calculate optimal rent price for a property based on:
- Market comparables in the area
- Property features and condition
- Current market conditions
- Owner preferences`,
parameters: z.object({
propertyId: z.string().uuid().describe("Property UUID"),
strategy: z.enum([
"market_rate", // Match market average
"premium", // Above market for high-end
"competitive", // Below market for fast fill
"custom" // Manual adjustment
]).describe("Pricing strategy"),
customAdjustment: z.number().min(-30).max(30).optional()
.describe("Custom percentage adjustment (-30% to +30%)"),
includeUtilities: z.boolean().optional()
.describe("Include utilities in rent"),
}),
execute: async ({ propertyId, strategy, customAdjustment, includeUtilities }) => {
// 1. Get property details
const property = await sql`
SELECT * FROM properties WHERE id = ${propertyId}
`
if (!property.length) {
return { success: false, error: "Property not found" }
}
const prop = property[0]
// 2. Get market comparables
const comparables = await sql`
SELECT AVG(rent_amount) as avg_rent,
MIN(rent_amount) as min_rent,
MAX(rent_amount) as max_rent,
COUNT(*) as count
FROM properties
WHERE suburb = ${prop.suburb}
AND bedrooms = ${prop.bedrooms}
AND property_type = ${prop.property_type}
AND id != ${propertyId}
AND status = 'rented'
AND updated_at > NOW() - INTERVAL '6 months'
`
const market = comparables[0]
// 3. Calculate base rent
let baseRent = Number(market.avg_rent) || 500 // Fallback
// 4. Apply property-specific adjustments
const adjustments = []
// Condition adjustment
if (prop.condition === "excellent") {
baseRent *= 1.1
adjustments.push({ factor: "Excellent condition", adjustment: "+10%" })
} else if (prop.condition === "needs_work") {
baseRent *= 0.9
adjustments.push({ factor: "Needs maintenance", adjustment: "-10%" })
}
// Amenities
if (prop.has_parking) {
baseRent += 50
adjustments.push({ factor: "Parking included", adjustment: "+$50" })
}
if (prop.has_pool) {
baseRent += 100
adjustments.push({ factor: "Pool", adjustment: "+$100" })
}
// 5. Apply strategy
let finalRent = baseRent
switch (strategy) {
case "premium":
finalRent = baseRent * 1.15
adjustments.push({ factor: "Premium strategy", adjustment: "+15%" })
break
case "competitive":
finalRent = baseRent * 0.95
adjustments.push({ factor: "Competitive strategy", adjustment: "-5%" })
break
case "custom":
if (customAdjustment) {
finalRent = baseRent * (1 + customAdjustment / 100)
adjustments.push({
factor: "Custom adjustment",
adjustment: `${customAdjustment > 0 ? '+' : ''}${customAdjustment}%`
})
}
break
}
// 6. Add utilities if requested
let utilitiesEstimate = 0
if (includeUtilities) {
utilitiesEstimate = prop.bedrooms * 40 + 80 // Rough estimate
finalRent += utilitiesEstimate
adjustments.push({
factor: "Utilities included",
adjustment: `+$${utilitiesEstimate}`
})
}
// 7. Round to nearest $5
finalRent = Math.round(finalRent / 5) * 5
return {
success: true,
recommendation: {
suggestedRent: finalRent,
weeklyRent: Math.round(finalRent / 4.33),
strategy,
confidence: market.count >= 5 ? "high" : market.count >= 2 ? "medium" : "low",
},
marketAnalysis: {
averageRent: Math.round(Number(market.avg_rent)),
rentRange: {
min: Math.round(Number(market.min_rent)),
max: Math.round(Number(market.max_rent)),
},
comparablesCount: Number(market.count),
suburb: prop.suburb,
},
adjustments,
breakdown: {
baseRent: Math.round(baseRent),
utilitiesEstimate: includeUtilities ? utilitiesEstimate : 0,
finalRent,
}
}
}
})// lib/tools/custom/tenant-screening.ts
import { z } from "zod"
import { tool } from "ai"
export const screenTenantTool = tool({
description: `Screen a tenant application against property requirements.
Checks income ratio, rental history, and references.`,
parameters: z.object({
applicationId: z.string().uuid(),
propertyId: z.string().uuid(),
strictMode: z.boolean().optional()
.describe("Apply stricter screening criteria"),
}),
execute: async ({ applicationId, propertyId, strictMode = false }) => {
// Fetch application and property data
const [application, property] = await Promise.all([
getApplication(applicationId),
getProperty(propertyId),
])
const checks: ScreeningCheck[] = []
let score = 100
// 1. Income-to-rent ratio (standard: 3x, strict: 4x)
const requiredRatio = strictMode ? 4 : 3
const actualRatio = application.monthlyIncome / property.monthlyRent
if (actualRatio >= requiredRatio) {
checks.push({
name: "Income Ratio",
status: "pass",
details: `${actualRatio.toFixed(1)}x rent (required: ${requiredRatio}x)`,
})
} else if (actualRatio >= requiredRatio * 0.8) {
checks.push({
name: "Income Ratio",
status: "warning",
details: `${actualRatio.toFixed(1)}x rent (below ${requiredRatio}x threshold)`,
})
score -= 15
} else {
checks.push({
name: "Income Ratio",
status: "fail",
details: `${actualRatio.toFixed(1)}x rent (required: ${requiredRatio}x)`,
})
score -= 30
}
// 2. Rental history
if (application.rentalHistory.length >= 2) {
const hasEvictions = application.rentalHistory.some(h => h.evicted)
const hasLatePayments = application.rentalHistory.filter(h =>
h.latePayments > 2
).length > 0
if (hasEvictions) {
checks.push({
name: "Rental History",
status: "fail",
details: "Previous eviction on record",
})
score -= 50
} else if (hasLatePayments) {
checks.push({
name: "Rental History",
status: "warning",
details: "Multiple late payments in history",
})
score -= 20
} else {
checks.push({
name: "Rental History",
status: "pass",
details: `${application.rentalHistory.length} positive references`,
})
}
} else {
checks.push({
name: "Rental History",
status: "warning",
details: "Limited rental history available",
})
score -= 10
}
// 3. Employment verification
if (application.employmentVerified) {
const employmentLength = application.employmentMonths
if (employmentLength >= 12) {
checks.push({
name: "Employment",
status: "pass",
details: `Verified, ${employmentLength} months tenure`,
})
} else {
checks.push({
name: "Employment",
status: "warning",
details: `Verified, but only ${employmentLength} months tenure`,
})
score -= 10
}
} else {
checks.push({
name: "Employment",
status: "fail",
details: "Employment not verified",
})
score -= 25
}
// 4. Credit check (if available)
if (application.creditScore) {
if (application.creditScore >= 700) {
checks.push({
name: "Credit Score",
status: "pass",
details: `Score: ${application.creditScore} (Excellent)`,
})
} else if (application.creditScore >= 600) {
checks.push({
name: "Credit Score",
status: "warning",
details: `Score: ${application.creditScore} (Fair)`,
})
score -= 15
} else {
checks.push({
name: "Credit Score",
status: "fail",
details: `Score: ${application.creditScore} (Poor)`,
})
score -= 30
}
}
// Determine recommendation
let recommendation: "approve" | "review" | "decline"
if (score >= 80) {
recommendation = "approve"
} else if (score >= 50) {
recommendation = "review"
} else {
recommendation = "decline"
}
return {
success: true,
screening: {
score: Math.max(0, score),
recommendation,
checks,
requiresHumanReview: recommendation === "review",
flags: checks.filter(c => c.status === "fail").map(c => c.name),
},
applicant: {
name: application.name,
incomeRatio: actualRatio.toFixed(1),
rentalHistoryCount: application.rentalHistory.length,
}
}
}
}).describe() to every parameter