GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/Workspace
Workspace/2026-04-09Advanced

Google Forms × Gemini API: Automated Response Analysis — Sentiment Analysis, Theme Extraction & Report Generation

Learn how to connect Google Forms with the Gemini API to automatically perform sentiment analysis, theme extraction, and report generation on survey responses. Includes complete Apps Script and Python implementation code you can deploy today.

google-formsgemini-api277apps-script10automation51sentiment-analysis2report-generationgoogle-workspace7

Setup and context: How Forms × Gemini API Transforms Survey Analysis

Google Forms is one of the most widely used survey tools in the world — collecting responses for marketing research, interview feedback, customer satisfaction surveys, employee evaluations, and more. But what happens after the responses come in?

Multiple-choice answers are easy to tally with formulas. Open-ended responses, however, typically require someone to read through each one manually, categorize them by hand, and piece together a narrative. When you're dealing with hundreds of responses, that analysis can consume an entire workday.

This is exactly where the Gemini API changes the equation. Gemini's powerful natural language understanding can analyze free-text responses in real time — extracting sentiment, identifying recurring themes, and generating structured insight reports automatically.

As an indie developer, I rely on open-ended survey responses to decide what to fix next across the apps and sites I run at Dolice. Collecting the answers was never the hard part — finding the hours to actually read them was, which meant the analysis kept slipping to the bottom of the list. That is the gap this setup closes: by the time the responses land, the first pass is already done. The rest of this guide builds that already-processed state with Forms and the Gemini API, following code you can run as-is.

In this guide, you'll build a complete system that connects Google Forms responses to the Gemini API for automated analysis. We'll cover two implementation approaches: Apps Script (great for most teams) and Python (for large-scale or custom deployments) — with full, working code for both.


System Architecture and Technology Choices

Overview of the Full Pipeline

Here's the end-to-end flow of the system you'll build:

Google Forms (response collection)
    ↓ Trigger (on submit or scheduled)
Google Sheets (response storage)
    ↓ Apps Script / Python
Gemini API (natural language analysis)
    ↓ Structured results
Google Sheets Analysis Tab / Gmail Summary Report

Choosing Your Implementation Approach

Apps Script (Recommended for most teams)

  • Pros: Native Google Workspace integration, no server needed, OAuth handled automatically
  • Cons: 6-minute execution time limit, not ideal for complex data processing pipelines
  • Best for: Up to ~500 responses, team collaboration, quick deployment

Python (For larger deployments)

  • Pros: No execution time limit, integrates with pandas and data science libraries, CI/CD friendly
  • Cons: Requires Google Sheets API setup and a runtime environment
  • Best for: High-volume responses, existing data infrastructure, scheduled batch processing

Step 1: Obtaining and Configuring Your Gemini API Key

You'll need a Gemini API key from Google AI Studio to get started.

Getting Your API Key

Visit Google AI Studio, sign in with your Google account, and click "Get API key" to generate a key for your project.

For Apps Script usage, store the key securely in Script Properties — never hardcode it directly in your script.

// Run this function once in the Apps Script editor to store your key
function setApiKey() {
  PropertiesService.getScriptProperties().setProperty(
    'GEMINI_API_KEY',
    'YOUR_GEMINI_API_KEY'  // Replace with your actual key
  );
  Logger.log('API key saved successfully');
}

Which Model Should You Use?

We recommend Gemini 2.5 Flash for this system.

  • Why: Best balance of analysis accuracy and cost efficiency
  • Cost estimate: ~$0.075 per million input tokens (as of April 2026)
  • Per-analysis cost: For 50 responses averaging 200 characters each, roughly $0.002

Step 2: Core Apps Script Implementation

Connecting Forms to Sheets

In Google Forms, go to the "Responses" tab and click "View in Sheets" to create a spreadsheet that automatically collects all responses. This sheet becomes the input for our analysis system.

Main Analysis Script

Paste the following into the Apps Script editor (open via Extensions → Apps Script from your spreadsheet):

// ============================================================
// Google Forms × Gemini API Response Analysis System
// ============================================================
 
const GEMINI_API_KEY = PropertiesService.getScriptProperties()
  .getProperty('GEMINI_API_KEY');
const GEMINI_API_URL = 
  'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
 
// Configure sheet names and target columns
const CONFIG = {
  sourceSheetName: 'Form Responses 1',  // Default name created by Forms
  analysisSheetName: 'Analysis Results',
  targetColumns: [3, 4, 5],  // Column numbers (1-indexed) for open-ended fields
  batchSize: 10,  // Number of responses to send to Gemini at once
};
 
/**
 * Main function — processes unanalyzed responses
 */
function analyzeFormResponses() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sourceSheet = ss.getSheetByName(CONFIG.sourceSheetName);
  const analysisSheet = getOrCreateAnalysisSheet(ss);
  
  const lastSourceRow = sourceSheet.getLastRow();
  const lastAnalysisRow = analysisSheet.getLastRow();
  const startRow = Math.max(2, lastAnalysisRow + 1); // Skip header
  
  if (startRow > lastSourceRow) {
    Logger.log('No new responses to analyze');
    return;
  }
  
  Logger.log(`Analyzing rows ${startRow} to ${lastSourceRow}`);
  
  // Process in batches
  for (let row = startRow; row <= lastSourceRow; row += CONFIG.batchSize) {
    const endRow = Math.min(row + CONFIG.batchSize - 1, lastSourceRow);
    const batch = [];
    
    for (let r = row; r <= endRow; r++) {
      const texts = CONFIG.targetColumns.map(col => 
        sourceSheet.getRange(r, col).getValue()
      ).filter(text => text && text.toString().trim().length > 0);
      
      if (texts.length > 0) {
        batch.push({ row: r, texts: texts });
      }
    }
    
    if (batch.length > 0) {
      const results = analyzeBatch(batch);
      writeAnalysisResults(analysisSheet, results);
    }
    
    // Respect API rate limits
    Utilities.sleep(1000);
  }
  
  Logger.log('Analysis complete!');
}
 
/**
 * Send a batch of responses to Gemini for analysis
 */
function analyzeBatch(batch) {
  const prompt = buildAnalysisPrompt(batch);
  
  const payload = {
    contents: [{
      parts: [{ text: prompt }]
    }],
    generationConfig: {
      temperature: 0.1,  // Low temperature for consistent analysis
      responseMimeType: 'application/json',
    }
  };
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };
  
  const url = `${GEMINI_API_URL}?key=${GEMINI_API_KEY}`;
  const response = UrlFetchApp.fetch(url, options);
  
  if (response.getResponseCode() !== 200) {
    Logger.log('API error: ' + response.getContentText());
    return batch.map(b => ({ row: b.row, error: 'API Error' }));
  }
  
  const responseData = JSON.parse(response.getContentText());
  const resultText = responseData.candidates[0].content.parts[0].text;
  
  try {
    return JSON.parse(resultText);
  } catch (e) {
    Logger.log('JSON parse error: ' + resultText);
    return batch.map(b => ({ row: b.row, error: 'Parse Error' }));
  }
}
 
/**
 * Build the analysis prompt for a batch of responses
 */
function buildAnalysisPrompt(batch) {
  const responses = batch.map((b, i) => 
    `Response ID: ${b.row}\nContent: ${b.texts.join(' / ')}`
  ).join('\n\n');
  
  return `
Analyze the following open-ended survey responses and return a JSON object.
 
${responses}
 
Return results in this exact format:
{
  "results": [
    {
      "row": <response ID as number>,
      "sentiment": "Positive" or "Negative" or "Neutral",
      "sentiment_score": <number from -1.0 to 1.0>,
      "main_themes": ["theme1", "theme2"],
      "key_insight": "The most important insight from this response (under 100 characters)",
      "action_suggestion": "A specific, actionable improvement suggestion (under 100 characters)"
    }
  ]
}
 
Guidelines:
- Base sentiment on the overall tone of the full response
- main_themes should capture key concepts (maximum 3)
- key_insight should be concrete and immediately useful
- action_suggestion should describe something a team could realistically act on
`;
}
 
/**
 * Write analysis results back to the spreadsheet
 */
function writeAnalysisResults(sheet, results) {
  if (!results.results) return;
  
  results.results.forEach(result => {
    if (result.error) return;
    
    const outputRow = sheet.getLastRow() + 1;
    
    sheet.getRange(outputRow, 1).setValue(result.row);
    sheet.getRange(outputRow, 2).setValue(result.sentiment);
    sheet.getRange(outputRow, 3).setValue(result.sentiment_score);
    sheet.getRange(outputRow, 4).setValue(
      Array.isArray(result.main_themes) ? result.main_themes.join(', ') : ''
    );
    sheet.getRange(outputRow, 5).setValue(result.key_insight);
    sheet.getRange(outputRow, 6).setValue(result.action_suggestion);
    sheet.getRange(outputRow, 7).setValue(new Date());
    
    // Color-code rows by sentiment
    const color = result.sentiment === 'Positive' ? '#e8f5e9' :
                  result.sentiment === 'Negative' ? '#ffebee' : '#f5f5f5';
    sheet.getRange(outputRow, 1, 1, 7).setBackground(color);
  });
  
  SpreadsheetApp.flush();
}
 
/**
 * Get or create the analysis results sheet
 */
function getOrCreateAnalysisSheet(ss) {
  let sheet = ss.getSheetByName(CONFIG.analysisSheetName);
  
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.analysisSheetName);
    const headers = [
      'Source Row', 'Sentiment', 'Score', 'Themes',
      'Key Insight', 'Action Suggestion', 'Analyzed At'
    ];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length)
      .setBackground('#1a73e8')
      .setFontColor('#ffffff')
      .setFontWeight('bold');
    sheet.setFrozenRows(1);
  }
  
  return sheet;
}

Step 3: Automated Summary Report Generation

Beyond individual response analysis, you need an aggregate view to identify patterns across all responses. Here's how to generate an AI-powered summary report:

/**
 * Generate a summary report from all analysis results
 */
function generateSummaryReport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const analysisSheet = ss.getSheetByName(CONFIG.analysisSheetName);
  
  if (!analysisSheet || analysisSheet.getLastRow() < 2) {
    Logger.log('Insufficient analysis data');
    return;
  }
  
  const data = analysisSheet.getDataRange().getValues();
  const rows = data.slice(1);
  
  const total = rows.length;
  const sentimentCounts = { 'Positive': 0, 'Negative': 0, 'Neutral': 0 };
  const allThemes = [];
  const allInsights = [];
  
  rows.forEach(row => {
    const sentiment = row[1];
    if (sentimentCounts.hasOwnProperty(sentiment)) sentimentCounts[sentiment]++;
    if (row[3]) allThemes.push(...row[3].split(', '));
    if (row[4]) allInsights.push(row[4]);
  });
  
  const themeCounts = {};
  allThemes.forEach(t => { themeCounts[t] = (themeCounts[t] || 0) + 1; });
  const topThemes = Object.entries(themeCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([theme, count]) => `${theme} (${count})`)
    .join(', ');
  
  const summaryPrompt = `
You are a business analyst summarizing survey data for executives and team leads.
 
Data:
- Total responses: ${total}
- Sentiment distribution: Positive ${sentimentCounts['Positive']} (${Math.round(sentimentCounts['Positive']/total*100)}%), Negative ${sentimentCounts['Negative']} (${Math.round(sentimentCounts['Negative']/total*100)}%), Neutral ${sentimentCounts['Neutral']} (${Math.round(sentimentCounts['Neutral']/total*100)}%)
- Top themes: ${topThemes}
- Sample insights (up to 20): ${allInsights.slice(0, 20).join(' | ')}
 
Write a concise, actionable summary report with these sections:
1. Executive Summary (3 sentences)
2. Key Findings (3–5 bullet points)
3. Top Issues from Negative Responses (3 specific items)
4. Recommended Actions (3 concrete, implementable suggestions)
 
Write for a decision-maker who needs to act quickly. Be specific, not generic.
`;
  
  const summaryResult = callGeminiAPI(summaryPrompt);
  
  let summarySheet = ss.getSheetByName('Summary Report');
  if (!summarySheet) {
    summarySheet = ss.insertSheet('Summary Report');
  } else {
    summarySheet.clear();
  }
  
  const timestamp = Utilities.formatDate(new Date(), 'GMT', "MMMM d, yyyy 'at' HH:mm 'UTC'");
  summarySheet.getRange('A1').setValue(`Survey Analysis Report — ${timestamp}`);
  summarySheet.getRange('A1').setFontSize(16).setFontWeight('bold');
  summarySheet.getRange('A2').setValue(summaryResult);
  summarySheet.getRange('A2').setWrap(true);
  summarySheet.setColumnWidth(1, 600);
  
  return summaryResult;
}
 
/**
 * Utility: Call Gemini API with a text prompt
 */
function callGeminiAPI(prompt) {
  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: { temperature: 0.3 }
  };
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };
  
  const response = UrlFetchApp.fetch(
    `${GEMINI_API_URL}?key=${GEMINI_API_KEY}`, options
  );
  
  if (response.getResponseCode() !== 200) {
    throw new Error('Gemini API Error: ' + response.getResponseCode());
  }
  
  const data = JSON.parse(response.getContentText());
  return data.candidates[0].content.parts[0].text;
}

Step 4: Automated Email Delivery via Gmail

Send the generated report to your team automatically:

/**
 * Email the summary report to stakeholders
 */
function sendReportEmail() {
  const summary = generateSummaryReport();
  
  const recipients = ['manager@example.com', 'team@example.com'];
  const subject = `[Gemini Analysis] Survey Report — ${
    Utilities.formatDate(new Date(), 'GMT', 'MMMM d, yyyy')
  }`;
  
  const htmlBody = `
    <div style="font-family: Arial, sans-serif; max-width: 700px; margin: 0 auto;">
      <h2 style="color: #1a73e8; border-bottom: 2px solid #1a73e8; padding-bottom: 8px;">
        Survey Analysis Report (Gemini AI)
      </h2>
      <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; white-space: pre-wrap;">
        ${summary.replace(/\n/g, '<br>')}
      </div>
      <p style="color: #666; font-size: 12px; margin-top: 16px;">
        This report was generated automatically using the Gemini API.
        View the full dataset in your 
        <a href="${SpreadsheetApp.getActiveSpreadsheet().getUrl()}">Google Sheet</a>.
      </p>
    </div>
  `;
  
  recipients.forEach(recipient => {
    GmailApp.sendEmail(recipient, subject, summary, {
      htmlBody: htmlBody,
      name: 'Gemini Analysis System'
    });
  });
  
  Logger.log(`Report sent to: ${recipients.join(', ')}`);
}

Step 5: Python Implementation for Large-Scale Deployments

When you're dealing with hundreds or thousands of responses, or when you need to integrate with existing data pipelines, Python is the better choice. Here's a complete implementation:

Environment Setup

pip install google-generativeai google-auth google-auth-oauthlib \
            google-auth-httplib2 google-api-python-client \
            pandas openpyxl python-dotenv

Full Python Implementation

"""
Google Forms × Gemini API Response Analysis System (Python)
"""
import os
import json
import time
from datetime import datetime
import pandas as pd
from dotenv import load_dotenv
import google.generativeai as genai
from googleapiclient.discovery import build
from google.oauth2.service_account import Credentials
 
load_dotenv()
 
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
SPREADSHEET_ID = os.getenv('SPREADSHEET_ID')
SERVICE_ACCOUNT_FILE = 'service_account.json'
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
 
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel('gemini-2.5-flash')
 
def get_sheets_service():
    """Initialize Google Sheets API client"""
    creds = Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES
    )
    return build('sheets', 'v4', credentials=creds)
 
def fetch_form_responses(service: object, sheet_name: str = 'Form Responses 1') -> pd.DataFrame:
    """Fetch all form responses from Google Sheets"""
    result = service.spreadsheets().values().get(
        spreadsheetId=SPREADSHEET_ID,
        range=f"'{sheet_name}'!A:Z"
    ).execute()
    
    values = result.get('values', [])
    if not values:
        return pd.DataFrame()
    
    df = pd.DataFrame(values[1:], columns=values[0])
    print(f"📊 Fetched {len(df)} responses")
    return df
 
def analyze_with_gemini(texts: list[str]) -> dict:
    """Analyze a list of response texts with Gemini"""
    if not texts:
        return {}
    
    combined = ' / '.join([t for t in texts if t.strip()])
    
    prompt = f"""
Analyze this survey response and return a JSON object only (no markdown).
 
Response: {combined}
 
Return format:
{{
  "sentiment": "Positive" or "Negative" or "Neutral",
  "sentiment_score": <float from -1.0 to 1.0>,
  "main_themes": ["theme1", "theme2"],
  "key_insight": "Key insight under 100 characters",
  "action_suggestion": "Specific action under 100 characters",
  "urgency": "High" or "Medium" or "Low"
}}
"""
    
    try:
        response = model.generate_content(
            prompt,
            generation_config=genai.GenerationConfig(
                temperature=0.1,
                response_mime_type='application/json'
            )
        )
        return json.loads(response.text)
    except Exception as e:
        print(f"❌ Analysis error: {e}")
        return {
            'sentiment': 'Neutral', 'sentiment_score': 0.0,
            'main_themes': [], 'key_insight': 'Error during analysis',
            'action_suggestion': 'Manual review required', 'urgency': 'Low'
        }
 
def analyze_all_responses(df: pd.DataFrame, text_columns: list[str]) -> pd.DataFrame:
    """Run Gemini analysis on all responses"""
    results = []
    total = len(df)
    
    for i, (idx, row) in enumerate(df.iterrows()):
        print(f"⏳ Processing: {i+1}/{total}...")
        
        texts = [str(row[col]) for col in text_columns 
                 if col in df.columns and str(row[col]).strip()]
        
        analysis = analyze_with_gemini(texts)
        
        results.append({
            'original_index': idx,
            **{col: row[col] for col in df.columns},
            'sentiment': analysis.get('sentiment', ''),
            'sentiment_score': analysis.get('sentiment_score', 0),
            'main_themes': ', '.join(analysis.get('main_themes', [])),
            'key_insight': analysis.get('key_insight', ''),
            'action_suggestion': analysis.get('action_suggestion', ''),
            'urgency': analysis.get('urgency', 'Low'),
            'analyzed_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        })
        
        time.sleep(1)  # Stay within 60 RPM limit
    
    return pd.DataFrame(results)
 
def generate_summary_report(results_df: pd.DataFrame) -> str:
    """Generate an executive summary report using Gemini"""
    total = len(results_df)
    sentiment_dist = results_df['sentiment'].value_counts().to_dict()
    top_themes = (
        results_df['main_themes']
        .str.split(', ').explode()
        .value_counts().head(10)
    )
    urgent_count = len(results_df[results_df['urgency'] == 'High'])
    insights_sample = results_df['key_insight'].dropna().head(20).tolist()
    
    prompt = f"""
You are a business analyst preparing an executive summary of survey results.
 
Data:
- Total responses: {total}
- Sentiment breakdown: {sentiment_dist}
- High-urgency responses: {urgent_count}
- Top themes: {dict(top_themes)}
- Sample insights: {insights_sample[:10]}
 
Write a concise executive summary with:
1. Three-sentence overview
2. Key findings (3–5 points)
3. Priority issues (from high-urgency responses)
4. Recommended actions (3 concrete suggestions)
 
Be specific and actionable. This goes directly to decision-makers.
"""
    
    response = model.generate_content(prompt)
    return response.text
 
def upload_results_to_sheets(service, results_df: pd.DataFrame):
    """Upload analysis results back to Google Sheets"""
    headers = results_df.columns.tolist()
    values = [headers] + results_df.values.tolist()
    
    service.spreadsheets().values().clear(
        spreadsheetId=SPREADSHEET_ID,
        range='Analysis Results!A:Z'
    ).execute()
    
    service.spreadsheets().values().update(
        spreadsheetId=SPREADSHEET_ID,
        range='Analysis Results!A1',
        valueInputOption='RAW',
        body={'values': [[str(v) for v in row] for row in values]}
    ).execute()
    
    print(f"✅ Uploaded {len(results_df)} results to Google Sheets")
 
def main():
    print("🚀 Google Forms × Gemini API Analysis System Starting")
    
    service = get_sheets_service()
    df = fetch_form_responses(service)
    
    if df.empty:
        print("⚠️ No response data found")
        return
    
    # Set the column names for your open-ended questions
    text_columns = ['What did you like?', 'What could be improved?', 'Other comments']
    
    results_df = analyze_all_responses(df, text_columns)
    
    print("\n📝 Generating summary report...")
    summary = generate_summary_report(results_df)
    print("\n=== Summary Report ===")
    print(summary)
    
    upload_results_to_sheets(service, results_df)
    
    output_file = f"analysis_report_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
    results_df.to_excel(output_file, index=False)
    print(f"💾 Excel report saved: {output_file}")
 
if __name__ == '__main__':
    main()

Step 6: Scheduling and Automation

Apps Script Trigger Setup

/**
 * Set up all triggers — run this function once manually
 */
function setupTriggers() {
  // Remove existing triggers first
  ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t));
  
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // Analyze in real-time when the spreadsheet changes (new form submission)
  ScriptApp.newTrigger('analyzeFormResponses')
    .forSpreadsheet(ss)
    .onChange()
    .create();
  
  // Send daily email report at 9 AM
  ScriptApp.newTrigger('sendReportEmail')
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
  
  Logger.log('Triggers configured successfully');
}

Python Scheduling (cron or Cloud Scheduler)

# Add to crontab (runs at 8 AM daily)
0 8 * * * cd /path/to/project && python3 main.py >> logs/analysis.log 2>&1
 
# Google Cloud Scheduler + Cloud Run
gcloud scheduler jobs create http forms-analysis-job \
  --schedule="0 8 * * *" \
  --uri="https://YOUR_CLOUD_RUN_URL/analyze" \
  --time-zone="America/New_York" \
  --http-method=POST

Step 7: Visualizing Results in Looker Studio

Once your analysis results are flowing into Google Sheets, connect them to Looker Studio for a live dashboard.

In Looker Studio (lookerstudio.google.com), click "Add data source" → "Google Sheets" → select your analysis results sheet.

Recommended chart types

  • Sentiment distribution → Donut chart
  • Sentiment score over time → Line chart (shows trends)
  • Top themes by frequency → Bar chart
  • High-urgency responses → Table with Urgency = "High" filter

This gives your stakeholders a self-serve dashboard they can explore without needing to read through raw data.


Summary: Transforming Survey Operations with Forms × Gemini API

This guide walked you through building a complete automated survey analysis system — from collecting responses in Google Forms through to generating actionable insight reports with the Gemini API. You now have working code in both Apps Script and Python that you can deploy immediately.

The impact of this system is tangible: teams that once spent a full day manually reading through 200+ open-ended responses can now get a structured, AI-generated insight report within minutes of the survey closing.

As next steps, consider extending the system with response clustering (K-means grouping of similar themes), longitudinal trend analysis comparing sentiment across multiple survey cycles, or integrating with other Google Workspace tools like Gmail and Docs to automatically generate targeted follow-up action plans for each department.

Gemini API and Google Workspace together open up a remarkably broad range of automation possibilities. We hope this guide gives you a solid foundation to start transforming how your team works with survey data.

To go further with the approaches in this guide, a few related articles pair naturally with what you've built here. If you want to take your Sheets integration further, Building an AI Automated Report Pipeline with Gemini × Google Sheets Apps Script covers advanced pipeline patterns. For visualizing your analysis results in Looker Studio, Google Looker Studio × Gemini API: Business Report AI Analysis Guide walks through the full setup. And if you're ready to extend automation across Gmail, Calendar, and Drive, Gemini API × Apps Script: Cross-Workspace AI Workflow Guide shows you how to tie it all together.


Advanced Use Case: Multi-Form Aggregation and Comparative Analysis

Once your basic analysis pipeline is running, the real power comes from comparing results across multiple surveys — tracking how sentiment changes over time, or comparing feedback from different customer segments.

Aggregating Multiple Forms

If you run recurring surveys (monthly NPS, quarterly employee engagement, weekly pulse checks), you'll want to analyze trends across all of them. Here's how to build a cross-form aggregation layer:

/**
 * Aggregate analysis results from multiple survey sheets
 * Useful for tracking sentiment trends across time periods
 */
function aggregateMultipleSurveys() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // Define all survey sheets to aggregate
  const surveySheets = [
    { name: 'Q1 2026 Analysis', period: '2026-Q1' },
    { name: 'Q2 2026 Analysis', period: '2026-Q2' },
    { name: 'Q3 2026 Analysis', period: '2026-Q3' },
  ];
  
  const aggregatedData = [];
  
  surveySheets.forEach(config => {
    const sheet = ss.getSheetByName(config.name);
    if (!sheet) return;
    
    const data = sheet.getDataRange().getValues();
    const rows = data.slice(1); // Skip header
    
    const periodData = {
      period: config.period,
      total: rows.length,
      avgSentimentScore: 0,
      sentimentBreakdown: { Positive: 0, Negative: 0, Neutral: 0 },
      topThemes: {},
    };
    
    rows.forEach(row => {
      const score = parseFloat(row[2]) || 0;
      periodData.avgSentimentScore += score;
      
      const sentiment = row[1];
      if (periodData.sentimentBreakdown.hasOwnProperty(sentiment)) {
        periodData.sentimentBreakdown[sentiment]++;
      }
      
      if (row[3]) {
        row[3].split(', ').forEach(theme => {
          periodData.topThemes[theme] = (periodData.topThemes[theme] || 0) + 1;
        });
      }
    });
    
    if (rows.length > 0) {
      periodData.avgSentimentScore = 
        (periodData.avgSentimentScore / rows.length).toFixed(3);
    }
    
    aggregatedData.push(periodData);
  });
  
  // Ask Gemini to interpret the trend
  const trendPrompt = `
You are a business analyst interpreting multi-quarter survey trend data.
 
Data across periods:
${JSON.stringify(aggregatedData, null, 2)}
 
Write a trend analysis that covers:
1. Overall sentiment trajectory (improving, declining, or stable?)
2. Which themes are gaining or losing prominence?
3. What does the data suggest about the root causes of any changes?
4. What should the team prioritize in the next quarter based on these trends?
 
Be specific about which numbers support each conclusion.
`;
  
  const trendAnalysis = callGeminiAPI(trendPrompt);
  
  // Write trend analysis to a dedicated sheet
  let trendSheet = ss.getSheetByName('Trend Analysis');
  if (!trendSheet) trendSheet = ss.insertSheet('Trend Analysis');
  else trendSheet.clear();
  
  trendSheet.getRange('A1').setValue('Multi-Period Trend Analysis');
  trendSheet.getRange('A1').setFontSize(16).setFontWeight('bold');
  trendSheet.getRange('A2').setValue(trendAnalysis);
  trendSheet.getRange('A2').setWrap(true);
  trendSheet.setColumnWidth(1, 650);
  
  Logger.log('Trend analysis complete');
  return trendAnalysis;
}

Segment Comparison Analysis

When your survey collects responses from multiple customer segments (enterprise vs. SMB, new vs. returning customers), comparing sentiment and themes across segments surfaces insights that aggregate analysis misses.

/**
 * Compare sentiment across customer segments
 * Assumes a "Segment" column exists in form responses
 */
function compareSegments(segmentColumnIndex = 2) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sourceSheet = ss.getSheetByName(CONFIG.sourceSheetName);
  const analysisSheet = ss.getSheetByName(CONFIG.analysisSheetName);
  
  if (!analysisSheet) {
    Logger.log('Run analyzeFormResponses first');
    return;
  }
  
  // Get segment data from source sheet
  const sourceData = sourceSheet.getDataRange().getValues();
  const analysisData = analysisSheet.getDataRange().getValues();
  
  // Build a map of row -> segment
  const segmentMap = {};
  sourceData.slice(1).forEach((row, i) => {
    segmentMap[i + 2] = row[segmentColumnIndex - 1]; // Adjust for 0-index
  });
  
  // Group analysis results by segment
  const segmentResults = {};
  analysisData.slice(1).forEach(row => {
    const sourceRow = parseInt(row[0]);
    const segment = segmentMap[sourceRow] || 'Unknown';
    
    if (!segmentResults[segment]) {
      segmentResults[segment] = {
        count: 0, totalScore: 0, themes: {}, insights: []
      };
    }
    
    const group = segmentResults[segment];
    group.count++;
    group.totalScore += parseFloat(row[2]) || 0;
    if (row[3]) {
      row[3].split(', ').forEach(t => { group.themes[t] = (group.themes[t] || 0) + 1; });
    }
    if (row[4]) group.insights.push(row[4]);
  });
  
  // Prepare summary for Gemini
  const segmentSummary = Object.entries(segmentResults).map(([seg, data]) => ({
    segment: seg,
    avgSentimentScore: (data.totalScore / data.count).toFixed(2),
    responseCount: data.count,
    topThemes: Object.entries(data.themes)
      .sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k}(${v})`),
    sampleInsights: data.insights.slice(0, 5)
  }));
  
  const comparisonPrompt = `
Compare these customer segments based on survey analysis:
 
${JSON.stringify(segmentSummary, null, 2)}
 
Provide:
1. Which segment has the highest/lowest satisfaction and why?
2. What themes are unique to each segment vs. shared across all?
3. What different actions should the team take for each segment?
4. Are there any surprising differences that warrant deeper investigation?
`;
  
  const comparison = callGeminiAPI(comparisonPrompt);
  Logger.log('Segment comparison:\n' + comparison);
  return comparison;
}

Scaling Considerations and Cost Management

As your survey analysis system grows, keeping costs and performance in check becomes important.

Optimizing Token Usage

The primary cost driver for the Gemini API is the number of input tokens. Here are practical strategies to reduce costs without sacrificing analysis quality.

Response truncation: Most free-text responses are under 500 characters. Responses exceeding 2,000 characters often contain redundant content. Truncate at a sensible limit:

function truncateResponse(text, maxChars = 1500) {
  if (!text || text.length <= maxChars) return text;
  return text.substring(0, maxChars) + '... [truncated]';
}

Batch multiple responses in a single API call: Instead of calling the API once per response, the batch approach shown in Step 2 can handle up to 10–15 responses per call, dramatically reducing the number of API invocations and their associated overhead.

Caching repeated analysis: If your survey has follow-up rounds where some respondents submit the same text (common in NPS surveys where the numeric score drives everything), deduplicate before sending to the API.

Monitoring Quota and Costs

Set up a simple cost tracker in Apps Script:

/**
 * Track cumulative API usage in Script Properties
 */
function trackApiUsage(estimatedTokens) {
  const props = PropertiesService.getScriptProperties();
  const current = parseInt(props.getProperty('totalTokensUsed') || '0');
  props.setProperty('totalTokensUsed', (current + estimatedTokens).toString());
  
  const totalTokens = current + estimatedTokens;
  const estimatedCostUSD = (totalTokens / 1000000) * 0.075; // Flash input price
  
  Logger.log(`Total tokens used this session: ${totalTokens} (~$${estimatedCostUSD.toFixed(4)})`);
}

For large-scale deployments, consider implementing context caching — a Gemini API feature that lets you cache frequently reused prompt prefixes (like your analysis instructions) to avoid re-processing them on every call, reducing costs by up to 75% for repeated prompts.


Integrating with Other Google Workspace Tools

This analysis system pairs naturally with other parts of the Google Workspace ecosystem:

Google Docs: Auto-generate a formatted report document from the summary, complete with charts embedded via the Slides API, for distribution to stakeholders who prefer documents over spreadsheets.

Google Chat: Post a daily or weekly digest of key findings directly to a team space, so the insights reach people where they already work.

Google Calendar: When the analysis flags high-urgency negative feedback, automatically create a calendar event for a review meeting with the relevant team lead.

Google Slides: Convert the summary report into a presentation deck using the Slides API — useful for monthly business reviews where survey results are a standing agenda item.

Each of these extensions uses the same pattern: Apps Script connecting Google APIs with Gemini-generated content as the intelligence layer.

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

Workspace2026-06-29
When Apps Script Time-Driven Triggers Quietly Run Out: Consolidating Gemini Automations into One Dispatcher
Apps Script caps you at 20 triggers per user and a daily total trigger-runtime budget. Here is how to stop your Gemini automations from silently dying as you add more, using a single dispatcher, a schedule table, and an external heartbeat.
Workspace2026-04-09
Google Looker Studio × Gemini API: Automate Business Report Analysis with AI
Connect Looker Studio with the Gemini API to auto-generate AI insights on reports. Includes Apps Script code, daily trigger setup, and troubleshooting tips.
Workspace2026-03-31
Building an AI Presentation Pipeline with Gemini API and Google Slides Apps Script — A
Learn how to build a complete presentation pipeline using Gemini API and Google Apps Script that auto-generates slide outlines, content, and designs from a single topic input.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →