◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-05-03Intermediate

Automate Contact Form Handling with Gemini API — Classification, Priority Scoring & Slack Alerts

Build a Python system that automatically classifies incoming contact form submissions using Gemini API, scores their priority, and sends structured Slack notifications — ready to deploy today.

gemini-api286python108automation55slack3structured-output28

If you run a web service or app solo, you already know the feeling: a batch of contact form submissions hits your inbox, and half your morning disappears sorting through them.

Is this a bug? A billing question? A feature request that's been asked five times before? Each message takes just a few minutes to triage — but those minutes add up fast when you're the only one handling support.

Gemini API changes that. The Python system below reads incoming inquiries, asks Gemini to classify them and assign a priority score, then posts a structured notification to Slack. No specialized infrastructure required.

How the System Works

The flow is straightforward:

  1. A contact form submission comes in (via email, webhook, or any trigger)
  2. Python sends the content to Gemini API
  3. Gemini returns a structured JSON with category, priority, summary, and suggested response time
  4. The result gets posted to your Slack channel with color-coded formatting

This works with whatever form tool you're using — Google Forms, Typeform, a custom contact page — as long as you can extract the subject and body text.

Prerequisites

Install the required packages:

pip install google-genai requests

Set your environment variables:

export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

Get your Gemini API key from Google AI Studio for free. For a refresher on getting started with the API, see Gemini API Quickstart. For Slack, create an Incoming Webhooks app in your workspace settings and copy the webhook URL for the channel you want notifications sent to.

Classifying Inquiries with Gemini API

Here's the core function that sends inquiry content to Gemini and gets back structured classification data:

import os
import json
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
def classify_inquiry(subject: str, body: str) -> dict:
    """
    Classify an inquiry and return a priority score.
 
    Returns:
        {
          "category": str,
          "priority": int (1-5),
          "priority_reason": str,
          "summary": str,
          "suggested_response_time": str
        }
    """
    prompt = f"""
Analyze the following support inquiry.
 
Subject: {subject}
Body: {body}
 
Return a JSON object with this exact structure:
{{
  "category": "bug_report | feature_request | usage_question | billing | other",
  "priority": integer from 1 to 5 (5 = highest urgency),
  "priority_reason": "one sentence explaining the priority score",
  "summary": "inquiry summary in 10 words or fewer",
  "suggested_response_time": "immediate | within 24 hours | within 3 business days | within 1 week"
}}
 
Priority guidelines:
- 5: Service-wide outage, data loss risk, security issue
- 4: Specific feature broken, billing problem
- 3: Performance issue, partial feature malfunction
- 2: Feature request, improvement suggestion
- 1: General usage question
"""
 
    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            response_mime_type="application/json",
            temperature=0.1,  # low temperature for consistent classification
        ),
    )
 
    return json.loads(response.text)

A note on temperature=0.1: classification tasks benefit from consistency over creativity. You want the same type of message to always get the same category. In practice, temperature=0 can occasionally prepend stray characters before the JSON — 0.1 has been more reliable in my testing.

Setting response_mime_type="application/json" tells Gemini to return raw JSON rather than wrapping it in a markdown code block. Without this, you'd need to strip the ```json delimiters before parsing. For a deeper look at structured output patterns, see the Gemini Structured Output Guide.

Sending Priority-Aware Slack Notifications

This function formats the classification result into a color-coded Slack attachment and sends it:

import requests
from datetime import datetime
 
def send_slack_notification(inquiry_data: dict, classification: dict) -> bool:
    """
    Post classification results to Slack with priority-aware formatting.
 
    Returns:
        True on success, raises on error
    """
    category_emoji = {
        "bug_report": "🐛",
        "feature_request": "✨",
        "usage_question": "❓",
        "billing": "💳",
        "other": "📝",
    }
 
    priority_colors = {
        5: "#FF0000",  # red — urgent
        4: "#FF6600",  # orange — high
        3: "#FFCC00",  # yellow — medium
        2: "#00CC00",  # green — low
        1: "#999999",  # gray — minimal
    }
 
    category = classification.get("category", "other")
    priority = classification.get("priority", 1)
    emoji = category_emoji.get(category, "📝")
    color = priority_colors.get(priority, "#999999")
 
    # Escalate header text for high-priority items
    if priority >= 5:
        header = "🚨 <!channel> URGENT: Immediate response needed"
    elif priority >= 4:
        header = f"⚠️ High-priority inquiry {emoji}"
    else:
        header = f"{emoji} New inquiry (priority {priority}/5)"
 
    payload = {
        "text": header,
        "attachments": [
            {
                "color": color,
                "fields": [
                    {
                        "title": "Summary",
                        "value": classification.get("summary", ""),
                        "short": False,
                    },
                    {
                        "title": "Category",
                        "value": category,
                        "short": True,
                    },
                    {
                        "title": "Priority",
                        "value": f"{'⭐' * priority} ({priority}/5)",
                        "short": True,
                    },
                    {
                        "title": "Suggested Response Time",
                        "value": classification.get("suggested_response_time", ""),
                        "short": True,
                    },
                    {
                        "title": "Reasoning",
                        "value": classification.get("priority_reason", ""),
                        "short": False,
                    },
                    {
                        "title": "From",
                        "value": inquiry_data.get("email", "unknown"),
                        "short": True,
                    },
                ],
                "footer": f"Received: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}",
            }
        ],
    }
 
    response = requests.post(
        os.environ["SLACK_WEBHOOK_URL"],
        json=payload,
        timeout=10,
    )
    response.raise_for_status()
    return True

Putting It All Together

The main handler ties classification and notification together, with proper error handling for production use:

def handle_inquiry(inquiry_data: dict) -> dict:
    """
    Accept raw inquiry data, classify it, and send a Slack notification.
 
    inquiry_data fields:
        subject: email subject or form field title
        body: full message content
        email: sender's email address
        name: sender's name (optional)
    """
    classification = None
 
    try:
        # Step 1: classify with Gemini
        classification = classify_inquiry(
            subject=inquiry_data.get("subject", ""),
            body=inquiry_data.get("body", ""),
        )
 
        # Step 2: notify Slack
        send_slack_notification(inquiry_data, classification)
 
        return {"status": "success", "classification": classification}
 
    except json.JSONDecodeError as e:
        # Gemini returned non-JSON — notify Slack with a fallback classification
        print(f"JSON parse error: {e}")
        fallback = {
            "category": "other",
            "priority": 3,
            "priority_reason": "Auto-classification failed — manual review needed",
            "summary": inquiry_data.get("subject", "")[:50],
            "suggested_response_time": "within 24 hours",
        }
        send_slack_notification(inquiry_data, fallback)
        return {"status": "fallback", "error": str(e)}
 
    except requests.exceptions.RequestException as e:
        # Classification succeeded but Slack failed
        print(f"Slack notification failed: {e}")
        return {
            "status": "classified_but_notify_failed",
            "classification": classification,
        }
 
    except Exception as e:
        print(f"Unexpected error: {e}")
        raise
 
# Quick test
if __name__ == "__main__":
    test_inquiry = {
        "subject": "Can't log in since this morning",
        "body": "I was using the app fine yesterday but now I'm getting an authentication error every time I try to log in. I haven't changed my password.",
        "email": "user@example.com",
        "name": "Jane Smith",
    }
 
    result = handle_inquiry(test_inquiry)
    print(result["classification"])
    # Expected: category=bug_report, priority=4, suggested_response_time=within 24 hours

What I've Learned Running This in Production

A few things stood out after running this for a couple of months:

The prompt wording matters more than you'd expect. The specificity of your priority guidelines directly affects classification consistency. Vague criteria like "high priority = urgent" produce inconsistent results. Concrete examples — "priority 5 = service-wide outage or data loss risk" — give Gemini a clear reference point.

Emotionally charged messages get slightly elevated priority. A furious bug report tends to score slightly higher than a calm one describing the same issue. This is actually reasonable behavior: a visibly frustrated user probably needs a faster response regardless of technical severity.

The cost is negligible. With Gemini 2.5 Flash, each classification request costs well under $0.01. For most indie projects handling under a few hundred inquiries per month, this is effectively free. For error handling strategies that keep costs predictable, see Gemini API Error Handling Guide.

Connecting It to Google Forms

If you use Google Forms, the simplest approach is to set up a Flask or FastAPI webhook endpoint and call it from an Apps Script trigger on form submission:

from flask import Flask, request, jsonify
 
app = Flask(__name__)
 
@app.route("/webhook/inquiry", methods=["POST"])
def inquiry_webhook():
    data = request.get_json()
    if not data:
        return jsonify({"error": "Invalid JSON"}), 400
    result = handle_inquiry(data)
    return jsonify(result)
 
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Deploy this to Cloud Run and it only runs when a request comes in — no always-on server costs. Railway is another solid option if you prefer minimal configuration.

Next Steps

The biggest practical win from this system isn't speed — it's catching things that would have slipped through the cracks. Urgent bugs that arrive at 2am now surface in Slack instead of waiting until morning.

Start by running the test script with a few real inquiries from your existing backlog. Adjust the category definitions and priority thresholds to match your service's reality, then hook it up to your form. The full working code is above — copy, configure your environment variables, and you're ready to go.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-05-17
Auto-generating Japanese and English Release Notes from git log with Gemini API — A Real Implementation from Beautiful HD Wallpapers v2.1.0
I realized I was spending 1–2 hours per release writing notes in multiple languages. Here's how I automated that with Gemini API and git log — tested on Beautiful HD Wallpapers v2.1.0, with code you can run today.
◈ API / SDK2026-05-16
Automating Firebase Crashlytics Analysis with Gemini API — A Real-World Implementation from an Indie App
A real-world implementation record of automating Firebase Crashlytics log analysis with Gemini API, validated during development of an indie wallpaper app (v2.1.0). Includes Before/After code for a RecyclerView crash fix and a production cost breakdown.
◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.