DEV Community

Cover image for Google Forms + Apps Script Is a Workflow, Not Just a Notification
Lovanaut
Lovanaut

Posted on

Google Forms + Apps Script Is a Workflow, Not Just a Notification

Google Forms is not the weak part of many form workflows.

The first version is usually good:

Google Form
-> Google Sheet
-> Apps Script trigger
-> Slack notification
Enter fullscreen mode Exit fullscreen mode

For many teams, this is exactly the right default.

The form is easy to create. The response is safely stored in Sheets. Apps Script can run on form submit. Slack gets a message quickly. Nobody needs to introduce a new backend just to know that a lead, inquiry, or internal request arrived.

The problem starts later.

The Slack notification works, so the team asks for one more thing:

  • include the inquiry type
  • exclude obvious spam
  • show the response row
  • add an owner
  • add a status
  • send an auto-reply
  • retry failed Slack posts
  • split notifications by category
  • count unresolved responses each week

Each request is reasonable.

Together, they turn a notification script into a small operations system.

I have been building FORMLOVA, a form operations product where people can create forms, review responses, manage response status, configure notifications, and run form workflows through chat and MCP clients.

One product lesson keeps repeating:

A form integration becomes fragile when the team treats "notified" as the same thing as "handled."

This post is about that boundary.

The Minimal Apps Script Version

Here is the common starting point.

You connect a Google Form to a response Sheet. In the Sheet, you create an Apps Script project. Then you add an installable form-submit trigger and post to a Slack Incoming Webhook.

Google documents installable triggers for Apps Script events, and UrlFetchApp.fetch() is the standard Apps Script API for making HTTP requests. Slack's Incoming Webhooks accept JSON payloads at a generated URL, and Slack treats that URL as a secret.

Those three pieces are enough for the first version.

const SLACK_WEBHOOK_URL =
  PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");

function onFormSubmit(e) {
  if (!SLACK_WEBHOOK_URL) {
    throw new Error("SLACK_WEBHOOK_URL is not set.");
  }

  const values = e.namedValues;

  const name = first(values, "Name");
  const email = first(values, "Email");
  const category = first(values, "Inquiry type", "Uncategorized");
  const message = first(values, "Message");

  const payload = {
    text: `New form response: ${category}`,
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: [
            "*New form response*",
            `*Type:* ${category}`,
            `*Name:* ${name}`,
            `*Email:* ${email}`,
            `*Message:* ${message}`,
            "*Initial status:* New",
          ].join("\n"),
        },
      },
    ],
  };

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

  const status = response.getResponseCode();
  if (status < 200 || status >= 300) {
    console.error(response.getContentText());
    throw new Error(`Slack notification failed: ${status}`);
  }
}

function first(values, key, fallback = "Not provided") {
  return values[key]?.[0] || fallback;
}
Enter fullscreen mode Exit fullscreen mode

This is a useful script.

But it only proves one thing:

Apps Script attempted to send a Slack message for a response.

It does not prove that the team saw the message.

It does not prove that someone owns the response.

It does not prove that the response is done.

That distinction is where the design work begins.

Store Secrets Outside the Code

The webhook URL should not live in the script body.

Use Script Properties:

Project Settings
-> Script Properties
-> Add script property

Key: SLACK_WEBHOOK_URL
Value: https://hooks.slack.com/services/...
Enter fullscreen mode Exit fullscreen mode

Then read it from code:

PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");
Enter fullscreen mode Exit fullscreen mode

This is not a perfect secrets-management system.

But it is much better than pasting the webhook into source code, a blog post, a screenshot, or a shared GitHub repository.

Once a form is used by a real team, the webhook URL becomes infrastructure. Treat it that way.

The Sheet Columns Become a Contract

The next problem is not Slack.

It is the Sheet.

As soon as Apps Script reads columns by name, the Sheet becomes part of the system contract.

For example:

Timestamp
Name
Email
Inquiry type
Message
Status
Owner
Slack notified at
Last error
Enter fullscreen mode Exit fullscreen mode

These columns are no longer just spreadsheet labels.

They are the API between the form, the script, and the team.

If someone renames "Inquiry type" to "Category", the script may stop finding the value. If someone removes "Status", the team loses its operational view. If someone inserts manual notes into a column the script expects to control, the behavior gets ambiguous.

There are ways to make this safer:

  • read by header name instead of column position
  • protect operational columns
  • separate raw response data from workflow columns
  • keep configuration in a dedicated tab
  • log errors in a predictable place

But notice what happened.

The form is no longer just a form. It now has schema design, permissions, logs, and change management.

Notification State Is Not Response State

A Slack post is a notification state.

It should not be the response state.

I like to separate them explicitly.

response_status:
  new
  in_progress
  done
  excluded

notification_status:
  pending
  sent
  failed
  skipped
Enter fullscreen mode Exit fullscreen mode

That distinction prevents a common mistake:

Slack message sent
-> therefore the response is handled
Enter fullscreen mode Exit fullscreen mode

No.

The better model is:

Response captured
-> notification attempted
-> owner assigned
-> status changed by a human or workflow
-> response closed or excluded
Enter fullscreen mode Exit fullscreen mode

In a small team, this may be just two Sheet columns.

Status: New / In progress / Done / Excluded
Owner: Alice / Bob / Unassigned
Enter fullscreen mode Exit fullscreen mode

That is enough to make the operation visible.

Without those columns, Slack becomes the task system by accident. Threads, reactions, and "I'll check this" replies become the only source of truth.

That is fragile.

Slack Failure Should Not Lose the Response

The response capture path should stay simple:

submit form
-> save response in Sheets
-> then try notification
Enter fullscreen mode Exit fullscreen mode

If Slack is down, the response should still be in the Sheet.

If the webhook URL was rotated, the response should still be in the Sheet.

If Apps Script times out, the raw response should not disappear.

This is one reason Google Forms + Sheets is a strong starting point: the response storage path is already separated from the Slack notification path.

The risk is assuming that because Sheets captured the response, the workflow is complete.

It is not.

A more operational Sheet might include:

Slack notified at
Notification status
Last notification error
Retry count
Owner
Status
Closed at
Enter fullscreen mode Exit fullscreen mode

Now the team can tell the difference between:

  • response was received
  • Slack was notified
  • notification failed
  • someone owns it
  • the work is done

That is the minimum boundary I care about.

Retries Need a Record

Once a form matters, failed notifications need a retry path.

For simple use cases, you can retry manually after checking the Apps Script logs.

For more serious workflows, write the failure into the Sheet.

Pseudo-logic:

function markNotificationResult(rowNumber, result) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");

  sheet.getRange(rowNumber, column("Notification status")).setValue(result.status);
  sheet.getRange(rowNumber, column("Last notification error")).setValue(result.error || "");
  sheet.getRange(rowNumber, column("Slack notified at")).setValue(result.notifiedAt || "");
}
Enter fullscreen mode Exit fullscreen mode

The exact implementation depends on your Sheet.

The important part is that failure becomes visible in the same operational surface as the response.

Otherwise, the team only learns about notification failure when somebody notices silence.

When Google Forms + Apps Script Is Enough

I would keep the Google Forms + Sheets + Apps Script setup when:

  • the form volume is low
  • one person owns the workflow
  • notification failure is not business-critical
  • the Sheet is the accepted source of truth
  • the team is comfortable maintaining Apps Script
  • the workflow has only a few states

This is a perfectly valid setup.

It is often the best first version.

The mistake is not using Google Forms.

The mistake is letting a small script quietly become an undocumented operations system.

When to Move Beyond the Script

The boundary usually appears when the team starts asking questions like:

  • Which responses are still open?
  • Who owns this response?
  • Why did this person get two notifications?
  • Did the auto-reply send?
  • Can we exclude test submissions from analytics?
  • Can the support team edit status without touching script columns?
  • Can an AI assistant find unresolved responses and summarize them?

At that point, the workflow is no longer just about posting to Slack.

It is about response operations.

That is the area I am building FORMLOVA around. FORMLOVA exposes form creation, response review, notifications, analytics, and workflow operations through 127 typed MCP tools across 25 categories, so the operational surface can be handled from ChatGPT, Claude, and other MCP clients instead of being hidden inside a fragile spreadsheet script.

The product bet is simple:

The value after form creation is not another prettier form. It is the operational system around the response.

A Practical Rule

If your team only needs awareness, a Slack notification is enough.

If your team needs ownership, status, retries, auditability, or AI-assisted follow-up, the notification is only the first event in the workflow.

That is the line I would watch.

Start with Google Forms, Sheets, and Apps Script.

Just do not confuse "the message was posted" with "the work is handled."

References Checked While Drafting

Top comments (0)