Skip to content

Teams 通知・Asana タスク連携 ​

第7版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

プリザンター標準の Teams 通知(通知種別 Teams)は Microsoft Teams の Incoming Webhook コネクタを利用していたため、2025 年のコネクタ廃止により使えなくなっています。代替は Workflows(Power Automate)の Webhook + HttpClient 通知か、チャネルのメールアドレスへのメール通知です。 あわせて、コマンドボタンと拡張サーバースクリプトだけで Asana にタスクを登録する方法も紹介します。どちらも API トークンや Webhook URL はサーバ側に置き、ブラウザに出さない構成です。

Teams への通知 ​

なぜ Teams 通知が使えなくなったのか ​

Teams 通知は Incoming Webhook URL に { "text": "通知メッセージ" } のような JSON を POST して動作していました。Teams 側の Incoming Webhook コネクタが廃止され、この URL が無効になったため、通知が届かなくなっています。

WARNING

既存の Incoming Webhook URL は段階的に廃止されています。まだ動作している場合でも、早めの移行をおすすめします。

代替策の比較 ​

比較項目代替策 1: Workflows + HttpClient代替策 2: メール投稿
準備Workflows 作成 + パラメータ変更SMTP の設定のみ
通知の見た目Adaptive Card でリッチな表示が可能メール形式(プレーンテキスト)
柔軟性サーバースクリプトで高度な制御も可能標準のメール通知の範囲
難易度やや高い低い

手軽さ優先ならメール投稿、リッチな通知や柔軟な制御が必要なら Workflows + HttpClient がおすすめです。

代替策 1: Workflows + HttpClient 通知 ​

Teams の Workflows(Power Automate)で Webhook の受け口を作り、プリザンターの HttpClient 通知から POST します。

図を読み込み中…

1. Teams でワークフローを追加する ​

  1. Teams の通知先チャネルを開く
  2. チャネル名の右にある「…」(その他のオプション)をクリックする
  3. 「ワークフロー」を選択する
  4. テンプレートから「Webhook 要求を受信するとチャネルに投稿する」を選択する
  5. フローに名前をつけて「次へ」をクリックする
  6. 投稿先のチームとチャネルを確認し「ワークフローを追加」をクリックする

作成されると Webhook URL が表示されるのでコピーします。

text
https://<region>.logic.azure.com:443/workflows/<id>/triggers/manual/paths/invoke?api-version=...&sp=...&sv=...&sig=...

WARNING

この URL には認証情報が含まれています。外部に公開しないように管理してください。

既定のテンプレートではプレーンテキストの投稿になります。Adaptive Card を使う場合は、Power Automate のフロー編集画面で「チャットまたはチャネルでメッセージを投稿する」アクションを選択し、必要に応じて Adaptive Card JSON を設定します。

2. HttpClient 通知を有効化する ​

HttpClient 通知は既定で無効です。App_Data/Parameters/Notification.json の HttpClient を true にします(既定値の Notification.json)。

json
{
    "Teams": true,
    "HttpClient": true
}

変更後、パラメータを再読み込みします。特権ユーザでログインして https://<your-pleasanter-url>/admins/reloadparameters にアクセスするか、サービスを再起動します。

3. テーブルの通知設定 ​

対象テーブルの「テーブルの管理」→「通知」タブで「新規作成」し、次のように設定します。

項目設定値
通知種別HttpClient
アドレス手順 1 で取得した Workflows の Webhook URL
メソッドPost
Content-Typeapplication/json
エンコードutf-8

「カスタムデザインを使う」にチェックを入れ、フォーマットに JSON を設定します。

json
{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","contentUrl":null,"content":{"$schema":"http://adaptivecards.io/schemas/adaptive-card.json","type":"AdaptiveCard","version":"1.2","body":[{"type":"TextBlock","text":"レコードが更新されました","weight":"Bolder","size":"Medium"},{"type":"TextBlock","text":"更新者: {UserName}","wrap":true},{"type":"TextBlock","text":"{Url}","wrap":true}]}}]}

複数行の JSON でもよい

カスタムフォーマットの本文は改行で分割され、各行の前後の空白を除いてから改行(\n)でつなぎ直されます(ResultModel.cs)。JSON では改行は空白と同じ扱いなので、複数行に整形した JSON もそのまま有効な JSON として送られます。ただし、ある 1 行だけで "Name":"[Title]" のような項目の書式として読める JSON オブジェクトになっていると、その行は項目の値に置き換わります。詳しくは 通知のカスタムフォーマットとリマインダーの内部動作 を参照してください。

使えるプレースホルダは 4 種類だけ

カスタムフォーマットで使えるプレースホルダは {Url}・{UserName}・{MailAddress}・{LoginId} の 4 種類のみです。{Title} や {Status} のようなカラム値は置換されません。カラム値を含めたい場合は、次のサーバースクリプトを使います。

HttpClient 通知の送信処理

HttpClient 通知は設定値(エンコード・ヘッダ・メソッド)を解釈したあと、送信そのものは Task.Run で別スレッドに投げます。失敗しても SysLogs に例外が記録されるだけで、再試行はしません(HttpClient.cs)。メソッドが未設定なら POST、ヘッダは「ヘッダ」欄の JSON の辞書(例: {"Authorization":"Bearer xxx"})をそのまま付けます。接続は静的な HttpClient を共有し、2xx 以外の応答は例外として記録されます(NotificationHttpClient.cs)。通知の「トークン」欄は HttpClient では使われません(NotificationUtilities.cs)。本文テンプレートや認証方式を持つ Webhook 種別を足す改修案は Webhook 通知(送信)の改修案 にまとめています。

4. サーバースクリプトから送る ​

サーバースクリプトの httpClient を使えば、プレースホルダの制限なく Adaptive Card を組み立てられ、model.Title や context.UserName なども埋め込めます。実用的にはこちらがおすすめです。

js
// Workflows Webhook URL
const webhookUrl = 'https://<region>.logic.azure.com:443/workflows/<id>/triggers/manual/paths/invoke?api-version=...&sp=...&sv=...&sig=...';

// 通知本文の組み立て
const payload = JSON.stringify({
    type: 'message',
    attachments: [{
        contentType: 'application/vnd.microsoft.card.adaptive',
        contentUrl: null,
        content: {
            '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json',
            type: 'AdaptiveCard',
            version: '1.2',
            body: [
                {
                    type: 'TextBlock',
                    text: '[プリザンター] ' + model.Title + 'が更新されました',
                    weight: 'Bolder',
                    size: 'Medium'
                },
                {
                    type: 'TextBlock',
                    text: '更新者: ' + context.UserName,
                    wrap: true
                }
            ],
            actions: [
                {
                    type: 'Action.OpenUrl',
                    title: 'レコードを開く',
                    url: context.AbsoluteUri
                }
            ]
        }
    }]
});

// HTTPリクエストの組み立てと送信
httpClient.RequestUri = webhookUrl;
httpClient.Content = payload;
httpClient.MediaType = 'application/json';
httpClient.Post();

WARNING

httpClient.ResponseHeaders は送信のたびに内部でクリアされる(ServerScriptModelHttpClient.cs)ので、事前に Clear() する必要はありません。httpClient を複数回使う場合に残るのは RequestHeaders・Content・MediaType などの送信側の設定なので、使う直前に設定し直します。

通知設定の HttpClient と、サーバースクリプトの notifications は別物

ここでの HttpClient は「テーブルの管理 → 通知」で設定する通知です。サーバースクリプトの notifications.New() で Type = 9(HttpClient)を送ろうとすると例外になり使えないことが確認されています(サーバースクリプトから生成 AI を使う を参照)。サーバースクリプトから Webhook に送る場合は、上のように httpClient を直接使います。

代替策 2: チャネルへのメール投稿 ​

Teams のチャネルにはメールアドレスを割り当てられるため、プリザンター標準のメール通知で Teams に投稿できます。Workflows や HttpClient の設定が不要で、もっとも手軽な方法です。

図を読み込み中…

  1. Teams の通知先チャネルで「…」→「メール アドレスを取得」を選択し、表示されたアドレスをコピーする
  2. 対象テーブルの「テーブルの管理」→「通知」タブで「新規作成」する
  3. 通知種別を「メール」にし、アドレス欄にチャネルのメールアドレスを設定する

WARNING

メール投稿には SMTP の設定が必要です。投稿はメール形式のため、Adaptive Card のようなリッチな表示はできません。

INFO

組織のセキュリティポリシーによっては外部からのメール受信がブロックされている場合があります。Teams 管理センターでチャネルメールの設定を確認してください。

サーバースクリプトの notifications でメールを送ることもできます。ただし、サーバースクリプトからの送信は通知設定側の実行条件(作成後・更新後など)による制御が効かず、スクリプトの実行タイミングで条件を管理する必要があるため、特別な理由がなければ通知設定の利用をおすすめします。

js
const notification = notifications.New();
notification.Type = 1; // 1 = Mail
notification.Address = '<channel-email>@<tenant>.teams.ms';
notification.Title = '[プリザンター] ' + model.Title + 'が更新されました';
notification.Body = '更新者: ' + context.UserName;
notification.Send();

既存の Teams 通知からの移行手順 ​

  1. 代替策 1 または 2 を選ぶ
  2. 選んだ方法のセットアップを行う
  3. 既存の Teams 通知設定を無効化する
  4. 新しい通知設定を追加する
  5. テスト送信で動作を確認する
  6. 問題なければ旧設定を削除する

既存の Teams 通知(Type が 6)がどのサイトにあるかは、SQL で抽出できます。

sql
SELECT [SiteId], [Title]
FROM [Sites]
CROSS APPLY OPENJSON([SiteSettings], '$.Notifications')
WHERE JSON_VALUE(value, '$.Type') = '6'
sql
SELECT s."SiteId", s."Title"
FROM "Sites" s
CROSS JOIN LATERAL jsonb_array_elements(s."SiteSettings"::jsonb->'Notifications') n(value)
WHERE n.value->>'Type' = '6'
sql
SELECT s.`SiteId`, s.`Title`
FROM `Sites` s
CROSS JOIN JSON_TABLE(s.`SiteSettings`, '$.Notifications[*]' COLUMNS (
    `Type` INT PATH '$.Type'
)) n
WHERE n.`Type` = 6

Asana へのタスク登録 ​

一覧画面と編集画面に「Asana に登録」ボタンを追加し、ボタンから Asana にタスクを登録します。ボタンはサイト設定のコマンドボタン機能で追加し、Asana API の呼び出しは拡張サーバースクリプトで行います。

仕組み ​

図を読み込み中…

  1. ユーザが「Asana に登録」ボタンをクリックする
  2. ブラウザにプロジェクト・担当者の選択ダイアログを表示する
  3. 選択後、レコード更新 API を ?asana=1&project=GID&assignee=GID というクエリパラメータ付きで呼び出す
  4. AfterUpdate の拡張サーバースクリプトがクエリパラメータの有無を見て、httpClient で Asana API にタスクを登録する
  5. ブラウザ側で更新 API のステータスに応じてメッセージを表示する

API トークンはサーバースクリプト(サーバ側)にだけ置かれるため、ブラウザには露出しません。

Asana API の準備 ​

連携用サービスアカウント ​

個人アカウントのトークンではなく、連携専用のサービスアカウントを用意することをおすすめします。

  • 担当者の異動・退職でトークンが無効になるリスクを避けられる
  • 連携に必要な最小限の権限だけを付与できる
  • アクセスログでシステム連携と個人操作を区別できる

Asana の組織管理者がサービスアカウントを作成し、連携対象プロジェクトにメンバーとして招待しておきます。

INFO

Asana のサービスアカウントは、API 連携専用のアカウントとして API 利用規約 で認められています。ただし、複数の人間が 1 つのアカウントにログインして共有する使い方は ユーザー利用規約 で禁止されています。サービスアカウントはシステム連携専用とし、トークンの安全管理と最小権限の付与を徹底してください。

パーソナルアクセストークン(PAT) ​

サービスアカウントで Asana にログインし、https://app.asana.com/0/my-apps で「Create new token」からトークンを発行して、安全な場所に保管します。

プロジェクト GID・メンバー GID ​

プロジェクト GID は、プロジェクトページの URL https://app.asana.com/0/{プロジェクトGID}/list に含まれています。担当者のユーザ GID は Asana API で取得できます(ワークスペース GID は管理コンソール画面の URL などから確認)。

bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://app.asana.com/api/1.0/workspaces/{ワークスペースGID}/users?opt_fields=name,email"

レスポンスの gid がユーザの GID です。

json
{
  "data": [
    { "gid": "1234567890123", "name": "ユーザーA", "email": "user-a@example.com" },
    { "gid": "1234567890124", "name": "ユーザーB", "email": "user-b@example.com" }
  ]
}

拡張サーバースクリプト(Asana API 呼び出し) ​

context.QueryStrings.Data('asana') でクエリパラメータを確認し、プロジェクト GID と担当者 GID もクエリパラメータから受け取ります。

json
{
    "AfterUpdate": true,
    "Functionalize": true
}

本文は同じフォルダの AsanaPost.json.js に書きます。.json.js があると、その内容が Body として読み込まれます(Initializer.cs)。スクリプトの先頭で return を使うので、Functionalize を true にして関数として実行させます。サーバースクリプトの本文はそのまま V8 で実行され、Functionalize が無いと関数の外の return は構文エラーになります(ServerScriptUtilities.cs)。

js
// ?asana=1 のときだけ処理
if (context.QueryStrings.Data('asana') !== '1') return;

var personalAccessToken = 'YOUR_SERVICE_ACCOUNT_ACCESS_TOKEN';

// プロジェクトGIDと担当者GIDをクエリパラメータから取得
var projectGid = context.QueryStrings.Data('project');
var assigneeGid = context.QueryStrings.Data('assignee');

if (!projectGid) return;

var taskData = {
    name: model.Title,
    notes: model.Body || '',
    projects: [projectGid]
};

// 担当者が指定されている場合のみ設定
if (assigneeGid) {
    taskData.assignee = assigneeGid;
}

var payload = JSON.stringify({ data: taskData });

httpClient.RequestUri = 'https://app.asana.com/api/1.0/tasks';
httpClient.Content = payload;
httpClient.MediaType = 'application/json';
httpClient.RequestHeaders.Clear();
httpClient.RequestHeaders.Add('Authorization', 'Bearer ' + personalAccessToken);
httpClient.Post();

WARNING

このサーバースクリプトは更新 API から呼び出されるため、context.AddResponse によるメッセージ返却は使えません(更新 API のレスポンスは {Id, StatusCode, Message} の固定形式です)。メッセージの表示はクライアント側のスクリプトで行います。

編集画面のコマンドボタン ​

「テーブルの管理」→「エディタ」のコマンドボタンで「追加」し、名称を「Asanaに登録」、スクリプト欄に次を設定します。先頭の asanaProjects と asanaUsers に、確認した GID を設定してください。

js
// --- Asana プロジェクト・担当者の設定 ---
var asanaProjects = [
    { gid: '1234567890123', name: 'プロジェクトA' },
    { gid: '1234567890124', name: 'プロジェクトB' }
];
var asanaUsers = [
    { gid: '1234567890125', name: 'ユーザーA' },
    { gid: '1234567890126', name: 'ユーザーB' }
];
// --- 設定ここまで ---

var dialog = document.createElement('dialog');
dialog.style.cssText = 'padding:20px;border:1px solid #ccc;border-radius:8px;min-width:320px';
dialog.innerHTML = '<form method="dialog">'
    + '<h3 style="margin-top:0">Asanaにタスクを登録</h3>'
    + '<div style="margin-bottom:12px"><label>プロジェクト:</label><br>'
    + '<select id="asana-project" style="width:100%;padding:4px">'
    + asanaProjects.map(function (p) {
        return '<option value="' + p.gid + '">' + p.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="margin-bottom:16px"><label>担当者:</label><br>'
    + '<select id="asana-assignee" style="width:100%;padding:4px">'
    + '<option value="">(未指定)</option>'
    + asanaUsers.map(function (u) {
        return '<option value="' + u.gid + '">' + u.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="text-align:right">'
    + '<button type="button" onclick="this.closest(\'dialog\').close()"'
    + ' style="margin-right:8px">キャンセル</button>'
    + '<button type="submit" value="ok">登録</button></div></form>';
document.body.appendChild(dialog);
dialog.showModal();
dialog.addEventListener('close', function () {
    var ok = dialog.returnValue === 'ok';
    var projectGid = dialog.querySelector('#asana-project').value;
    var assigneeGid = dialog.querySelector('#asana-assignee').value;
    dialog.remove();
    if (!ok) return;
    var url = $p.apiUrl($p.getId(), 'update')
        + '?asana=1&project=' + projectGid;
    if (assigneeGid) url += '&assignee=' + assigneeGid;
    $p.apiExec(url, {
        data: {},
        done: function () {
            $p.setMessage('#Message', JSON.stringify({
                Text: 'Asanaにタスクを登録しました',
                Css: 'alert-success'
            }));
        },
        fail: function () {
            $p.setMessage('#Message', JSON.stringify({
                Text: 'Asana登録に失敗しました',
                Css: 'alert-error'
            }));
        }
    });
});

一覧画面のコマンドボタン ​

「テーブルの管理」→「一覧」のコマンドボタンで同様に追加します。ダイアログ部分は編集画面と同じで、チェックした行ごとに更新 API を呼び出す点が異なります。

js
// …(asanaProjects / asanaUsers の設定は編集画面と同じ)…

var checkedRows = $('.grid-check:checked');
if (checkedRows.length === 0) {
    $p.setMessage('#Message', JSON.stringify({ Text: 'レコードをチェックしてください', Css: 'alert-warning' }));
    return false;
}

// …(ダイアログの生成は編集画面と同じ。見出しに件数 checkedRows.length を表示)…

dialog.addEventListener('close', function () {
    var ok = dialog.returnValue === 'ok';
    var projectGid = dialog.querySelector('#asana-project').value;
    var assigneeGid = dialog.querySelector('#asana-assignee').value;
    dialog.remove();
    if (!ok) return;
    var processed = 0;
    var failed = 0;
    checkedRows.each(function () {
        var itemId = $(this).closest('tr').data('id');
        var url = $p.apiUrl(itemId, 'update')
            + '?asana=1&project=' + projectGid;
        if (assigneeGid) url += '&assignee=' + assigneeGid;
        $p.apiExec(url, {
            data: {},
            fail: function () { failed++; },
            always: function () {
                processed++;
                if (processed === checkedRows.length) {
                    if (failed > 0) {
                        alert(failed + '件の登録に失敗しました');
                    }
                    location.reload();
                }
            }
        });
    });
});

チェックした行から data('id') でレコード ID を取得し、1 件ずつ更新 API を呼び出して、すべて完了したら画面をリロードします。

更新 API は POST だけを受け付ける(ItemsController.cs)ため、PUT で送る $.ajax から、POST で送る $p.apiExec(_api.js)に修正しています。$p.apiExec は画面の #Token も送信データに含めます。また $p.message という関数は無いため、$p.setMessage(第 2 引数は JSON 文字列。message.js)に修正しています。

API の URL はヘルパーで組み立てる ​

サブディレクトリ配置(例: https://example.com/pleasanter/)では、/api/items/... のように / 始まりで直書きするとパスが一致しなくなります。次のヘルパーを使えば ApplicationPath(例: /pleasanter/)が自動的に補完されます。

場面スクリプト(クライアント側)サーバースクリプト
API URL$p.apiUrl(id, 'update')context.ApplicationPath + 'api/items/' + id + '/update'
ルートパス$('#ApplicationPath').val()context.ApplicationPath
コントローラーベース$('#BaseUrl').val()context.ApplicationPath + context.Controller + '/'

$p.apiUrl(id, action) は内部で $('#ApplicationPath').val() を参照しています。

context.ApplicationPath はホスト名を含まないパスです。サーバースクリプトの httpClient.RequestUri には絶対 URL が必要なので、この値だけでは API を呼べません。サーバースクリプトからレコードを操作するときは、httpClient ではなく items.Update などの items オブジェクトを使います。

関連ページ ​

変更履歴

第7版Teams 通知設定の取得 SQL を3種類のDBMSに対応
第6版通知とリマインダーの書式・置き換わらない書き方・タイムゾーンの影響、システムログの外部通知の解説と、関連する改修・設計メモを追加
第5版外部連携の改修・設計メモ(iCal・RSS/Atom・Webhook 送受信・iPaaS・短縮 URL・POP 受信・マスターデータ同期)を追加
第4版「外部連携・AI」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「外部連携・AI」セクションの記事を追加