Googleフォームの回答をSlackに通知すると、気づくのは早くなります。
でも、あとから見返すときに困ることがあります。
Slack通知は見つかった
回答の要約もある
でも、元の回答へ戻れない
Google Sheetsを開いて、Timestampで探す。
名前で検索する。
行番号を思い出す。
この作業が増えると、Slack通知は便利なのに、対応管理は重くなります。
この記事では、Googleフォームの編集URLを management_url として残し、Slack通知や未対応管理から元回答へ戻れるようにする設計を整理します。
先に結論
Googleフォーム + Sheets + Apps Scriptで運用するなら、回答レコードにこの5つを分けて残します。
| 列 | 役割 |
|---|---|
response_id |
どの回答か |
submitted_at |
いつ送られたか |
management_url |
後から戻る場所 |
status |
いまどう処理されているか |
owner |
誰が見るか |
ここで大事なのは、management_url を回答本文の一部にしないことです。
これは公開用リンクではありません。
運用側が回答へ戻るための内部リンクです。
Slack通知には抜粋だけでなく戻り先を入れる
Slack通知に、回答内容だけを入れる構成はよくあります。
新しい回答が届きました
名前: 山田 太郎
種別: 資料請求
内容: 料金について知りたいです
これでも新着には気づけます。
ただ、あとで作業するときには足りません。
どの行を見るのか
どの回答なのか
対応状況はどうなっているのか
誰が担当なのか
Slackは流れていきます。
だから通知の中には、最低限、戻り先を入れたほうがいいです。
新しい回答が届きました
response_id: 2_ABaOnuc...
status: 未対応
owner: 未設定
management_url: https://docs.google.com/forms/...
通知は読む場所です。
management_url は戻る場所です。
この2つを分けます。
getEditResponseUrl()で編集URLを取る
Google Apps Scriptの FormResponse には、提出済み回答の編集URLを生成する getEditResponseUrl() があります。
Forms側のフォーム送信トリガーなら、イベントオブジェクトの e.response から FormResponse を扱えます。
ここで使うのは、Googleフォーム側のインストール型フォーム送信トリガーです。回答先Google Sheets側のトリガーでは、e.range や e.values は扱いやすいですが、e.response は同じ形では出てきません。
function onFormSubmit(e) {
const response = e.response;
const responseId = response.getId();
const submittedAt = response.getTimestamp();
const managementUrl = response.getEditResponseUrl();
console.log({
responseId,
submittedAt,
managementUrl,
});
}
ここで、3つを混ぜません。
responseId: どの回答か
submittedAt: いつ送られたか
managementUrl: 後から戻るURL
TimestampをIDにしないのと同じです。
URLもIDではありません。
戻る場所です。
編集URLは便利だが、扱いは慎重にする
getEditResponseUrl() は便利です。
ただし、無条件に公開していいURLではありません。
GoogleのApps Scriptドキュメントでは、このURLは提出済み回答を編集するために使えるURLとして説明されています。回答編集が無効な場合は、編集が無効であることを説明するページへ進むとされています。
また、リンクを持っている人が回答を編集できる点にも注意が必要です。ログインやアクセス制限の設定によって挙動は変わりますが、少なくとも「外に広く貼ってよいURL」とは考えないほうがいいです。
メールアドレス収集を有効にしている場合、編集後の記録に残るメールアドレスの扱いも変わるため、監査や本人確認に使うフォームでは特に注意します。
私は、このURLを次のように扱います。
公開記事や公開ページには載せない
社内の管理シートにだけ残す
Slackに出す場合は限定チャンネルにする
回答者へ送る本文には安易に入れない
個人情報を扱うフォームでは共有範囲を絞る
名前を edit_url ではなく management_url にするのも、そのためです。
「回答者のための編集リンク」ではなく、「運用側が戻るための管理リンク」として扱う意図を明確にします。
response_recordsシートへ残す最小例
回答先のGoogle Sheetsに直接列を追加してもよいですが、ここでは管理用に response_records シートを別に作る例にします。
フォーム回答シートは、Googleフォームが書き込む原本です。
管理用シートは、運用の状態を持つ場所です。
フォームの回答 1
-> Googleフォームが書き込む
response_records
-> response_id, management_url, status, ownerを持つ
最小例はこうです。
const SPREADSHEET_ID = "YOUR_SPREADSHEET_ID";
const RECORD_SHEET_NAME = "response_records";
const HEADERS = [
"response_id",
"submitted_at",
"management_url",
"status",
"owner",
"last_updated_at",
"next_action",
];
function onFormSubmit(e) {
const response = e.response;
const record = {
responseId: response.getId(),
submittedAt: response.getTimestamp(),
managementUrl: response.getEditResponseUrl(),
};
const sheet = getRecordSheet();
ensureHeaders(sheet);
upsertResponseRecord(sheet, record);
}
function getRecordSheet() {
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = spreadsheet.getSheetByName(RECORD_SHEET_NAME);
if (!sheet) {
return spreadsheet.insertSheet(RECORD_SHEET_NAME);
}
return sheet;
}
function ensureHeaders(sheet) {
const values = sheet.getDataRange().getValues();
const firstRow = values[0] || [];
const hasHeaders = HEADERS.every((header, index) => firstRow[index] === header);
if (!hasHeaders) {
sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
}
}
function upsertResponseRecord(sheet, record) {
const values = sheet.getDataRange().getValues();
const responseIdColumn = 0;
const now = new Date();
for (let row = 1; row < values.length; row += 1) {
if (values[row][responseIdColumn] === record.responseId) {
sheet.getRange(row + 1, 2, 1, 3).setValues([[
record.submittedAt,
record.managementUrl,
values[row][3] || "未対応",
]]);
sheet.getRange(row + 1, 6).setValue(now);
return;
}
}
sheet.appendRow([
record.responseId,
record.submittedAt,
record.managementUrl,
"未対応",
"",
now,
"",
]);
}
この例では、response_id がすでにある場合は更新し、なければ追加します。
最初から完璧なワークフローにしなくても大丈夫です。
まずは、回答が届いた瞬間に戻り先を失わないことが大事です。
Slack通知に入れるのはURLそのものだけではない
Slack通知に management_url だけを入れると、たしかに戻れます。
でも、運用上は状態も一緒に出したほうがいいです。
function buildSlackMessage(record) {
return [
"新しいGoogleフォーム回答が届きました",
`response_id: ${record.responseId}`,
"status: 未対応",
"owner: 未設定",
`management_url: ${record.managementUrl}`,
].join("\n");
}
通知文は長くしすぎなくていいです。
必要なのは、読むための全文ではなく、戻るための手がかりです。
誰の回答か
どの状態か
誰が見るか
どこへ戻るか
この4つがあれば、Slack通知は「流れるメッセージ」ではなく「管理シートへ戻る入口」になります。
edit_urlではなくmanagement_urlと呼ぶ理由
私は、列名を edit_url より management_url にするほうが好きです。
理由は、編集するためだけのURLではないからです。
実務では、URLの役割はもっと広いです。
元回答を確認する
状態更新の根拠を見る
Slack通知から戻る
未対応ダイジェストから戻る
除外理由を確認する
つまり、これは「編集URL」というより、運用上の戻り先です。
もちろん内部的には getEditResponseUrl() で取得したURLかもしれません。
でも、列名は業務上の意味に合わせたほうが、あとから読みやすくなります。
よくある事故
management_url がないと、こういう事故が起きます。
Slack通知を見ても、どの回答か探せない
Timestampで検索しても同じ人の回答が複数ある
行番号を見ていたが、並び替え後にずれた
未対応ダイジェストに戻り先がなく、結局シートを目視する
回答編集URLを公開チャンネルに貼ってしまう
最後のものは特に注意です。
回答には個人情報や問い合わせ内容が含まれることがあります。
戻れるURLは便利ですが、共有範囲を決めてから使います。
FORMLOVAで見ていること
FORMLOVAでは、フォーム回答を単なる行ではなく、状態を持つ回答として見ています。
一覧を見るだけで終わりではありません。
回答一覧を見る
条件で絞る
回答内容を確認する
対応状況を変える
必要なら担当者や次アクションを持つ
Googleフォーム + Sheets + Apps Scriptで運用する場合も、同じ考え方が使えます。
回答が届く。
通知する。
管理シートに残す。
戻れるURLを持つ。
ステータスを進める。
この順番にすると、Googleフォームはただの受付口ではなく、運用の入口として扱いやすくなります。
チェックリスト
Googleフォームの回答をSlack通知やSheetsで扱うなら、ここを確認します。
[ ] 回答ごとにresponse_idを持っている
[ ] submitted_atとresponse_idを混ぜていない
[ ] management_urlを管理用に保存している
[ ] Slack通知に戻り先がある
[ ] management_urlを公開チャンネルに出していない
[ ] statusとownerをURLとは別に持っている
[ ] 未対応リマインドから元回答へ戻れる
全部を一度に作る必要はありません。
まずは、通知から回答へ戻れること。
ここを作るだけでも、対応漏れの調査はかなり楽になります。
まとめ
Googleフォームの回答運用では、回答内容を取得するだけでは足りません。
あとから戻れる場所が必要です。
response_id は、どの回答か。
submitted_at は、いつ送られたか。
management_url は、どこへ戻るか。
status は、どこまで進んだか。
owner は、誰が見るか。
この5つを分けて残しておくと、Slack通知、未対応管理、除外、CSV、週次確認がかなり安定します。
参考
- Class FormResponse - Apps Script
- イベント オブジェクト - Apps Script
- Installable Triggers - Apps Script
- Googleフォームの回答を表示、管理する - Google ドキュメント エディタ ヘルプ
