0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

無料診断フォームの回答からAI導入前の業務棚卸しスコアをGASで作る

0
Last updated at Posted at 2026-06-15

この記事は、無料診断フォームに入った回答をそのまま読むだけで終わらせず、AI導入前にどの業務から整理するべきかをスコア化するための実装メモです。

AI APIは呼びません。Googleフォーム、スプレッドシート、GASだけで、問い合わせ内容を業務棚卸しログへ変換します。

作るもの

Googleフォームの回答シートから、次の3枚を作ります。

  • form_responses: Googleフォームの回答
  • workflow_inventory: 業務棚卸しログ
  • scoring_rules: 優先度スコアのルール

完成形は、AIツール選定表ではありません。

「問い合わせ対応」「FAQ」「CRM」「承認フロー」「データ整理」のどこから着手するべきかを、フォーム回答から判断しやすくするための台帳です。

無料診断フォームから業務棚卸しログを作る流れ

なぜフォーム回答を棚卸しログに変換するのか

無料診断フォームでは、次のような回答が集まりがちです。

  • AIを入れたい業務
  • 困っていること
  • 使っているツール
  • 月間件数
  • 手作業の時間
  • 個人情報や契約情報の有無
  • 誰が最終確認しているか

この回答を文章のまま読むだけだと、毎回判断が属人化します。

たとえば、同じ「問い合わせ対応をAI化したい」という相談でも、状態は大きく違います。

状態 先にやること
FAQがない FAQ候補を作る
問い合わせログがない 受付ログを作る
個人情報が多い AIへ送らない情報を決める
返金や契約が多い 人間確認ルールを作る
件数が少ない 自動化より運用整理を優先する

そのため、フォーム回答を受けた時点で、業務領域、件数、リスク、手戻り、AI向きかどうかを同じ形式に揃えます。

フォーム項目

Googleフォームには、最初は次の項目を置きます。

項目 目的
company_name 株式会社サンプル 相談元
contact_name 山田 連絡担当
workflow_area 問い合わせ対応 業務領域
current_tools Googleフォーム, スプレッドシート 現在のツール
monthly_volume 120 月間件数
manual_minutes_per_case 8 1件あたり手作業分
pain_points 返信が遅れる、FAQがない 困りごと
has_personal_data yes 個人情報の有無
has_contract_or_money yes 契約、料金、返金の有無
has_human_approval no 人間確認ルールの有無
desired_ai_use 返信下書き AIでやりたいこと
notes 自由記述 補足

ポイントは、AIでやりたいことだけでなく、現状のログ、確認者、リスクを聞くことです。

AIに何をさせるかは、業務の入口が見えてから決めた方が安定します。

workflow_inventory シート

GASで作る棚卸しログには、次の列を置きます。

項目 意味
A logged_at 変換日時
B diagnosis_id 診断ID
C company_name 会社名
D workflow_area 業務領域
E monthly_volume 月間件数
F manual_hours_monthly 月間手作業時間
G risk_level low / medium / high
H readiness_level low / medium / high
I priority_score 優先度スコア
J recommended_first_step 最初にやること
K ai_use_candidate AI用途候補
L blocker_notes 先に解くべき課題
M source_row 元フォーム回答行

priority_score は「すぐAI化してよい点数」ではありません。

優先度が高いほど、先に業務整理、ログ整備、FAQ化、人間確認ルール化の価値が大きいという意味で扱います。

scoring_rules シート

スコアの重みはコードに直書きせず、シートで管理します。

rule_id condition_key condition_value score recommended_first_step blocker_note
volume_high monthly_volume_min 100 20 受付ログと分類ルールを作る 件数が多いため判断ログが必要
manual_time_high manual_hours_min 20 20 手作業の内訳を分ける どの作業が重いか分解する
personal_data has_personal_data yes 25 AIへ送らない情報を決める 個人情報の扱いを先に固定
contract_money has_contract_or_money yes 25 人間確認ルールを作る 契約、料金、返金は自動送信しない
no_approval has_human_approval no 15 承認フローを作る 最終確認者が未定
faq_intent desired_ai_use_contains FAQ 10 FAQ候補ログを作る よくある質問を先に集める
reply_draft_intent desired_ai_use_contains 返信下書き 10 返信下書きの承認キューを作る 送信前確認が必要

この表は、最初から完璧にする必要はありません。

診断を重ねながら、よく出る相談に合わせて重みと推奨ステップを見直します。

GASコード

次のコードは、フォーム回答から業務棚卸しログを作る最小サンプルです。

const SHEETS = {
  responses: 'form_responses',
  inventory: 'workflow_inventory',
  rules: 'scoring_rules',
};

const INVENTORY_HEADERS = [
  'logged_at',
  'diagnosis_id',
  'company_name',
  'workflow_area',
  'monthly_volume',
  'manual_hours_monthly',
  'risk_level',
  'readiness_level',
  'priority_score',
  'recommended_first_step',
  'ai_use_candidate',
  'blocker_notes',
  'source_row',
];

function buildWorkflowInventoryFromResponses() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const responseSheet = ss.getSheetByName(SHEETS.responses);
  const inventorySheet = getOrCreateSheet_(ss, SHEETS.inventory, INVENTORY_HEADERS);
  const rules = loadScoringRules_(ss);

  const responses = readObjectsWithRow_(responseSheet);
  const existingSourceRows = loadExistingSourceRows_(inventorySheet);
  const newRows = [];

  responses.forEach(({ rowNumber, data }) => {
    if (existingSourceRows.has(rowNumber)) return;

    const result = scoreResponse_(data, rules);
    newRows.push([
      new Date(),
      buildDiagnosisId_(rowNumber),
      data.company_name || '',
      data.workflow_area || '',
      toNumber_(data.monthly_volume),
      calcManualHoursMonthly_(data),
      result.riskLevel,
      result.readinessLevel,
      result.priorityScore,
      result.firstSteps.join(' / '),
      data.desired_ai_use || '',
      result.blockerNotes.join(' / '),
      rowNumber,
    ]);
  });

  if (newRows.length > 0) {
    inventorySheet
      .getRange(inventorySheet.getLastRow() + 1, 1, newRows.length, INVENTORY_HEADERS.length)
      .setValues(newRows);
  }
}

function scoreResponse_(data, rules) {
  const matched = rules.filter((rule) => matchesRule_(data, rule));
  const priorityScore = matched.reduce((sum, rule) => sum + toNumber_(rule.score), 0);

  return {
    priorityScore,
    riskLevel: resolveRiskLevel_(data, priorityScore),
    readinessLevel: resolveReadinessLevel_(data),
    firstSteps: unique_(matched.map((rule) => rule.recommended_first_step).filter(Boolean)),
    blockerNotes: unique_(matched.map((rule) => rule.blocker_note).filter(Boolean)),
  };
}

function matchesRule_(data, rule) {
  const key = String(rule.condition_key || '').trim();
  const value = String(rule.condition_value || '').trim();

  if (key === 'monthly_volume_min') {
    return toNumber_(data.monthly_volume) >= toNumber_(value);
  }

  if (key === 'manual_hours_min') {
    return calcManualHoursMonthly_(data) >= toNumber_(value);
  }

  if (key === 'has_personal_data') {
    return normalizeYesNo_(data.has_personal_data) === normalizeYesNo_(value);
  }

  if (key === 'has_contract_or_money') {
    return normalizeYesNo_(data.has_contract_or_money) === normalizeYesNo_(value);
  }

  if (key === 'has_human_approval') {
    return normalizeYesNo_(data.has_human_approval) === normalizeYesNo_(value);
  }

  if (key === 'desired_ai_use_contains') {
    return String(data.desired_ai_use || '').includes(value);
  }

  return false;
}

function resolveRiskLevel_(data, priorityScore) {
  if (
    normalizeYesNo_(data.has_personal_data) === 'yes' ||
    normalizeYesNo_(data.has_contract_or_money) === 'yes'
  ) {
    return 'high';
  }
  if (priorityScore >= 40) return 'medium';
  return 'low';
}

function resolveReadinessLevel_(data) {
  const hasTools = String(data.current_tools || '').trim().length > 0;
  const hasApproval = normalizeYesNo_(data.has_human_approval) === 'yes';
  const hasArea = String(data.workflow_area || '').trim().length > 0;

  if (hasTools && hasApproval && hasArea) return 'high';
  if (hasTools && hasArea) return 'medium';
  return 'low';
}

function calcManualHoursMonthly_(data) {
  const volume = toNumber_(data.monthly_volume);
  const minutes = toNumber_(data.manual_minutes_per_case);
  return Math.round((volume * minutes / 60) * 10) / 10;
}

function loadScoringRules_(ss) {
  const sheet = ss.getSheetByName(SHEETS.rules);
  if (!sheet) return [];
  return readObjectsWithRow_(sheet).map(({ data }) => data);
}

function loadExistingSourceRows_(sheet) {
  if (sheet.getLastRow() < 2) return new Set();

  const sourceRowColumn = INVENTORY_HEADERS.indexOf('source_row') + 1;
  const values = sheet.getRange(2, sourceRowColumn, sheet.getLastRow() - 1, 1).getValues();
  return new Set(values.map(([value]) => Number(value)).filter(Boolean));
}

function readObjectsWithRow_(sheet) {
  const values = sheet.getDataRange().getValues();
  if (values.length < 2) return [];

  const headers = values[0].map((header) => String(header).trim());
  return values.slice(1).map((row, index) => {
    const data = {};
    headers.forEach((header, columnIndex) => {
      data[header] = row[columnIndex];
    });
    return {
      rowNumber: index + 2,
      data,
    };
  });
}

function getOrCreateSheet_(ss, name, headers) {
  const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
  if (sheet.getLastRow() === 0) {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  }
  return sheet;
}

function buildDiagnosisId_(rowNumber) {
  const date = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd');
  return `DIA-${date}-${String(rowNumber).padStart(4, '0')}`;
}

function normalizeYesNo_(value) {
  const text = String(value || '').trim().toLowerCase();
  if (['yes', 'true', 'あり', '', 'はい'].includes(text)) return 'yes';
  if (['no', 'false', 'なし', '', 'いいえ'].includes(text)) return 'no';
  return text;
}

function toNumber_(value) {
  const number = Number(value);
  return Number.isFinite(number) ? number : 0;
}

function unique_(items) {
  return [...new Set(items)];
}

フォーム送信時に動かす

フォーム回答が追加された時に棚卸しログを更新したい場合は、Apps Scriptのトリガーで次の設定にします。

項目 設定
実行する関数 buildWorkflowInventoryFromResponses
イベントのソース スプレッドシートから
イベントの種類 フォーム送信時

既存回答をまとめて変換したい場合は、手動で同じ関数を実行します。

source_row を残しているので、同じフォーム回答を重複登録しにくくなります。

スコアの見方

priority_score は、次のように読みます。

score 読み方 次の行動
0から20 低優先 まず相談内容を読む
21から45 中優先 ログ、FAQ、確認者を整える
46以上 高優先 AI化より先に運用ルールを作る

注意したいのは、点数が高いほどAIで自動化しやすい、という意味ではないことです。

点数が高い相談は、改善余地が大きい一方で、個人情報、契約、承認不足などのリスクも含みやすいです。

よくある最初の推奨ステップ

フォーム回答から出す recommended_first_step は、最初は次の程度で十分です。

推奨ステップ 使う場面
受付ログと分類ルールを作る 件数が多いがログが散らばっている
FAQ候補ログを作る 同じ質問が繰り返されている
AIへ送らない情報を決める 個人情報や契約情報が含まれる
人間確認ルールを作る 返信、料金、返金、契約に触れる
承認キューを作る AI下書きを外へ出す前に確認が必要
CRM項目を揃える 顧客メモが後から探せない

この一覧は営業資料ではなく、診断後の作業順を決めるための運用メモです。

AI APIを呼ぶ前に確認すること

この段階では、AI APIを呼ばなくても十分に価値があります。

先に確認するのは次の項目です。

  • フォーム回答に個人情報や認証情報が入っていないか
  • AIに渡してよい項目と伏せる項目が分かれているか
  • 契約、料金、返金、専門判断に見える相談を人間確認に戻せるか
  • 診断結果を誰が読み、次の提案へ進めるか
  • 判断理由がログに残るか

ここが決まっていないままAI要約や返信下書きを入れると、便利さより確認コストが増えることがあります。

まとめ

無料診断フォームは、単なる問い合わせ窓口ではなく、AI導入前の業務棚卸しログにできます。

フォーム回答をスコア化すると、どの業務から整えるべきか、どこに人間確認が必要か、AIを使う前に何を固定すべきかが見えやすくなります。

Miraigentでは、AI導入の相談でも、最初にツール名ではなく業務フロー、ログ、確認ルールを整理します。フォーム回答を棚卸しログへ変換することは、その入口になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?