この記事では、Googleフォームで集めた職員会議の議題を、会議用アジェンダシートに自動整形する手順がわかります。
「期限後の議題」「担当未入力」「同じような議題」を、会議前に見つける実装がわかります。
GASだけで、入力受付からアジェンダ生成、確認メール文面の作成までを小さくつなげる方法がわかります。
課題
職員会議の議題をフォームで集めると、回収自体は楽になります。
ただ、そのままスプレッドシートに回答が積み上がるだけだと、会議前に次の作業が残ります。
- 議題をカテゴリ別に並べ替える
- 担当者が空欄の回答を探す
- 似た議題をまとめる
- 期限後に届いたものを分ける
- 当日配るアジェンダ用の形に整える
既出記事との重複も避けます。公開済みのQiitaには、たとえば「GASでフォーム回答から『未対応だけ』の職員用ToDo表を自動生成する」「GASでフォーム回答を週次ふり返りレポートに整える:AIに渡す前の集計テンプレート」「GASでフォーム回答から個別フィードバック下書きを作る: 校務で使えるAI前処理パターン」があります。
この記事では、フォーム回答の「未対応ToDo化」や「AI前処理」ではなく、職員会議の議題を会議アジェンダに整形するところに絞ります。
原因
フォーム回答のシートは、入力された時系列のログです。
一方で、会議アジェンダに必要なのは、会議で扱う順番に並んだ一覧です。
つまり、入力に向いた表と、会議に向いた表が違います。
そこで、回答シートを直接きれいにしようとせず、次の3層に分けます。
-
Form Responses:フォーム回答の生データ -
Agenda:会議用に整形した一覧 -
Issues:確認が必要な行だけを集めた一覧
この分け方にすると、フォーム回答を壊さず、会議前に必要な確認だけを見つけやすくなります。
実装
前提として、Googleフォームの回答先スプレッドシートに次の列があるとします。
| 列 | 内容 |
|---|---|
| A | タイムスタンプ |
| B | 議題 |
| C | カテゴリ |
| D | 担当 |
| E | 希望扱い |
| F | 補足 |
カテゴリは 報告、相談、決裁、共有 のように、フォーム側で選択式にしておくと後工程が安定します。
1. メニューを追加する
スプレッドシートを開いたときに、職員会議用のメニューを出します。
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('職員会議')
.addItem('アジェンダを更新', 'buildMeetingAgenda')
.addItem('確認事項を更新', 'buildIssueList')
.addToUi();
}
この段階では、まだ自動送信や外部API連携はしません。
まずは、職員室内で確認できるシートを作るだけにします。
2. 回答をアジェンダ用に整形する
次のコードでは、フォーム回答を読み取り、カテゴリ順と投稿時刻順で並べて Agenda シートに出力します。
const CONFIG = {
responseSheetName: 'Form Responses',
agendaSheetName: 'Agenda',
issueSheetName: 'Issues',
deadline: '2026-09-01T17:00:00+09:00',
categoryOrder: ['決裁', '相談', '報告', '共有']
};
function buildMeetingAgenda() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const responseSheet = ss.getSheetByName(CONFIG.responseSheetName);
if (!responseSheet) {
throw new Error(`シートが見つかりません: ${CONFIG.responseSheetName}`);
}
const values = responseSheet.getDataRange().getValues();
const rows = values.slice(1).map(toAgendaRow).filter(row => row.topic);
rows.sort((a, b) => {
const categoryDiff = categoryRank(a.category) - categoryRank(b.category);
if (categoryDiff !== 0) return categoryDiff;
return a.timestamp.getTime() - b.timestamp.getTime();
});
const agendaSheet = getOrCreateSheet_(ss, CONFIG.agendaSheetName);
agendaSheet.clear();
agendaSheet.getRange(1, 1, 1, 6).setValues([[
'No.',
'カテゴリ',
'議題',
'担当',
'希望扱い',
'補足'
]]);
if (rows.length === 0) return;
const output = rows.map((row, index) => [
index + 1,
row.category,
row.topic,
row.owner,
row.requestType,
row.note
]);
agendaSheet.getRange(2, 1, output.length, output[0].length).setValues(output);
agendaSheet.autoResizeColumns(1, 6);
}
function toAgendaRow(row) {
return {
timestamp: new Date(row[0]),
topic: String(row[1] || '').trim(),
category: String(row[2] || '共有').trim(),
owner: String(row[3] || '').trim(),
requestType: String(row[4] || '').trim(),
note: String(row[5] || '').trim()
};
}
function categoryRank(category) {
const index = CONFIG.categoryOrder.indexOf(category);
return index === -1 ? CONFIG.categoryOrder.length : index;
}
function getOrCreateSheet_(ss, name) {
return ss.getSheetByName(name) || ss.insertSheet(name);
}
この実装のポイントは、回答シートを加工しないことです。
会議用の見た目は Agenda に作り直すので、フォーム回答の履歴を残したまま、会議で使う順番だけを変えられます。
3. 確認が必要な行を抽出する
次に、期限後の投稿、担当者未入力、似た議題の疑いを Issues に集めます。
function buildIssueList() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const responseSheet = ss.getSheetByName(CONFIG.responseSheetName);
if (!responseSheet) {
throw new Error(`シートが見つかりません: ${CONFIG.responseSheetName}`);
}
const values = responseSheet.getDataRange().getValues();
const rows = values.slice(1).map(toAgendaRow).filter(row => row.topic);
const deadline = new Date(CONFIG.deadline);
const issues = [];
rows.forEach((row, index) => {
const rowNumber = index + 2;
if (!row.owner) {
issues.push([rowNumber, '担当未入力', row.topic, row.category, row.owner, row.requestType]);
}
if (row.timestamp.getTime() > deadline.getTime()) {
issues.push([rowNumber, '期限後投稿', row.topic, row.category, row.owner, row.requestType]);
}
});
const duplicatePairs = findSimilarTopicPairs_(rows);
duplicatePairs.forEach(pair => {
issues.push([
`${pair.aIndex + 2} / ${pair.bIndex + 2}`,
'類似議題の可能性',
`${pair.a.topic} / ${pair.b.topic}`,
`${pair.a.category} / ${pair.b.category}`,
`${pair.a.owner} / ${pair.b.owner}`,
''
]);
});
const issueSheet = getOrCreateSheet_(ss, CONFIG.issueSheetName);
issueSheet.clear();
issueSheet.getRange(1, 1, 1, 6).setValues([[
'回答行',
'確認種別',
'議題',
'カテゴリ',
'担当',
'希望扱い'
]]);
if (issues.length === 0) {
issueSheet.getRange(2, 1).setValue('確認が必要な行はありません。');
return;
}
issueSheet.getRange(2, 1, issues.length, issues[0].length).setValues(issues);
issueSheet.autoResizeColumns(1, 6);
}
function findSimilarTopicPairs_(rows) {
const pairs = [];
for (let i = 0; i < rows.length; i++) {
for (let j = i + 1; j < rows.length; j++) {
const score = similarityScore_(rows[i].topic, rows[j].topic);
if (score >= 0.6) {
pairs.push({ aIndex: i, bIndex: j, a: rows[i], b: rows[j], score });
}
}
}
return pairs;
}
function similarityScore_(a, b) {
const termsA = splitTerms_(a);
const termsB = splitTerms_(b);
if (termsA.size === 0 || termsB.size === 0) return 0;
const intersection = new Set([...termsA].filter(term => termsB.has(term)));
const union = new Set([...termsA, ...termsB]);
return intersection.size / union.size;
}
function splitTerms_(text) {
return new Set(
String(text)
.toLowerCase()
.replace(/[、。,.()()「」『』【】\[\]\s]/g, ' ')
.split(' ')
.map(term => term.trim())
.filter(term => term.length >= 2)
);
}
類似議題の判定は、厳密な自然言語処理ではありません。
ただ、職員会議の議題募集では、完全一致よりも「同じ話かもしれないものを事前に見つける」ことが重要です。
そのため、ここでは単語集合の重なりで、確認候補を出すだけにしています。
4. 確認メール文面を作る
送信まで自動化すると、宛先ミスや内容未確認のリスクが上がります。
まずは、確認用のメール文面だけをシートに出す形にします。
function buildConfirmationText() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const agendaSheet = ss.getSheetByName(CONFIG.agendaSheetName);
if (!agendaSheet) {
throw new Error(`先に ${CONFIG.agendaSheetName} を作成してください`);
}
const values = agendaSheet.getDataRange().getValues();
const rows = values.slice(1).filter(row => row[2]);
const lines = [
'職員会議の議題案を以下の通り整理しました。',
'',
...rows.map(row => `${row[0]}. [${row[1]}] ${row[2]}(担当: ${row[3] || '未入力'})`),
'',
'担当未入力や重複の可能性があるものは、確認事項シートを参照してください。'
];
const sheet = getOrCreateSheet_(ss, 'Mail Draft');
sheet.clear();
sheet.getRange(1, 1).setValue(lines.join('\n'));
sheet.autoResizeColumn(1);
}
ここでは MailApp.sendEmail() を使っていません。
本文を人が確認してから通常のメール作成画面に貼り付ける運用でも、会議前の整形作業はかなり減らせます。
検証
次のようなサンプル回答を入れて、動作を確認します。
| タイムスタンプ | 議題 | カテゴリ | 担当 | 希望扱い | 補足 |
|---|---|---|---|---|---|
| 2026/09/01 09:00 | 文化祭準備の役割分担 | 相談 | A係 | 協議 | 当日係を確認したい |
| 2026/09/01 10:00 | 文化祭の係分担について | 相談 | 協議 | 分担表の確認 | |
| 2026/09/01 11:00 | 月末提出物の確認 | 報告 | B係 | 共有 | 今月分 |
| 2026/09/02 09:00 | 備品購入の承認 | 決裁 | C係 | 決裁 | 見積確認 |
期待する結果は次の通りです。
-
Agendaでは、決裁、相談、報告の順に並ぶ - 担当が空欄の「文化祭の係分担について」が
Issuesに出る - 期限後の「備品購入の承認」が
Issuesに出る - 文化祭に関する2件が、類似議題の候補として
Issuesに出る
最低限、次の観点で見ます。
- 元のフォーム回答シートが変更されていない
-
Agendaの番号が1から振り直されている -
Issuesに確認理由が残っている - メール文面に担当未入力の表示が残る
再発防止
一度作って終わりにすると、次の会議でまた手作業に戻りがちです。
再発防止として、次の3つを運用に入れます。
1. フォーム入力を選択式に寄せる
カテゴリや希望扱いを自由記述にすると、相談、協議、話し合い のように表記が揺れます。
フォーム側で選択式にしておくと、GAS側の条件分岐が単純になります。
2. 回答シートを直接編集しない
回答シートを手で整えると、次回のフォーム回答が追加されたときに並びや書式が崩れます。
生データは残し、会議用シートは毎回作り直すほうが扱いやすいです。
3. 自動送信は最後にする
最初から送信まで自動化すると、誤ったアジェンダをそのまま配信するリスクがあります。
先に Agenda と Issues を作り、確認すべき点が見える状態にしてから、必要に応じて送信処理を追加するのが現実的です。
まとめ
職員会議の議題募集は、フォームを作るだけでは終わりません。
会議で使うには、カテゴリ順に並べ、担当未入力や期限後投稿を見つけ、似た議題をまとめる必要があります。
GASで回答ログと会議アジェンダを分けておくと、フォーム回答を壊さず、会議前の整形作業を小さくできます。