【Office Scripts×Power Automate】UserFormの代替で完了通知を自動化する

【Office Scripts×Power Automate】UserFormの代替で完了通知を自動化する
目次

はじめに

前回の記事では、VBAの汎用転記エンジンをOffice Scriptsに変換し、実際に動作させるところまで確認しました。

その中で明らかになった課題が、UserForm(プログレスバー・キャンセルボタン・MsgBox)が丸ごと使えないという点です。Office Scriptsはconsole.logしか持たないため、処理が終わったことにすら気付きにくいという弱点がありました。

今回は、この課題をPower Automateと組み合わせることで解決します。具体的には、Office Scriptsの実行結果を戻り値として受け取り、Teamsやメールへ完了通知・エラー通知を自動送信するフローを構築します。

前回の課題のおさらい

前回整理した「変換できなかった機能」のうち、今回扱うのは以下の部分です。

VBAで表示されていた情報Office Scripts単体Power Automate併用
処理完了のお知らせ(MsgBox)❌ 表示不可✅ Teams/メール通知で代替可能
処理件数・HIT件数のサマリー❌ コンソールのみ✅ 通知メッセージに埋め込み可能
エラー発生時のアラート❌ コンソールのみ✅ 条件分岐で通知先を変更可能
リアルタイムの進捗バー(%表示)❌ 表示不可❌ 依然として不可
処理中のキャンセルボタン❌ 表示不可❌ 依然として不可

重要な注意点: Power Automateを組み合わせても、処理中のリアルタイム進捗表示はできません。Power AutomateがOffice Scriptsを呼び出すのは「実行→完了を待つ→結果を受け取る」という一括処理のためです。今回解決できるのは、あくまで開始・完了・エラーのタイミングでの通知です。

全体の仕組み

Power AutomateとOffice Scriptsは、以下の流れで連携します。

Power Automateのトリガー(手動 or スケジュール)
  ↓
「Excel Online (Business)」コネクタの「スクリプトの実行」アクション
  ↓
Office Scriptsが実行され、処理結果をreturn文で返す
  ↓
戻り値を条件分岐(成功/エラー)
  ↓
Teamsへ投稿 or Outlookでメール送信

ポイントは、Office Scripts側でreturn文を使い、処理結果をオブジェクトとして返すことです。Power Automate側はこの戻り値を動的な値(Dynamic content)として受け取り、通知メッセージに埋め込めます。

手順

Step 1:Office Scripts側にreturn文を追加する

前回のmain関数に、処理結果を返すコードを追加します。あわせて、Power Automateから実行する際に必須となるもう1つの修正点があるため、完全なコードで示します。

注意:getActiveWorksheet()はPower Automate実行時に使えない

前回のコードでは、貼付元シートの取得にworkbook.getActiveWorksheet()を使っていました。これはExcel Onlineで自分がファイルを開いて手動実行する場合は問題ありませんが、Power Automate経由で実行する場合は意図したシートを取得できません

Power Automateにはライブの画面操作という概念がないため、getActiveWorksheet()ファイルが最後に保存された時点でアクティブだったシートを返します。保存時にたまたま貼付先シートや別のシートが選択されていると、prefShが意図しないシートになり、K1セルの値(貼付先シート名)が空欄や無関係な値になって後続の処理がすべて失敗します。

対処法は、シート名を直接指定することです。

// 修正前(Excel Onlineの手動実行では動くが、Power Automateでは失敗しうる)
const prefSh = workbook.getActiveWorksheet();

// 修正後:シート名を明示的に指定する
const prefSh = workbook.getWorksheet("貼付元");

この修正を反映した完全なコードは以下のとおりです。

function main(workbook: ExcelScript.Workbook): { count: number; hasError: boolean; message: string } {

  try {
    // ★ Power Automate実行時はアクティブシートが「最後に保存した時点のシート」になるため、
    //   getActiveWorksheet()ではなくシート名を直接指定する
    const prefSh = workbook.getWorksheet("貼付元");
    const sName  = prefSh.getRange("K1").getValue() as string;
    const workSh = workbook.getWorksheet(sName);

    if (!workSh) {
      return { count: 0, hasError: true, message: `シート "${sName}" が見つかりません` };
    }

    // ▼マーク列の自動検出 = VBAの WorksheetFunction.Match("▼", ...)
    const ptCol = findValueInRow(prefSh, 1, "▼");
    const wtCol = findValueInRow(workSh, 1, "▼");

    if (ptCol === -1 || wtCol === -1) {
      return { count: 0, hasError: true, message: "▼マークが見つかりません(2行目を確認してください)" };
    }

    // マッピング列数の取得 = VBAの WorksheetFunction.Max(...)
    const pMCol = getMaxInRow(prefSh, 1);
    const wMCol = getMaxInRow(workSh, 1);

    if (pMCol !== wMCol) {
      return { count: 0, hasError: true, message: "引き当てる列の数が不整合のため中止しました" };
    }

    // データ開始行の取得 = VBAの prefSh.Range("G1") / workSh.Range("G1")
    const pRow = prefSh.getRange("G1").getValue() as number;
    let   wRow = workSh.getRange("G1").getValue() as number;

    // 列マッピング配列の構築 = VBAの pCol() / wCol()
    const pCol: number[] = [];
    let pFlgCol = 0;
    for (let i = 1; i <= pMCol; i++) {
      const col = findValueInRow(prefSh, 1, i);
      pCol.push(col);
      if (i > 1 && col !== pCol[i - 2] + 1) pFlgCol = 1;
    }

    const wCol: number[] = [];
    let wFlgCol = 0;
    for (let i = 1; i <= wMCol; i++) {
      const col = findValueInRow(workSh, 1, i);
      wCol.push(col);
      if (i > 1 && col !== wCol[i - 2] + 1) wFlgCol = 1;
    }

    // 最終行の取得 = VBAの Cells(Rows.Count, col).End(xlUp).Row
    const workShEndR = getLastRow(workSh, wtCol);
    const prefShEndR = getLastRow(prefSh, ptCol);

    // ID照合用にターゲット列を配列として一括取得
    const tgetRng = prefSh
      .getRangeByIndexes(pRow - 1, ptCol, prefShEndR - pRow + 1, 1)
      .getValues();

    // 開始行の上書き指定(N1セル)= VBAの再開始行機能
    const n1Val = prefSh.getRange("N1").getValue();
    if (typeof n1Val === "number" && n1Val > wRow) wRow = n1Val;

    let lngHcount = 0;

    // メインループ = VBAの For tgetTmpR = wRow To workShEndR
    for (let tgetTmpR = wRow; tgetTmpR <= workShEndR; tgetTmpR++) {

      const tmpStr = workSh
        .getRangeByIndexes(tgetTmpR - 1, wtCol, 1, 1)
        .getValue();

      // ID照合 = VBAの WorksheetFunction.Match(tmpStr, tgetRng, 0)
      let matchRng = -1;
      for (let r = 0; r < tgetRng.length; r++) {
        if (tgetRng[r][0] === tmpStr) {
          matchRng = r + pRow;
          break;
        }
      }

      if (matchRng === -1) continue;

      // 転記元データの取得
      const myArray: (string | number | boolean)[] = [];
      if (pFlgCol === 1) {
        for (let i = 0; i < pMCol; i++) {
          myArray.push(
            prefSh.getRangeByIndexes(matchRng - 1, pCol[i], 1, 1)
                  .getValue() as string | number | boolean
          );
        }
      } else {
        const vals = prefSh
          .getRangeByIndexes(matchRng - 1, pCol[0], 1, pMCol)
          .getValues()[0];
        myArray.push(...(vals as (string | number | boolean)[]));
      }

      // 貼付先への書き込み
      if (wFlgCol === 1) {
        for (let i = 0; i < wMCol; i++) {
          workSh.getRangeByIndexes(tgetTmpR - 1, wCol[i], 1, 1)
                 .setValue(myArray[i]);
        }
      } else {
        workSh.getRangeByIndexes(tgetTmpR - 1, wCol[0], 1, pMCol)
               .setValues([myArray]);
      }

      lngHcount++;

      if (tgetTmpR % 1000 === 0) {
        console.log(`処理中... ${tgetTmpR} / ${workShEndR} 件`);
      }
    }

    console.log(`完了!【データ更新件数: ${lngHcount} 件】`);

    // ★ Power Automateに返す結果オブジェクト
    return {
      count: lngHcount,
      hasError: false,
      message: `転記が完了しました(${lngHcount} 件)`
    };

  } catch (e) {
    // ★ エラー発生時もオブジェクトとして返す
    return {
      count: 0,
      hasError: true,
      message: `エラーが発生しました:${e}`
    };
  }
}

// ── ヘルパー関数 ──────────────────────────────────

// 行内で値を検索して列インデックスを返す(0-indexed)
// = VBAの WorksheetFunction.Match の代替
function findValueInRow(
  sheet: ExcelScript.Worksheet,
  rowIndex: number,
  value: string | number
): number {
  const colCount = sheet.getUsedRange().getColumnCount();
  const rowValues = sheet
    .getRangeByIndexes(rowIndex, 0, 1, colCount)
    .getValues()[0];
  return rowValues.indexOf(value);
}

// 行の数値の最大値を返す
// = VBAの WorksheetFunction.Max の代替
function getMaxInRow(sheet: ExcelScript.Worksheet, rowIndex: number): number {
  const colCount = sheet.getUsedRange().getColumnCount();
  const rowValues = sheet
    .getRangeByIndexes(rowIndex, 0, 1, colCount)
    .getValues()[0];
  const nums = rowValues.filter(v => typeof v === "number") as number[];
  return nums.length > 0 ? Math.max(...nums) : 0;
}

// 最終行を返す(1-indexed)
// = VBAの Cells(Rows.Count, col).End(xlUp).Row の代替
function getLastRow(sheet: ExcelScript.Worksheet, colIndex: number): number {
  const rowCount = sheet.getUsedRange().getRowCount();
  const colValues = sheet
    .getRangeByIndexes(0, colIndex, rowCount, 1)
    .getValues();
  for (let r = rowCount - 1; r >= 0; r--) {
    if (colValues[r][0] !== "" && colValues[r][0] !== null) return r + 1;
  }
  return 1;
}
  • 関数の戻り値の型を{ count: number; hasError: boolean; message: string }のように明示しておくと、Power Automate側でも扱いやすくなります
  • 想定される異常系(シート不在・▼マーク未検出など)はhasError: trueにして早期returnする
  • console.logは開発時のデバッグ用として残しておいても問題ありません(Power Automate側の通知には影響しません)
  • 貼付元シートの取得はgetActiveWorksheet()ではなくgetWorksheet("シート名")で直接指定する(Power Automate実行時の必須対応)

Step 2:Power Automateで新しいフローを作成する

  1. ブラウザでmake.powerautomate.comにアクセスし、Microsoft 365の仕事用・学校用アカウントでサインインします
  2. 左側のナビゲーションメニューから「作成」をクリックします
  3. トリガーの選択画面が表示されるので、以下のいずれかを選びます
Power Automate トリガー選択画面
トリガーの種類用途例
指定イベントによるトリガー
(自動化したクラウドフロー)
特定のExcelファイルが更新されたら自動実行したい場合
フローを手動でトリガーする
(インスタントクラウドフロー)
ボタン一つで任意のタイミングで実行したい場合
定期的な実行(Recurrence)
(スケジュール済みクラウドフロー)
毎朝など決まった時間に自動実行したい場合

VBAのようにシート上のボタンから即座に起動したい場合は「フローを手動でトリガーする」が最も近い使用感でしょう。トリガーを選ぶと、空のフロー編集画面が開きます。

フローをトリガーするを選択する
フロー画面に「フローを手動でトリガーする」が追加された画像

Step 3:「スクリプトの実行」アクションを追加する

  1. トリガーの下にある「+ 新しいステップ」をクリックします
  2. 検索ボックスに「Excel Online (Business)」と入力し、コネクタを選択します
  3. アクション一覧から「スクリプトの実行(Run script)」を選択します

設定項目は以下のとおりです。

項目設定内容
場所OneDrive for Business / SharePointサイト など、ファイルの保存先
ドキュメントライブラリファイルが格納されているライブラリ
ファイル対象のExcelブックを選択
スクリプトリストから前回作成したOffice Scriptsを選択
「スクリプトの実行」アクションの設定項目入力

注意点: この機能を使うには、対象のExcelファイルがOneDrive for BusinessまたはSharePoint上に保存されている必要があります。ローカルのデスクトップに保存したファイルやローカル専用のOneDriveフォルダでは選択できません。

Step 4:戻り値を条件分岐で受け取る

  1. 「スクリプトの実行」アクションの下にある「+ 新しいステップ」をクリックします
  2. 条件」(Condition)アクションを検索して追加します
  3. 条件の左辺をクリックすると「動的なコンテンツ」パネルが開くので、一覧からスクリプトの実行の出力 > hasError(boolean型)を選択します
  4. 演算子は「次の値に等しい」、右辺は「true」を指定します
「条件」アクションを追加したフロー
「条件」のパラメーター設定画面
条件:スクリプトの実行の出力 > hasError が「true」に等しい
  Yes(エラーあり)→ エラー通知フローへ
  No(正常終了)  → 完了通知フローへ

条件アクションを追加すると、画面が「Trueの場合」「Falseの場合」の2列に分かれます。それぞれの列に、次のStep 5で通知アクションを追加していきます。

「条件」アクション追加で画面が「Trueの場合」「Falseの場合」の2列に分かれる

戻り値counthasErrormessageは、いずれの列でも「動的なコンテンツ」パネルからドラッグ&ドロップで参照できます。

Step 5:Teamsまたはメールで通知する

条件分岐のそれぞれの分岐に、通知アクションを追加します。

「Falseの場合」列(正常終了)に成功通知を追加する

  1. 「Falseの場合」列の「+ 新しいステップ」から「Microsoft Teams」コネクタを検索します
  2. フローボットがチャネルにメッセージを投稿する」(またはお使いの環境に応じたTeams投稿アクション)を選択します
  3. 投稿先のチーム・チャネルを指定し、メッセージ本文に動的なコンテンツを組み込みます

「Trueの場合」列(エラー発生)にエラー通知を追加する

  1. 「Trueの場合」列の「+ 新しいステップ」から「Office 365 Outlook」コネクタを検索します
  2. メールの送信(V2)」アクションを選択します
  3. 宛先・件名・本文を設定します

{count}{message}の部分は、Step 4で取得した「動的なコンテンツ」の戻り値です。

最後に画面右上(または左下)の「保存」をクリックしてフローを保存すれば設定は完了です。手動トリガーの場合は「テスト」から実際に実行して、Teams・メールに通知が届くか確認します。

Teamsに届いた通知を確認した画像

できること・できないことの整理

実現したいことPower Automate併用
処理完了をTeams/メールで通知✅ 可能
処理件数・結果サマリーの通知✅ 可能(戻り値に含めればそのまま埋め込める)
エラー発生時のみ担当者に通知✅ 可能(条件分岐で通知先を分岐)
毎朝決まった時間に自動実行✅ 可能(Recurrenceトリガー)
ファイル更新をきっかけに自動実行✅ 可能(SharePoint/OneDriveのトリガー)
処理中のリアルタイム進捗(%表示)❌ 不可(一括実行・一括結果取得のため)
処理途中のキャンセルボタン❌ 不可
処理中のダイアログ表示❌ 不可

まとめると: Power Automateが解決できるのは「処理の前後」(開始・完了・エラー)のコミュニケーションであり、VBAのUserFormが担っていた「処理」のリアルタイム表示は代替できません。この制約は設計上の前提として理解しておく必要があります。

よくある質問・エラー対処

Q. 「スクリプトの実行」アクションでスクリプトの一覧に自分のスクリプトが表示されない

→ Office Scriptsは対象のExcelファイルに紐づいているのではなく、Excel Online側のスクリプトライブラリに保存されています。対象ファイルを開いた状態で作成・保存したスクリプトのみが選択肢に表示されるため、正しいファイルでスクリプトを作成したか確認してください。

Q. ローカルに保存したExcelファイルでは使えない?

→ 使えません。「スクリプトの実行」アクションはOneDrive for BusinessまたはSharePoint上のファイルのみが対象です。ローカルファイルを使いたい場合は、事前にOneDriveへ同期・アップロードしておく必要があります。

Q. Power Automateは無料で使える?

→ Microsoft 365のライセンスに含まれる「Power Automate for Microsoft 365」であれば、追加費用なしで基本的なフローが作成できます。ただし1日あたりの実行回数に上限があるため、頻繁な自動実行を想定する場合はライセンス条件を確認してください。

Q. 戻り値の型が反映されない・エラーになる

→ Office Scripts側の関数シグネチャで戻り値の型を明示していない場合、Power Automate側で「動的なコンテンツ」として認識されないことがあります。function main(workbook: ExcelScript.Workbook): { ... }のように型を明示してください。

Q. 「アクション ‘スクリプトの実行’ に失敗しました:Bad Request」「getWorksheet」「InvalidArgument」というエラーが出る

workbook.getActiveWorksheet()で取得したシートのK1セルの値(貼付先シート名)が空欄・不正な値になっている可能性が高いです。Power Automate実行時はgetActiveWorksheet()が「ファイルを最後に保存した時点でアクティブだったシート」を返すため、想定と異なるシートが取得されることがあります。getWorksheet("シート名")のようにシート名を直接指定してください(Step 1のコード参照)。

まとめ

  • Office ScriptsのUserForm非対応という弱点は、Power Automateとの連携で「前後」の通知に限り代替できる
  • Office Scripts側でreturn文を使い、処理結果をオブジェクトとして返すのがポイント
  • Power Automate実行時はgetActiveWorksheet()が意図したシートを返さないため、シート名を直接指定する必要がある
  • Power Automateの「スクリプトの実行」アクションを使うには、ファイルがOneDrive for BusinessかSharePoint上にある必要がある
  • 条件分岐で戻り値のhasErrorを判定すれば、成功時とエラー時で通知先・内容を出し分けられる
  • リアルタイムの進捗表示やキャンセルボタンは、この方法でも実現できない点は設計時に理解しておく

次回予告: 「実現できない」とした処理中のリアルタイム進捗表示・キャンセルボタンについて、次回はワークシートのセルを仲介にした代替パターン(疑似進捗表示・協調的キャンセル)を掘り下げます。

関連記事

当サイトの記事で使用したVBAなどのサンプルをDLできます

ダウンロードページへは下のカードをクリックすればジャンプできます。
よろしければご利用ください!

よかったらシェアしてね!
  • URLをコピーしました!
目次