LINE公式アカウントの即時自動応答と申込フォームをGASだけで作る

LINE公式アカウントに届いたメッセージに、確認する前に一次返信が返る仕組みをGASだけで作った。体験・入会・講習・面談のフォームもここに接続している。

2026-09-14 公開 / 滋賀県彦根市の一人塾「総合学習教室ブリッジ」で実際に動いている仕組みの記録 / Google Apps Script(GAS)

何を自動化したか

LINE公式アカウントに届いたメッセージに、僕が確認するより先に一次返信が返る仕組みと、体験予約・入会・講習申込・面談予約をフォーム入力だけで完結させる仕組みを、Google Apps Script(GAS)だけで作った。塾のような対面ビジネスは「問い合わせから返信までの速さ」が成約率に直結する。深夜や指導中にLINEが来ても、キーワードに応じた案内文とフォームのリンクを1秒で返し、同時に自分のGmailに着信通知が届く。

効果を秒単位で計測してはいないため数字は体感になるが、以前は「授業が終わってから折り返す」までの数時間〜半日の空白があったのに対し、いまはその空白がゼロになった。フォーム側も、電話・LINEでの聞き取りをやめてスマホ入力だけで完結するようにしたことで、こちらの対応漏れがなくなった。

仕組み(データの流れ)

入口は2つある。ひとつはLINEのテキストメッセージ、もうひとつは公開フォーム(体験・入会・講習・面談)。どちらも最終的に同じスプレッドシートのハブに集まる。

入力処理出力
LINEのテキストキーワード判定→定型文生成→LINE Reply APIユーザーへ即時返信+オーナーへ着信通知
体験予約フォーム「体験生フォロー」シートへ追記カレンダー仮予定+オーナー通知(このシートは体験生フォローの自動化の入口にもなる)
入会・講習・面談フォームそれぞれの申込シートへ追記、講習は残席チェックオーナー通知(満席なら申込自体を拒否)

LINEのWebhookは1本のGASプロジェクトのdoPostに届く。そこからテキストメッセージだけを自動応答の関数に振り分け、画像メッセージは別のOCR処理に振り分けている。1つのdoPostに複数の役割を持たせているのは、GASのWebアプリは1プロジェクトにつき1つの公開URLしか持てず、LINEのWebhook URLを複数用意するのが現実的でないためだ。

コード(要点)

Webhookの入口はこうなっている。テキストメッセージだけを自動応答関数へ渡し、それ以外(画像など)は既存のOCR処理を通す。

function doPost(e) {
  if (e && e.parameter && e.parameter.app === 'pixi') return pixiApi(e);
  try {
    var events = JSON.parse(e.postData.contents).events;
    events.forEach(function(event) {
      if (event.type === "message" && event.message.type === "image") handleImage(event);
      else if (event.type === "message" && event.message.type === "text") bridgeLineAutoReply(event);
    });
  } catch (err) {
    // LINE webhookには常に200を返す必要があるため、エラーはログのみ
    Logger.log('doPost エラー: ' + err.message);
  }
}

自動応答本体はキーワードの正規表現マッチだけで、AIは呼んでいない。返信は定型文+フォームURLの組み合わせで、応答できなかった場合も含めてオーナーには必ず通知が飛ぶ。

function bridgeLineAutoReply(event) {
  var text = (event.message && event.message.text) || '';
  if (!text) return;
  var token = PropertiesService.getScriptProperties().getProperty('LINE_ACCESS_TOKEN');
  var base = PropertiesService.getScriptProperties().getProperty('WEBAPP_URL') || '';

  var reply = null;
  if (/体験|見学|たいけん/.test(text)) {
    reply = 'メッセージありがとうございます!\n無料体験は下のフォームから24時間いつでもご予約いただけます\n'
          + (base ? base + '?app=trial' : '');
  } else if (/料金|月謝|費用|いくら/.test(text)) {
    reply = 'お問い合わせありがとうございます!料金は折り返しご案内します。\n' + (base ? base + '?app=trial' : '');
  }
  // ...入会・講習・面談も同様のパターンマッチで分岐

  if (reply && token && event.replyToken) {
    UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
      method: 'POST',
      contentType: 'application/json',
      headers: { Authorization: 'Bearer ' + token },
      payload: JSON.stringify({ replyToken: event.replyToken, messages: [{ type: 'text', text: reply }] }),
      muteHttpExceptions: true,
    });
  }
  // 応答の有無に関わらず、問い合わせ着信をオーナーに即通知する
}

フォーム側は、体験予約なら「体験生フォロー」シートへの追記とカレンダー登録をセットで行う。カレンダー登録が失敗しても申込自体は成立させたいので、try/catchで独立させている。

function bridgeCreateEvent(title, dateStr, timeStr, desc) {
  try {
    var start = new Date(dateStr);
    if (isNaN(start.getTime())) return;
    var hour = 17, minute = 0;
    var m = String(timeStr || '').match(/(\d{1,2})[:時](\d{0,2})/);
    if (m) { hour = Number(m[1]); minute = Number(m[2] || 0); }
    start.setHours(hour, minute, 0, 0);
    var end = new Date(start.getTime() + 60 * 60 * 1000);
    CalendarApp.getDefaultCalendar().createEvent(title, start, end, { description: desc || '' });
  } catch (e) {
    Logger.log('カレンダー登録失敗: ' + e.message);
  }
}

フォーム送信の受け口は種別ごとに関数を分けたうえで、共通の入口関数でルーティングしている。長い分岐部分は省くが、考え方としては「1つのdoPost/1つの受け口関数に対して、種別ごとの処理関数を素直に並べる」だけで、フレームワークは使っていない。全文はnoteへ。

ハマった点と回避策

導入手順

  1. LINE DevelopersでMessaging APIのチャネルを作成し、チャネルアクセストークンを発行する。
  2. GASプロジェクトのスクリプトプロパティ(「プロジェクトの設定」→「スクリプトプロパティ」)に LINE_ACCESS_TOKEN を登録する。コードにトークンを直接書かない。
  3. GASを「ウェブアプリとして導入」し、発行されたURLをLINE Developersの「Webhook URL」に設定して有効化する。
  4. 初回アクセス時に WEBAPP_URL がスクリプトプロパティへ自動保存されるようにしておくと、フォームURLの組み立てが以後は自動になる。
  5. doPostでテキストメッセージを自動応答関数へ、画像メッセージを別処理へ振り分ける条件分岐を書く。
  6. 体験・入会・講習・面談それぞれのフォーム送信の受け口を用意し、対応するスプレッドシートのシート名・列見出しを決めて追記処理を書く。
  7. LINE公式アカウントのリッチメニューに、ウェブアプリURLに?app=trial等のパラメータを付けたリンクを設定する。
  8. 本番投入前に、自分のLINEアカウントから「体験」「料金」などのキーワードを実際に送って返信内容を確認する。

よくある質問

Q. AIを使わずにキーワードマッチだけにしているのはなぜですか

一次応答は速さが命で、AI呼び出しのレイテンシと料金をかける必要がないと判断したため。定型文で対応できない込み入った相談は、返信の中で「教室から直接連絡する」旨を伝え、人が引き取る前提にしている。

Q. 1つのGASプロジェクトにWebhookとフォームを同居させて問題はないですか

Webhookの受け口であるdoPostにe.parameter.appの有無で処理を分けているだけなので、リクエストの種類が増えても分岐を足すだけで対応できている。プロジェクトが肥大化してきたら分割も検討するが、現時点ではトリガー数や実行時間の制約に達していない。

Q. フォームの入力データはどこに保存されますか

「体験生フォロー」「入会申込」「講習申込」「面談予約」といったシート名でスプレッドシートに保存している。IDそのものはスクリプトプロパティで管理し、コードには書いていない。

この仕組みの「全体像」と「コード全文」