Skip to content

BlogGuides

Google Forms to Discord: Post Every Response in a Channel

Post Google Form responses in a Discord channel for free, with a webhook and a short Apps Script. Copy-paste script, setup steps, common errors and limits.

Mico Team, Mico

9 min read

Plenty of servers run staff, whitelist or event sign-ups on a Google Form, then check the spreadsheet every few hours. The wish is always the same: when someone submits, post it in a staff channel.

Google Forms has no Discord option, and you do not need a paid automation service for this. Discord gives any channel a webhook address, and Google Apps Script, which comes with every Google account, can send each response to it.

The Discord and Google steps below were checked in November 2026 against Discord's help center and developer docs and Google's Apps Script and Forms help. Both companies rename menus from time to time, so if a label differs on your screen, look for the closest match.

What do you need before you start?

What you need

A Discord channel
One where you have the Manage Webhooks permission. Private, if the answers are
The form
Edit access to the Google Form, or to its response spreadsheet
A Google account
The script runs in it. Apps Script is free and built in
About 10 minutes
Most of it is clicking through Google's permission screens

This is how a response travels:

  1. Someone submits the form
  2. The trigger runs the script
  3. The script posts to the webhook
  4. The answers appear in your channel

How do you make a Discord webhook for the channel?

A webhook is an address that posts into one channel. Anything that knows the address can send a message there, no bot needed.

  1. Open Server Settings

    Click your server's name at the top left, then Server Settings

  2. Go to Integrations

    Then open Webhooks

  3. Make a new webhook

    Discord's help names the button Create Webhook; on a server that already has one, it may read New Webhook

  4. Pick the channel and a name

    For example #form-responses and "Applications". The name and avatar are what the posts show

  5. Copy Webhook URL

    You paste it into the script in a minute

Applications hold things people would not say in public, so post them in a channel only your staff can see.

Where do you paste the script: the form or the sheet?

Either works, and the script below handles both. Pick one, not both, or each response posts twice.

  • In the form (the better choice). Open the form, click More (the three dots), then Apps Script. The script reads answers in question order, turns file uploads into links and adds the respondent's email if you collect it.
  • In the response spreadsheet. Open the sheet, then Extensions > Apps Script. Useful if your team already works in the sheet. If the form has no sheet yet, open Responses, then Summary, then More, choose Select destination for responses and create a new spreadsheet.

In the editor that opens, delete everything in Code.gs, paste the script, put your webhook URL between the quotes on the WEBHOOK_URL line, and save.

The script: Google Form responses to a Discord webhook

/**
 * Google Forms to Discord: posts each new response in a Discord channel.
 * Paste it in the form (More > Apps Script) or in its response sheet
 * (Extensions > Apps Script), not both. Then run setUp once.
 *
 * @OnlyCurrentDoc
 */

// Your channel's webhook URL. Keep it private: anyone who has it can post there.
const WEBHOOK_URL = 'PASTE_YOUR_WEBHOOK_URL_HERE';

// The colour of the embed's side bar, as a hex number.
const COLOR = 0x00e5b0;

// Discord's limits (checked November 2026), and how far one long answer may run.
const LIMIT = { title: 256, name: 256, value: 1024, fields: 25, total: 6000 };
const MAX_PARTS = 4; // one answer fills at most 4 fields, about 4,000 characters

/** Runs on each new response. Do not press Run on this one: use setUp or sendTest. */
function onFormSubmit(e) {
  const response = e.response ? readForm_(e) : readSheet_(e);
  post_(response.title, response.answers);
}

/** Run once. Adds the trigger (removing your older copies) and sends a test. */
function setUp() {
  ScriptApp.getProjectTriggers()
    .filter((t) => t.getHandlerFunction() === 'onFormSubmit')
    .forEach((t) => ScriptApp.deleteTrigger(t));

  const form = FormApp.getActiveForm(); // null when the script lives in the sheet
  const builder = ScriptApp.newTrigger('onFormSubmit');
  if (form) builder.forForm(form).onFormSubmit().create();
  else builder.forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create();

  sendTest();
}

/** Posts a sample response, so you can check the webhook without the form. */
function sendTest() {
  post_('Test from Google Forms', [
    { question: 'Is this working?', answer: 'Yes. New responses will appear here.' },
  ]);
}

// In the form: answers in question order, file uploads as Drive links.
function readForm_(e) {
  const answers = [];
  const email = e.response.getRespondentEmail();
  if (email) answers.push({ question: 'Email', answer: email });

  e.response.getItemResponses().forEach((r) => {
    const item = r.getItem();
    let value = r.getResponse();
    if (String(item.getType()) === 'FILE_UPLOAD') {
      value = value.map((id) => 'https://drive.google.com/open?id=' + id);
    }
    answers.push({ question: item.getTitle(), answer: toText_(value) });
  });
  return { title: 'New response: ' + e.source.getTitle(), answers };
}

// In the sheet: row 1 holds the questions, column A the timestamp.
function readSheet_(e) {
  const sheet = e.range.getSheet();
  const questions = sheet.getRange(1, e.range.getColumn(), 1, e.values.length).getValues()[0];
  const answers = questions
    .map((q, i) => ({ question: String(q), answer: String(e.values[i]) }))
    .slice(1);
  return { title: 'New response: ' + sheet.getParent().getName(), answers };
}

// Checkboxes and grids arrive as lists: one line each.
function toText_(value) {
  if (value === null || value === undefined) return '';
  if (!Array.isArray(value)) return String(value);
  return value.map((v) => (Array.isArray(v) ? v.join(', ') : toText_(v))).join('\n');
}

// Builds embeds inside Discord's limits and sends them one message at a time.
function post_(title, answers) {
  const fields = [];
  answers.forEach((a) => {
    const text = a.answer.trim();
    if (!text) return; // a skipped question: Discord refuses an empty field
    const name = cut_(a.question.trim() || 'Question', LIMIT.name);
    split_(cut_(text, LIMIT.value * MAX_PARTS), LIMIT.value).forEach((part, i) => {
      fields.push({ name: i === 0 ? name : '(continued)', value: part });
    });
  });

  const embeds = [];
  let size = 0;
  fields.forEach((f) => {
    let embed = embeds[embeds.length - 1];
    const extra = f.name.length + f.value.length;
    if (!embed || embed.fields.length === LIMIT.fields || size + extra > LIMIT.total) {
      embed = newEmbed_(embeds.length ? title + ' (continued)' : title);
      embeds.push(embed);
      size = embed.title.length;
    }
    embed.fields.push(f);
    size += extra;
  });
  if (!embeds.length) embeds.push(newEmbed_(title));

  embeds.forEach((embed, i) => {
    if (i > 0) Utilities.sleep(1000); // a short pause between messages
    send_({ embeds: [embed], allowed_mentions: { parse: [] } }); // never ping anyone
  });
}

function newEmbed_(title) {
  return { title: cut_(title, LIMIT.title), color: COLOR, timestamp: new Date().toISOString(), fields: [] };
}

// Posts to Discord. On 429 (too many requests) it waits as long as Discord asks.
function send_(payload) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = UrlFetchApp.fetch(WEBHOOK_URL, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true,
    });
    const code = res.getResponseCode();
    if (code >= 200 && code < 300) return;
    if (code !== 429) throw new Error('Discord answered ' + code + ': ' + res.getContentText());
    let wait = 2;
    try {
      wait = JSON.parse(res.getContentText()).retry_after || 2;
    } catch (err) {
      // not JSON: keep the default wait
    }
    Utilities.sleep(Math.ceil(Math.min(wait, 60) * 1000) + 250);
  }
  throw new Error('Discord kept answering 429 (rate limited). Try again later.');
}

// Shortens text to max characters, without cutting an emoji in half.
function cut_(text, max) {
  return text.length <= max ? text : slice_(text, max - 3) + '...';
}

// Splits text into parts of at most size characters, dropping blank parts.
function split_(text, size) {
  const parts = [];
  let rest = text;
  while (rest) {
    const part = rest.length > size ? slice_(rest, size) : rest;
    if (part.trim()) parts.push(part);
    rest = rest.slice(part.length);
  }
  return parts;
}

function slice_(text, end) {
  const code = text.charCodeAt(end - 1);
  return text.slice(0, code >= 0xd800 && code <= 0xdbff ? end - 1 : end);
}

What it does, in plain words:

  • One embed per response, a field for each question, in form order. Skipped questions are left out.
  • Long answers are split, not cut, across up to four fields. Past about 4,000 characters an answer ends in "...", and the full text stays in Google Forms.
  • It never pings anyone, even if an answer contains @everyone.
  • It waits when Discord says slow down, then tries again, up to three times.

The limits it works inside, from Discord's developer docs:

Part of a messageDiscord's limitWhat the script does
Embed title256 charactersCuts the form name to fit
Fields in one embed25Starts another message
Field name (the question)256 charactersCuts it, ending in "..."
Field value (the answer)1,024 charactersSplits it across up to four fields
Embed description4,096 charactersNot used
All embed text in one message6,000 charactersStarts another message
Plain message text2,000 charactersNot used

How do you authorize the script and turn it on?

  1. Choose setUp

    In the function list at the top of the editor, next to Run

  2. Click Run

    The first time, Google asks for permission

  3. Review permissions

    Choose your Google account, tick every permission it lists, then Allow. Google lets you pick only some, and the script fails without all of them

  4. Pass the unverified app screen

    Explained below. It is expected for a script you wrote yourself

  5. Check your channel

    A message titled "Test from Google Forms" should be there

setUp creates the trigger that runs the script on each new response, removing any older copy of yours first so you do not get double posts, then sends that test message.

The permissions cover this one form or sheet (the @OnlyCurrentDoc line keeps it to that file), connecting to an outside service, which is Discord, and running while you are away, which the trigger needs.

The unverified app screen. On a personal Google account, Google warns that it has not verified the app. Google's docs say this happens for any personal account, including the one that wrote the script. Click Advanced, then the link to go to your project (Google labels it unsafe), and continue. Workspace accounts where the script and the user share a domain do not see it.

Prefer to add the trigger by hand? In the editor, click Triggers (the alarm clock at the left), then Add Trigger, choose onFormSubmit and the form submit event, and Save. Do this instead of setUp, not as well.

How do you test it with a real response?

Open your form's link the way an applicant would, fill it in and submit. The post should land in your channel shortly after.

If it does not, open Executions at the left of the Apps Script editor. Each run is listed there, and a failed one shows its error. To see only those, add a filter on Status and tick Failed and Timed out.

Why are my form responses not showing up in Discord?

What you seeWhyFix
Nothing in Discord, no run in ExecutionsNo trigger, or it is on the other fileRun setUp in the file that holds the script
Cannot read properties of undefined (reading 'response')You pressed Run on onFormSubmit, which needs a real submissionRun sendTest, or submit the form
Discord answered 400Discord refused the message, usually a field over its limit or an empty oneThe script prevents both. If you edited it, read the error: Discord names the field
Discord answered 401 or 404The webhook was deleted, or part of the URL is missingMake a new webhook and paste the full URL again
Discord kept answering 429Too many messages too fast on one webhookThe script waits and retries. Give each busy form its own webhook
Each response posts twiceTwo triggers, or the script in both the form and the sheetOpen Triggers and delete the extra one
"Google hasn't verified this app"Normal for your own scriptAdvanced, then go to your project
A file upload link will not openUploads are stored in the form owner's DriveShare that folder with your reviewers

About file upload questions. Discord only gets a link. Google stores uploaded files in a new folder in the form owner's Drive, and respondents have to sign in to a Google account to answer the question. Share that folder with your reviewers only, not with the whole server.

What can a Google Form and a webhook not do?

It solves one problem: where the answers show up. Everything else stays as it was.

  • It does not know who applied. The form gets whatever Discord username someone types. Even with Collect email addresses set to Verified, you learn a Google email, not a Discord account.
  • Anyone with the link can submit. People outside your server, people you banned, or the same person five times.
  • The post has no buttons. There is no Accept or Deny. Teams decide in reactions or threads and keep track themselves.
  • No role on accept, no reply. Someone gives the role by hand and messages the applicant.
  • The channel is the only privacy. Anyone who can read it sees every answer.
  • It is yours to maintain. If the webhook is deleted, posts stop, and only the trigger's owner gets the failure email.

For a sign-up sheet or a twice-a-year round, that is a fair trade. For staff, whitelist or partner applications from your own members, those gaps are most of the work.

When does an application bot fit better?

When every applicant is a member of your server. Mico runs the application inside Discord: members press Apply on a panel and answer in DMs with the bot or on the website, and each application is tied to their Discord account.

What you needGoogle Form and webhookMico
SetupPaste a script, run setUpRun /setup in your server
Who can applyAnyone with the linkMembers of your server, on their Discord account
Rules before applyingNoneMinimum days in the server; cooldowns and required roles on paid plans
Question typesAny Google Forms type, file uploads includedText only
Where answers landA message in one channelA private review channel per panel, /review and the dashboard
DecidingNo buttonsAccept and Deny on the review message
After a decisionRole and reply by handGives or removes roles, tells the applicant by DM
Help reading answersNoneScores each one: a quality score and a looks AI-written score, each with a short reason
Applicants not on DiscordFineNot possible

The AI score is advice: a person decides. Mico has a free plan, and plans differ in what they include; the pricing page has the details.

Whether to switch at all is its own question, and Google Forms or a bot? answers it in full. The short version: if some applicants are not on Discord, or you need file uploads or checkboxes, keep the form and this script.

Going the other way. If you run applications in Mico and want them in a Google Sheet, a panel's webhook sends each application sent, accepted or denied as JSON to an address you choose, on paid plans. Sending applications to Google Sheets covers the relay in between, and the webhook reference lists every field.

To try Mico on your server, start here. Running /setup makes an apply channel, a private review channel, a reviewer role and a ready panel, using what your server already has.

Frequently asked questions

Yes. A webhook on your Discord channel plus a short Google Apps Script costs nothing. The script runs in your own Google account, and a personal account can make 20,000 outside calls a day, far more than any form needs. Paid automation services do the same job without code.

Usually there is no trigger: pasting the script is not enough, it has to be told to run on each submission. Run setUp once in the Apps Script editor. If it still fails, open Executions at the left of the editor, where each run is listed with its error.

Yes. Open the linked sheet, then Extensions and Apps Script, and paste the same script. It reads the questions from row 1 and the answers from the new row. Use the form or the sheet, not both, or each response posts twice.

As Drive links. Google stores uploaded files in a folder in the form owner's Drive, so a link only opens for people who can see that folder. Respondents also have to sign in to a Google account to upload.

No. It posts whatever the person typed, including any Discord username, and anyone with the form link can submit. An application bot such as Mico ties each application to the member's Discord account instead, because they apply from inside your server.

No. Treat it like a password: anyone who has it can post in that channel without logging in. Keep it out of public channels and shared copies of the script. If it leaks, delete the webhook and make a new one.

See it on your own server.

Add Mico free, run /setup, and the next application arrives scored, in its own review channel.

Free plan, unlimited applications. New servers get Premium free for 14 days with their first posted panel. No card.