Small and mid-sized businesses are drowning in repetitive work that off-the-shelf software doesn't fully solve. They know AI should help. They don't know how to make it work for their specific processes.
That gap is a business opportunity, and Gemini 2.5 Pro's Function Calling is the technical foundation for addressing it at scale.
This article documents the full process: understanding what Function Calling actually enables, designing a multi-tenant B2B SaaS around it, pricing it correctly, and closing the first clients.
What Function Calling Changes About Business Automation
Standard Gemini responses are text in, text out. Function Calling breaks that constraint by letting Gemini decide when to call external functions — your inventory system, your CRM, your ERP — and synthesize the results.
import google.generativeai as genai
import json
from datetime import datetime
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# Define what functions Gemini can call
tools = [{
"function_declarations": [
{
"name": "check_inventory",
"description": "Get current stock level and reorder status for a product",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"include_forecast": {"type": "boolean"}
},
"required": ["product_id"]
}
},
{
"name": "create_purchase_order",
"description": "Submit a purchase order to the procurement system",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"quantity": {"type": "integer"},
"supplier_id": {"type": "string"},
"requested_delivery": {"type": "string"}
},
"required": ["product_id", "quantity", "supplier_id"]
}
}
]
}]
def check_inventory(product_id: str, include_forecast: bool = False) -> dict:
# In production: call your actual inventory API
return {
"product_id": product_id,
"current_stock": 45,
"reorder_point": 30,
"daily_consumption_avg": 8.5,
"days_remaining": 5,
"pending_orders": 0
}
def create_purchase_order(product_id: str, quantity: int,
supplier_id: str, requested_delivery: str = None) -> dict:
order_id = f"PO-{datetime.now().strftime('%Y%m%d%H%M%S')}"
# In production: submit to your procurement system
return {
"order_id": order_id,
"status": "submitted",
"quantity": quantity,
"estimated_delivery": requested_delivery or "2026-05-12"
}
def run_procurement_automation(instruction: str) -> dict:
"""Execute multi-step procurement automation via natural language"""
model = genai.GenerativeModel("gemini-2.5-pro-preview-05-06", tools=tools)
chat = model.start_chat()
response = chat.send_message(instruction)
actions = []
for _ in range(10): # Prevent infinite loops
part = response.candidates[0].content.parts[0]
if not hasattr(part, 'function_call') or not part.function_call.name:
break
fc = part.function_call
handlers = {
"check_inventory": check_inventory,
"create_purchase_order": create_purchase_order
}
result = handlers.get(fc.name, lambda **k: {"error": "unknown"})(**dict(fc.args))
actions.append({"function": fc.name, "args": dict(fc.args), "result": result})
response = chat.send_message(
genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name=fc.name,
response={"result": json.dumps(result)}
)
)
)
return {"result": response.text, "actions": actions}
# A manager types a natural language instruction
result = run_procurement_automation(
"Check SKU-00123 inventory. If we're at risk of stockout within 7 days, "
"automatically place a replenishment order with supplier SUP-001."
)
print(result["result"])
# "SKU-00123 has 45 units with 5 days remaining at current consumption.
# I've submitted purchase order PO-20260505143022 for 100 units from
# SUP-001, expected delivery May 12."
print(f"Actions taken: {len(result['actions'])}")
# "Actions taken: 2"This is the core pattern: natural language instruction → Gemini decides which functions to call and in what order → structured results → synthesis into human-readable output.
Identifying Business Problems Worth Solving
Technical capability is not the constraint. Knowing which problems to solve is.
The most reliable method: read the operations manuals of your target industry. Dense, step-by-step manuals signal automatable processes. No manual means no standardized process, which means harder automation.
High-value automation patterns by industry:
Manufacturing & Wholesale:
- Inventory monitoring + auto-procurement (connects stock data to ordering)
- Quote generation (materials + labor + markup → PDF quote)
- Quality inspection report aggregation + anomaly detection
Real Estate:
- Showing request coordination (availability check + confirmation)
- Multi-portal listing sync (update one source, propagate everywhere)
- Rent collection follow-up (overdue detection → graduated outreach)
Professional Services (Lawyers, Accountants, HR Firms):
- Client inquiry categorization + routing to right staff member
- Invoice generation + delivery (time tracking → billing)
- Monthly report automation (data pull → analysis → formatted PDF)
The pattern that consistently leads to contracts: multi-step processes that currently require a human to bridge two or more systems. If someone copies data from System A to System B and then makes a judgment call, that's your opportunity.
The Automation Engine Architecture
Production B2B automation needs error handling, logging, and retry logic built in from day one. Demos that work once don't count.
# core/automation_engine.py
import google.generativeai as genai
from typing import Any, Callable
import logging
import time
from functools import wraps
logger = logging.getLogger(__name__)
def with_exponential_retry(max_retries: int = 3, base_delay: float = 1.0):
"""Retry decorator with exponential backoff for external API calls"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
logger.error(f"{func.__name__} failed after {max_retries} attempts: {e}")
raise
delay = base_delay * (2 ** attempt)
logger.warning(f"{func.__name__} attempt {attempt+1} failed, retry in {delay}s: {e}")
time.sleep(delay)
return wrapper
return decorator
class AutomationEngine:
"""Production-ready Gemini Function Calling engine"""
def __init__(self, api_key: str, tools: list, handlers: dict[str, Callable],
model: str = "gemini-2.5-pro-preview-05-06"):
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel(model, tools=tools)
self.handlers = handlers
self.max_function_calls = 15 # Safety limit
def execute(self, instruction: str, context: dict = None) -> dict:
"""
Execute an automation task from natural language instruction
Returns: {
"success": bool,
"result": str,
"actions_taken": list,
"error": str | None
}
"""
try:
return self._execute_with_tracking(instruction, context)
except Exception as e:
logger.error(f"Automation failed: {e}", exc_info=True)
return {"success": False, "result": "", "actions_taken": [], "error": str(e)}
def _execute_with_tracking(self, instruction: str, context: dict = None) -> dict:
context_addition = f"\n\nContext: {context}" if context else ""
chat = self.model.start_chat()
response = chat.send_message(instruction + context_addition)
actions_taken = []
call_count = 0
while call_count < self.max_function_calls:
call_count += 1
parts = response.candidates[0].content.parts
if not parts or not hasattr(parts[0], 'function_call') or not parts[0].function_call.name:
break
fc = parts[0].function_call
handler = self.handlers.get(fc.name)
if not handler:
result = {"error": f"Handler for '{fc.name}' not registered"}
logger.warning(result["error"])
else:
result = handler(**dict(fc.args))
actions_taken.append({
"function": fc.name,
"args": dict(fc.args),
"result": result,
"timestamp": time.time()
})
import json
response = chat.send_message(
genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name=fc.name,
response={"result": json.dumps(result, default=str)}
)
)
)
final_text = "".join(
p.text for p in response.candidates[0].content.parts
if hasattr(p, 'text')
)
return {
"success": True,
"result": final_text,
"actions_taken": actions_taken,
"error": None
}Multi-Tenant SaaS Design and Pricing
Serving multiple clients from the same codebase requires tenant isolation and usage tracking for billing.
# core/tenant.py
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import uuid
PLANS = {
"starter": {
"monthly_automations": 500,
"price_jpy": 50_000,
"price_usd": 350,
"max_integrations": 3
},
"growth": {
"monthly_automations": 3_000,
"price_jpy": 150_000,
"price_usd": 1_000,
"max_integrations": 10
},
"enterprise": {
"monthly_automations": -1, # Unlimited
"price_jpy": 400_000,
"price_usd": 2_700,
"max_integrations": -1
}
}
@dataclass
class Tenant:
company_name: str
plan: str
tenant_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
enabled_integrations: list[str] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
class UsageTracker:
"""Tracks automation usage per tenant per billing period"""
def __init__(self):
self._usage: dict[str, int] = {}
def _key(self, tenant_id: str) -> str:
return f"{tenant_id}:{datetime.now().strftime('%Y-%m')}"
def can_run(self, tenant_id: str, plan: str) -> bool:
limit = PLANS[plan]["monthly_automations"]
if limit == -1:
return True # Unlimited
current = self._usage.get(self._key(tenant_id), 0)
return current < limit
def record_run(self, tenant_id: str) -> int:
key = self._key(tenant_id)
self._usage[key] = self._usage.get(key, 0) + 1
return self._usage[key]
def monthly_invoice(self, tenant: Tenant) -> dict:
runs = self._usage.get(self._key(tenant.tenant_id), 0)
plan = PLANS[tenant.plan]
limit = plan["monthly_automations"]
overage = max(0, runs - limit) if limit != -1 else 0
overage_charge = overage * 15 # ¥15 per overage automation
return {
"tenant": tenant.company_name,
"plan": tenant.plan,
"base_price_jpy": plan["price_jpy"],
"automations_run": runs,
"overage_automations": overage,
"overage_charge_jpy": overage_charge,
"total_jpy": plan["price_jpy"] + overage_charge
}Three Mistakes That Kill B2B Automation Businesses
Mistake 1: No idempotency in destructive operations
A duplicate purchase order or double-sent invoice is a client relationship disaster. Every function that creates or modifies records must implement idempotency:
def create_order_idempotent(product_id: str, quantity: int,
supplier_id: str, idempotency_key: str) -> dict:
"""Never create the same order twice"""
existing = db.find_by_idempotency_key(idempotency_key)
if existing:
return {"status": "already_exists", "order_id": existing.id}
order = db.create_order(product_id, quantity, supplier_id, idempotency_key)
return {"status": "created", "order_id": order.id}Mistake 2: Underestimating token costs at scale
Gemini 2.5 Pro at 3,000 complex automations per month can easily hit 30–50M tokens. Calculate your costs before setting plan prices, not after.
def estimate_monthly_token_cost(
avg_tokens_per_automation: int,
automations_per_month: int,
input_price_per_m: float = 1.25, # Gemini 2.5 Pro pricing
output_price_per_m: float = 10.0
) -> float:
input_tokens = avg_tokens_per_automation * 0.6 * automations_per_month
output_tokens = avg_tokens_per_automation * 0.4 * automations_per_month
cost = (input_tokens / 1_000_000 * input_price_per_m +
output_tokens / 1_000_000 * output_price_per_m)
return round(cost, 2)
# Growth plan: 3,000 automations at ~8,000 tokens each
cost = estimate_monthly_token_cost(8000, 3000)
print(f"${cost}") # ~$37.80 — safely covered by $1,000/month planMistake 3: Selling the technology instead of the outcome
"AI-powered automation" closes fewer deals than "eliminate the 3 hours/day your team spends on X."
The Sales Process That Closes in 3 Weeks
Week 1: The Ops Audit Call
Frame it as: "I'm researching how [industry] businesses handle [specific process]. Would you spend 30 minutes walking me through how you currently do it?"
You're looking for: steps involving manual data transfer, judgment calls based on simple rules, and recurring processes that take more than 2 hours/week.
Week 2: The Demo
Build a working PoC for the single most painful process you identified. Show it live. Don't over-engineer — functional beats polished at this stage.
Week 3: The ROI Proposal
ROI Calculation Template:
Current State:
Staff performing task: 2 people × 3 hours/day × 20 working days
Monthly labor cost: 2 × 3 × 20 × [hourly rate] = ¥360,000
After Automation:
Staff oversight: 15 minutes/day review
Monthly labor cost: ¥12,500
Monthly savings: ¥347,500
System cost: ¥150,000/month (Growth Plan)
Net monthly benefit: ¥197,500
Payback period (including ¥300,000 setup): 1.5 months
Most decision-makers at SMBs understand this math intuitively. When the system costs ¥150K and saves ¥347K, objections about price evaporate.
The path to ¥3M MRR in 6 months requires roughly 10 Growth plan clients or 7-8 Enterprise clients. Neither is unrealistic if you focus on a single vertical and build deep expertise in their specific workflows. Pick your industry, build your first PoC this week.