Automation

One Form. Five Automations. Zero Manual Work.

Systemic Analysis
One Form. Five Automations. Zero Manual Work.

Every school website has the same problem: content sits in someone’s head instead of on the page. Teachers don’t publish because publishing is too hard — wrong folder, wrong filename format, missing image, forgotten date.

The answer isn’t a fancier editor. It’s removing the editor entirely and replacing it with something every teacher already knows: a Google Form.

Why a Form Beats a CMS

A typical “school CMS” introduces friction at every step: log in, find the right section, remember the format, upload the image separately, hit publish, pray it didn’t break. Teachers avoid it. Content dies in inboxes.

A Google Form removes all of that. Five fields. Thirty seconds. Done.

The best editorial interface is one that doesn’t look like an editorial interface.

It’s familiar by default. Every teacher has filled in a Google Form. No training, no documentation, no onboarding session.

It works from a phone. Teacher photographs the school concert, opens the form on their way to the car, types a few keywords — the post is live before they get home.

It enforces structure. No risk of missing a date, misformatting a title, or uploading to the wrong folder. The form fields are the contract between the teacher and the system.

It’s controlled and audited. Restrict to your domain. Every submission carries a timestamp and an author. Full history, zero overhead.

The Pipeline: What Happens After “Submit”

The teacher sees a form and then sees an effect on the school website. Between those two moments, Apps Script runs a five-step pipeline — invisible, automatic, and fault-tolerant.

Google Form
    ↓ trigger: onFormSubmit
Apps Script

┌───────────────────┬────────────────────┐
│                   │                    │
Gemini API      Google Drive         Google Calendar
(expands notes) (creates Doc)       (creates event)
│                   │
└───────────────────┘

    Web App CMS

    Google Sites

The teacher does one thing. The system does five.

The Form: Five Fields, No Instructions Needed

The form has two categories of fields. Content fields flow into the published document and the website. Operational fields control how Apps Script routes the submission — they never appear in the final article.

Content fields:

  • Title — short answer. Example: “Family Christmas Concert.”
  • Date — date picker. If the news isn’t tied to a specific event, the teacher enters the publication date.
  • Type — dropdown: CONCERT, INFO, PROJECT, SPORT, RECRUITMENT. Becomes a badge on the website.
  • Keywords / description — paragraph. The teacher writes the essence: “Grades 4–6 performed carols. Parents and guests attended. Venue: Youth Palace.” They don’t need to write polished prose — Gemini will expand it.
  • Photos — file upload, images only, multiple files allowed.

Operational fields (add based on your school’s needs): target classes, priority, publication channels.

Step-by-Step: What the Script Does

Step 1 — Read the form response

Apps Script catches the onFormSubmit event and reads each field in order using responses[N].getResponse(). The date is formatted to YYYY-MM-DD — the format the Web App CMS expects in the filename.

Step 2 — Gemini expands the keywords

The teacher’s bullet points are sent to Gemini 2.5 Flash with a strict prompt: write 4–5 sentences, neutral informational tone, no invented facts. The original keywords are preserved at the bottom of the document as metadata — so you always see what the human wrote versus what the AI expanded.

If Gemini is unavailable (rate limit, outage), the original description becomes the article text. The pipeline never blocks on the AI step.

Step 3 — Create a structured Google Doc

A new document is created in the CMS folder with the filename YYYY-MM-DD | Title. Apps Script inserts: H1 heading, type badge (bold, coloured), Gemini text, scaled photos (max 5, max 500px wide), and the original keywords as 9pt grey metadata at the bottom. The file is shared as “anyone with link can view.”

Step 4 — Add to the school calendar

An all-day calendar event is created with the event type tag and a link to the document. Administrators, teachers, and parents can subscribe to the calendar and see every published news item automatically.

Step 5 — Invalidate the CMS cache

The script cache key used by the Web App is cleared. The next page load fetches a fresh list of documents — the new post appears without any manual refresh or republish action.

The Complete Script

Attach this to your Google Form via Script Editor (⋮ → Script editor). Store your Gemini API key in Project Settings → Script Properties as GEMINI_API_KEY.

// ===== CONFIG =====
const CONFIG = {
  FOLDER_ID:    "PASTE_YOUR_NEWS_FOLDER_ID",
  CALENDAR_ID:  "PASTE_YOUR_CALENDAR_ID",
  FONT_FAMILY:  "Lato",
  MAX_IMAGES:   5,
  MAX_IMG_WIDTH: 500,
  CACHE_KEY:    "NEWS_HTML"
};

// ===== TRIGGER =====
function onFormSubmit(e) {
  const responses = e.response.getItemResponses();
  const title    = responses[0].getResponse();
  const dateStr  = responses[1].getResponse();
  const type     = responses[2].getResponse();
  const keywords = responses[3].getResponse();
  const fileIds  = responses[4] ? responses[4].getResponse() : null;

  const date    = new Date(dateStr);
  const isoDate = Utilities.formatDate(date, "Europe/Warsaw", "yyyy-MM-dd");

  let newsText = generateNewsText(title, type, keywords);
  if (!newsText) newsText = keywords;

  const docUrl = createNewsDoc(title, isoDate, type, newsText, keywords, fileIds);
  createCalendarEvent(title, date, type, docUrl);
  clearCmsCache();
}

// ===== GEMINI =====
function generateNewsText(title, type, keywords) {
  try {
    const apiKey = PropertiesService.getScriptProperties()
      .getProperty("GEMINI_API_KEY");
    if (!apiKey) return "";

    const prompt =
      "You are a school website editor. Based on the teacher's notes, " +
      "write a polished news post. 4-5 sentences, neutral informational " +
      "tone, understandable for parents. Do not invent facts.\n\n" +
      "Title: " + title + "\nEvent type: " + type +
      "\nTeacher's notes: " + keywords;

    const url =
      "https://generativelanguage.googleapis.com" +
      "/v1beta/models/gemini-2.5-flash:generateContent?key=" + apiKey;

    const payload = {
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: {
        maxOutputTokens: 1024,
        thinkingConfig: { thinkingBudget: 0 }
      }
    };

    const res = UrlFetchApp.fetch(url, {
      method: "post",
      contentType: "application/json",
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    });

    const json = JSON.parse(res.getContentText());
    if (json.candidates?.[0]) {
      return json.candidates[0].content.parts[0].text.trim();
    }
  } catch (err) {
    Logger.log("Gemini error: " + err);
  }
  return "";
}

// ===== DOCUMENT =====
function createNewsDoc(title, isoDate, type, newsText, keywords, fileIds) {
  const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
  const fileName = isoDate + " | " + title;
  const doc  = DocumentApp.create(fileName);
  const body = doc.getBody();

  const h1 = body.getParagraphs()[0];
  h1.setHeading(DocumentApp.ParagraphHeading.HEADING1);
  h1.setText(title);
  h1.setFontFamily(CONFIG.FONT_FAMILY);

  if (type) {
    const badge = body.appendParagraph(type.toUpperCase());
    badge.setBold(true).setForegroundColor("#1a73e8").setFontFamily(CONFIG.FONT_FAMILY);
  }

  body.appendParagraph("");
  body.appendParagraph(newsText).setFontFamily(CONFIG.FONT_FAMILY);

  if (fileIds?.length > 0) {
    body.appendParagraph("");
    const limit = Math.min(fileIds.length, CONFIG.MAX_IMAGES);
    for (let i = 0; i < limit; i++) {
      try {
        const blob  = DriveApp.getFileById(fileIds[i]).getBlob();
        const image = body.appendImage(blob);
        const w = image.getWidth();
        if (w > CONFIG.MAX_IMG_WIDTH) {
          const ratio = CONFIG.MAX_IMG_WIDTH / w;
          image.setWidth(CONFIG.MAX_IMG_WIDTH);
          image.setHeight(Math.round(image.getHeight() * ratio));
        }
        body.appendParagraph("");
      } catch (err) { Logger.log(err); }
    }
    if (fileIds.length > CONFIG.MAX_IMAGES) {
      const info = body.appendParagraph(
        "Remaining photos (" + (fileIds.length - CONFIG.MAX_IMAGES) + ") available in Drive folder."
      );
      info.setFontSize(9).setForegroundColor("#999999").setItalic(true);
    }
  }

  const kw = body.appendParagraph("Keywords: " + keywords);
  kw.setFontSize(9).setForegroundColor("#999999").setItalic(true).setFontFamily(CONFIG.FONT_FAMILY);

  const file = DriveApp.getFileById(doc.getId());
  folder.addFile(file);
  DriveApp.getRootFolder().removeFile(file);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

  return doc.getUrl();
}

// ===== CALENDAR =====
function createCalendarEvent(title, date, type, docUrl) {
  try {
    const cal = CalendarApp.getCalendarById(CONFIG.CALENDAR_ID);
    if (!cal) { Logger.log("Calendar not found"); return; }
    const event = cal.createAllDayEvent(
      (type ? "[" + type + "] " : "") + title, date
    );
    event.setDescription("School news\n\nDocument:\n" + docUrl);
  } catch (err) {
    Logger.log("Calendar error: " + err);
  }
}

// ===== CACHE =====
function clearCmsCache() {
  try {
    CacheService.getScriptCache().remove(CONFIG.CACHE_KEY);
  } catch (err) {
    Logger.log("Cache clear skipped: " + err);
  }
}

Deployment Checklist

Google Form setup:

  • Create a new Google Form restricted to your school domain
  • Add field: Title (short answer, required)
  • Add field: Date (date picker, required)
  • Add field: Type (dropdown: CONCERT, INFO, PROJECT, SPORT, RECRUITMENT)
  • Add field: Keywords / description (paragraph, required)
  • Add field: Photos (file upload, images only, allow multiple)

Apps Script setup:

  • Open Script Editor (⋮ → Script editor) and paste the full script
  • Update CONFIG.FOLDER_ID with your CMS folder ID (from the Drive URL)
  • Update CONFIG.CALENDAR_ID with your school calendar ID
  • Add GEMINI_API_KEY in Project Settings → Script Properties

Triggers and testing:

  • Set trigger: clock icon → Add trigger → onFormSubmit → On form submit
  • Submit a test response and verify: Doc appears in folder, calendar event created, page updates

The Interactive Version

The widget below walks through the full pipeline step by step and lets you explore the code in context.

📋
Google Form
⚙️
Apps Script
🧠
Gemini API
📄
Google Docs
📅
Calendar + Sites
📋
1. Teacher fills the form
5 fields · 30 seconds

The form has two categories of fields. Content fields (title, date, type, keywords, photos) flow into the published document. Operational fields (target classes, priority) control routing. Teachers never touch a folder, filename, or Docs template.

⚙️
2. onFormSubmit trigger fires
Event caught · pipeline starts

Apps Script catches the form submit event and reads each field in order using responses[N].getResponse(). The date is formatted to YYYY-MM-DD — the format the CMS Web App expects in the filename. Each downstream step is independent: if one fails, the others continue.

Apps Script
function onFormSubmit(e) {
  const responses = e.response.getItemResponses();
  const title    = responses[0].getResponse();
  const dateStr  = responses[1].getResponse();
  const type     = responses[2].getResponse();
  const keywords = responses[3].getResponse();
  const fileIds  = responses[4]
                   ? responses[4].getResponse() : null;
  const isoDate = Utilities.formatDate(
    new Date(dateStr), "Europe/Warsaw", "yyyy-MM-dd"
  );
  // → hand off to next steps
}
🧠
3. Gemini expands the keywords
Bullet points → 4–5 sentence paragraph

The teacher's notes are sent to Gemini 2.5 Flash with a strict prompt: neutral tone, no invented facts, 4–5 sentences. thinkingBudget: 0 ensures all tokens go to the article text. If Gemini is unavailable (rate limit, outage), the original keywords become the article. The pipeline never blocks on the AI step.

Apps Script
const prompt =
  "You are a school website editor. Write a polished " +
  "news post. 4-5 sentences, neutral tone, " +
  "understandable for parents. No invented facts.\n\n" +
  "Title: " + title + "\nType: " + type +
  "\nNotes: " + keywords;

const payload = {
  contents: [{ parts: [{ text: prompt }] }],
  generationConfig: {
    maxOutputTokens: 1024,
    thinkingConfig: { thinkingBudget: 0 }
  }
};
📄
4. Structured document created
Filename · H1 · badge · text · photos · metadata

A new Doc is created in the CMS folder with filename YYYY-MM-DD | Title. Apps Script inserts: H1 heading, coloured type badge, Gemini text, scaled photos (max 5, max 500px wide), and the original keywords as 9pt grey metadata at the bottom. The file is shared as 'anyone with link can view.'

Apps Script
const fileName = isoDate + " | " + title;
const doc  = DocumentApp.create(fileName);
const body = doc.getBody();

// H1 title
body.getParagraphs()[0]
  .setHeading(DocumentApp.ParagraphHeading.HEADING1)
  .setText(title);

// Type badge
body.appendParagraph(type.toUpperCase())
  .setBold(true).setForegroundColor("#1a73e8");

// AI-generated article text
body.appendParagraph(""); 
body.appendParagraph(newsText);

// Move to CMS folder, share publicly
const file = DriveApp.getFileById(doc.getId());
folder.addFile(file);
DriveApp.getRootFolder().removeFile(file);
file.setSharing(
  DriveApp.Access.ANYONE_WITH_LINK,
  DriveApp.Permission.VIEW
);
📅
5. Published & synced
Calendar event · cache cleared · page live

An all-day calendar event is created with the event type tag and a link to the document. The script cache key used by the Web App is cleared — the next page load fetches the fresh document list. The post is live on the school website without any manual action.

Apps Script
// Calendar event
const event = cal.createAllDayEvent(
  "[" + type + "] " + title, date
);
event.setDescription("Document:\n" + docUrl);

// Invalidate CMS page cache
CacheService.getScriptCache()
  .remove(CONFIG.CACHE_KEY);

The Same Pattern, Different Problems

This architecture — form → Apps Script → structured output — is reusable across dozens of school workflows.

Field trip registration. Teacher submits event name, date, class, headcount. Script creates a registration document from a template, adds to the trips calendar, emails the teacher, and notifies the head.

Evaluation reports. After a survey closes, Apps Script tallies responses, sends them to Gemini for a summary, generates a PDF, and deposits it in the leadership Drive folder. What used to take an hour happens in seconds.

Weekly parent newsletter. A time-based trigger collects the last seven days of news documents, asks Gemini to write a digest, and sends a BCC email to all parent groups. Zero editorial effort for the same quality output.

Substitute cover requests. Teacher submits date, lesson, class, and notes. Script creates a cover sheet, emails the admin, and adds an absence to the shared staff calendar.

The pattern is always the same: the form collects structured input, Apps Script routes and transforms it, the output lands exactly where it needs to be. The teacher does one thing. The institution remembers everything.


A note on AI and data

The prompt sent to Gemini contains a title and keywords — never student names, ID numbers, or personal data. Keep it that way. AI helps with writing; it doesn’t process personal information. If teachers follow the simple rule — describe what happened, not who was involved — the system is GDPR-compliant by design.