はじめに
hkob の雑記録の第517回目(連続90日目)は、Teams から Notion の日付なしタスクを生成する Webhook の実装を進めていきましたが、最後にライセンスの壁にぶち当たってしまった件を解説します。
TL;DR
先に書いておきますが、今回の実装は失敗に終わりました。ワークフローの設定まで終わり、保存した段階で Power Automate Premium ライセンスが必要と出て来ました。そういえば数年前もこれでハマったことを今頃思い出しました。

途中までの作業記録
なんだかせっかくここまで実装してきたので、もったいないのでそのまま貼っておきます。使わないものにコメントをつけるのも面倒なので、ログそのままです。
作業手順書(Teams → Notion Tasks 手動取り込み)
目的
Microsoft Teams の特定メッセージを、手動フロー(Power Automate)実行により Notion の Tasks(Tasks)へタスク登録する。見落とし防止・GTD の Inbox 化。
完成形(期待する動作)
- Teams 上で「タスク化したいメッセージ」を選ぶ
- Power Automate の手動フローを実行する
- Worker(Webhook)が冪等性を保証しつつ Notion Tasks に 1 件のタスクを作成する
- 同一メッセージを再実行しても Tasks は重複しない(External ID により防止)
0. 前提チェック
- Tasks データソースに External ID(冪等性キー)プロパティが存在すること
- Worker(Notion Workers)が外部から到達可能な Webhook エンドポイントを持つこと
- Power Automate で HTTP(POST)アクションが利用できること
1. Worker(Webhook)を先に実装(エンドポイント確定)
Notion Workers では Webhook 作成時にエンドポイント(URL)が自動発行される。したがって、Power Automate 側を作る前に Worker を deploy して Webhook URL を確定する(moodle webhook と同じ順序)。
1.1 ローカル作業ディレクトリを作成
mkdir -p ~/Dropbox/workers/teams-webhookcd ~/Dropbox/workers/teams-webhook
実行結果
hkob@hM5Air ~> mkdir -p ~/Dropbox/workers/teams-webhook hkob@hM5Air ~> cd ~/Dropbox/workers/teams-webhook hkob@hM5Air ~/D/w/teams-webhook>
1.2 Worker 雛形を作成
ntn workers new .- git 初期化 / 依存関係インストールは moodle と同様
- (必要なら)
ntn update
実行結果
ntn workers new . ✔ Template downloaded ✔ Initialize a git repository? · yes ✔ Install dependencies? · yes ✔ Template files extracted ✔ Git repository initialized ✔ Dependencies installed with npm ✔ Worker project created Next steps: 1. ntn workers deploy
1.3 src/index.ts を実装(受信 → 認証 → 冪等性 → Tasks 作成)
src/index.ts(ひな形)
以下をそのまま src/index.ts に貼り付けてから、必要に応じて調整する(Tasks のプロパティ名はこのワークスペースの Tasks スキーマに合わせてある)。
補足: VSCode で crypto が見つからない場合(Node 型定義)
名前 'crypto' が見つかりません が出る場合は、Node.js の型定義を追加する。
npm i -D @types/node- (必要なら)
tsconfig.jsonのcompilerOptions.typesに"node"を追加
実行結果
npm i -D @types/node added 1 package, and audited 16 packages in 755ms 2 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
実行結果(tsconfig.json 変更)
{ "compilerOptions": { "target": "ES2020", "module": "nodenext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "nodenext", "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
import crypto from "crypto"; import { Worker, WebhookVerificationError } from "@notionhq/workers"; const worker = new Worker(); export default worker; /** * Power Automate から送られてくる Teams メッセージの正規化ペイロード(想定) */ type TeamsWebhookPayload = { event?: string; occurredAt?: string; tenantId?: string; conversation?: { type?: "channel" | "chat" | string; teamId?: string; teamName?: string; channelId?: string; channelName?: string; chatId?: string; }; message?: { id?: string; webUrl?: string; subject?: string; text?: string; from?: { id?: string; displayName?: string; }; }; }; function requireEnv(name: string): string { const v = process.env[name]; if (!v) throw new Error(`${name} not configured`); return v; } function verifySharedSecret(headers: Record<string, string>): void { const secret = requireEnv("TEAMS_WEBHOOK_SECRET"); // Workers の headers は lowercase 化されている前提で扱う const got = headers["x-webhook-secret"] ?? ""; if (!got) throw new WebhookVerificationError("Missing X-Webhook-Secret"); if (got.length !== secret.length) throw new WebhookVerificationError("Invalid X-Webhook-Secret"); if (!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(secret))) { throw new WebhookVerificationError("Invalid X-Webhook-Secret"); } } function buildExternalId(body: TeamsWebhookPayload): string { const c = body.conversation ?? {}; const m = body.message ?? {}; const convType = (c.type ?? "unknown") as string; const messageId = (m.id ?? "").trim(); if (!messageId) throw new Error("message.id is required"); if (convType === "channel") { const teamId = (c.teamId ?? "").trim(); const channelId = (c.channelId ?? "").trim(); if (!teamId || !channelId) throw new Error("conversation.teamId/channelId is required for channel"); return `teams:channel:${teamId}:${channelId}:message:${messageId}`; } if (convType === "chat") { const chatId = (c.chatId ?? "").trim(); if (!chatId) throw new Error("conversation.chatId is required for chat"); return `teams:chat:${chatId}:message:${messageId}`; } // どちらでもない場合は最低限 messageId で収束させる(後で厳格化してもよい) return `teams:${convType}:message:${messageId}`; } function truncate(s: string, n: number): string { return s.length > n ? s.slice(0, n - 1) + "…" : s; } worker.webhook("onTeamsMessageToNotionTask", { title: "Teams message → create Notion task", description: "Receives a Teams message payload (via Power Automate) and creates a row in Tasks DB with idempotency.", execute: async (events: any[], { notion }: any) => { const tasksDataSourceId = requireEnv("TASKS_DATA_SOURCE_ID"); const assigneeUserId = process.env.ASSIGNEE_USER_ID ?? ""; // 任意(固定アサインする場合のみ) for (const event of events) { verifySharedSecret(event.headers as Record<string, string>); const body = event.body as TeamsWebhookPayload; // 最低限のバリデーション if (!body?.conversation?.type) throw new Error("conversation.type is required"); if (!body?.message?.id) throw new Error("message.id is required"); if (!body?.message?.webUrl) throw new Error("message.webUrl is required"); if (!body?.message?.from?.displayName) throw new Error("message.from.displayName is required"); if (!body?.occurredAt) throw new Error("occurredAt is required"); const externalId = buildExternalId(body); // Task title const fromName = (body.message?.from?.displayName ?? "").trim(); const subject = (body.message?.subject ?? "").trim(); const text = (body.message?.text ?? "").trim(); const head = subject || truncate(text.replace(/\s+/g, " "), 60); const taskName = `【Teams】${fromName}: ${head || externalId}`; const link = (body.message?.webUrl ?? "").trim(); const summary = truncate(text, 2000); // 冪等性チェック(External ID) const existing = await notion.dataSources.query({ data_source_id: tasksDataSourceId, filter: { property: "External ID", rich_text: { equals: externalId } }, page_size: 1, }); if (existing.results.length > 0) continue; // Tasks 作成(最小プロパティ) await notion.pages.create({ parent: { data_source_id: tasksDataSourceId }, properties: { "Task name": { title: [{ text: { content: taskName } }] }, ...(summary ? { Summary: { rich_text: [{ text: { content: summary } }] } } : {}), ...(link ? { Link: { url: link } } : {}), "External ID": { rich_text: [{ text: { content: externalId } }] }, ...(assigneeUserId ? { Assignee: { people: [{ id: assigneeUserId }] } } : {}), }, }); } }, });
認証(最小構成: 共有シークレット)
- Header
X-Webhook-Secretを検証し、不一致なら401 Unauthorized
入力バリデーション(最低限)
conversation.typemessage.idmessage.webUrlmessage.from.displayNameoccurredAtmessage.text(保存方針により省略可)
冪等性(External ID)
teams:channel:<teamId>:<channelId>:message:<messageId>teams:chat:<chatId>:message:<messageId>
Tasks への作成(最小プロパティ)
- Task name / Summary / Link / External ID(必要なら Date)
1.4 deploy(Webhook URL を発行させる)
ntn workers deploy- 目的: Worker を作成/更新し、
workers.jsonを生成し、Webhook URL を確定させる
- 目的: Worker を作成/更新し、
ntn workers webhooks list- 発行された Webhook URL(https://...)を控える(Power Automate の送信先に使う)
実行結果
ntn workers deploy Deploying to workspace: hkob's ambassador workspace ✔ Worker name · teams-webhook [build:out] Running npm install... [build:out] added 14 packages, and audited 15 packages in 1s [build:out] 2 packages are looking for funding [build:out] run `npm fund` for details [build:out] found 0 vulnerabilities [build:err] npm notice [build:err] npm notice New major version of npm available! 10.9.7 -> 11.16.0 [build:err] npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.16.0 [build:err] npm notice To update run: npm install -g npm@11.16.0 [build:err] npm notice [build:out] Running npm run build... [build:out] > @notionhq/workers-template@0.0.0 build [build:out] > tsc [build:out] Running npm install --omit=dev... [build:out] up to date, audited 9 packages in 358ms [build:out] 2 packages are looking for funding [build:out] run `npm fund` for details [build:out] found 0 vulnerabilities [build:out] Moving package.json... [build:out] Moving package-lock.json... [build:out] Moving node_modules... [build:out] Creating bundle from dist... [build:out] Uploading bundle... [build:out] Bundle upload complete. [build:out] Preparing runtime filesystem for snapshot... ✔ Worker created Webhooks: + onTeamsMessageToNotionTask Webhook URLs: → https://www.notion.so/webhooks/worker/d08cad25-f90c-4f24-832c-38049a6b0b3a/019eac17-67e5-7867-83fe-bd77124d0d00/VJii1qhslrMjAtK0/onTeamsMessageToNotionTask (onTeamsMessageToNotionTask) To list webhook URLs later, run: ntn workers webhooks list
実行結果(webhooks list)
ntn workers webhooks list Webhook URLs for teams-webhook (019eac17-67e5-7867-83fe-bd77124d0d00): onTeamsMessageToNotionTask → https://www.notion.so/webhooks/worker/d08cad25-f90c-4f24-832c-38049a6b0b3a/019eac17-67e5-7867-83fe-bd77124d0d00/VJii1qhslrMjAtK0/onTeamsMessageToNotionTask
1.5 環境変数(secrets)を設定
ntn workers env set TASKS_DATA_SOURCE_ID=...ntn workers env set ASSIGNEE_USER_ID=...(Assignee を固定で入れる場合)ntn workers env set TEAMS_WEBHOOK_SECRET=...(X-Webhook-Secretの照合に使う)context.notionを使うための API トークン注入(moodle と同様の手順・制約に従う)
実行結果(TASKS_DATA_SOURCE_ID)
ntn workers env set TASKS_DATA_SOURCE_ID=1f70ce2a-6520-80ce-bc36-000b08bc3b11 ✔ Set environment variable 'TASKS_DATA_SOURCE_ID'
実行結果(ASSIGNEE_USER_ID)
ntn workers env set ASSIGNEE_USER_ID=fffd872b594c811b82180002b0392cd8 ✔ Set environment variable 'ASSIGNEE_USER_ID'
実行結果(TEAMS_WEBHOOK_SECRET)
ntn workers env set TEAMS_WEBHOOK_SECRET=(secret) ✔ Set environment variable 'TEAMS_WEBHOOK_SECRET'
実行結果(NOTION_API_TOKEN: env push)
cp ../moodle-webhook/.env . ntn workers env push The following changes will be pushed to the remote environment: + NOTION_API_TOKEN ✔ Push changes? · yes ✔ Environment variables pushed successfully
1.6 curl で疎通確認 → runs logs で確認
curl -i -X POST <WEBHOOK_URL> -H 'Content-Type: application/json' -H 'X-Webhook-Secret: ...' --data-raw '<json>'ntn workers runs listntn workers runs logs <run-id>
実行結果(curl)
HTTP/2 202 date: Tue, 09 Jun 2026 11:27:49 GMT content-type: application/json; charset=utf-8 content-length: 50 cf-ray: a08fca0e79737897-NRT cf-cache-status: DYNAMIC etag: W/"32-p6+EaX7E5WNiL4DZzDx3VOZOpcU" server: cloudflare ...
実行結果(runs list)
ntn workers runs list RUN ID NAME EXIT CODE STARTED AT ENDED AT 019eac24-1431-75ed-9ce4-309795ca1b66 webhook:onTeamsMessageToNotionTask 1 2026-06-09T11:28:26.161Z 2026-06-09T11:28:30.581Z 019eac23-8909-7e3a-ab9b-b21199022a93 webhook:onTeamsMessageToNotionTask 1 2026-06-09T11:27:50.537Z 2026-06-09T11:27:53.960Z 019eac21-ff3d-7ad2-bd3d-d8bcdac44b4d fetchAndSaveCapabilities 0 2026-06-09T11:26:09.725Z 2026-06-09T11:26:12.795Z 019eac20-9e5c-7df5-9699-695cff4dcaee fetchAndSaveCapabilities 0 2026-06-09T11:24:39.389Z 2026-06-09T11:24:40.847Z 019eac20-2e8e-746c-9351-66f053439fbf fetchAndSaveCapabilities 0 2026-06-09T11:24:10.766Z 2026-06-09T11:24:14.124Z 019eac1c-dd8f-790c-b36c-b9f1e5783eff fetchAndSaveCapabilities 0 2026-06-09T11:20:33.423Z 2026-06-09T11:20:34.918Z 019eac1a-dc29-7e38-875b-87d559236167 fetchAndSaveCapabilities 0 2026-06-09T11:18:21.993Z 2026-06-09T11:18:25.301Z 019eac17-b59e-7c2c-a68c-19e5e8f979de fetchAndSaveCapabilities 0 2026-06-09T11:14:55.518Z 2026-06-09T11:14:58.835Z 019eac17-70c4-7b68-8e61-3dd406dd4a63 deploy 0 2026-06-09T11:14:37.899Z 2026-06-09T11:14:55.233Z
※ webhook:onTeamsMessageToNotionTask が exit code=1 のため、次に runs logs を確認する。
実行結果(runs logs: 019eac24-1431-75ed-9ce4-309795ca1b66)
ntn workers runs logs 019eac24-1431-75ed-9ce4-309795ca1b66
{"_tag":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: conversation.type is required","trace":"ExecutionError: Error during worker execution: Error: conversation.type is required\n at Object.handler (file:///vercel/sandbox/node_modules/@notionhq/workers/dist/capabilities/webhook.js:28:61)"}}
node:internal/process/promises:394
triggerUncaughtException(err, true /* fromPromise */);
対応(原因と次アクション)
- 原因: 送信した JSON に
conversation.typeが入っていない(または入れ子が想定と異なる)ため、Worker がバリデーションで停止。 - 次:
curlの--data-rawを、下の「最小 JSON 例」に差し替えて再実行し、再度runs list/runs logsを確認する。
最小 JSON 例(channel の場合)
{ "event": "teams.message.selected", "occurredAt": "2026-06-09T11:27:49Z", "conversation": { "type": "channel", "teamId": "TEAM_ID", "channelId": "CHANNEL_ID" }, "message": { "id": "MESSAGE_ID", "webUrl": "https://teams.microsoft.com/l/message/...", "text": "test", "from": { "displayName": "hkob" } } }
最小 JSON 例(chat の場合)
{ "event": "teams.message.selected", "occurredAt": "2026-06-09T11:27:49Z", "conversation": { "type": "chat", "chatId": "CHAT_ID" }, "message": { "id": "MESSAGE_ID", "webUrl": "https://teams.microsoft.com/l/message/...", "text": "test", "from": { "displayName": "hkob" } } }
実行結果(runs list: 成功)
ntn workers runs list RUN ID NAME EXIT CODE STARTED AT ENDED AT 019eac29-bf71-7c50-a553-689a381fa76f webhook:onTeamsMessageToNotionTask 0 2026-06-09T11:34:37.681Z 2026-06-09T11:34:40.381Z
実行結果(runs logs: 019eac29-bf71-7c50-a553-689a381fa76f)
ntn workers runs logs 019eac29-bf71-7c50-a553-689a381fa76f
{"_tag":"success","value":{"status":"success"}}
2. Power Automate(手動フロー)を作成
- Power Automate で新規フローを作成(インスタント / 手動起動)。
- トリガに Teams の 「選択されたメッセージに対して(V2)」 を設定。
- 次の情報を取得できるように、後続ステップで Teams メッセージの出力を参照できる状態にする。
- team/channel(または chat)情報
- messageId
- webUrl
- 本文(text)
- 送信者(from.displayName / from.id)
- 日時(occurredAt 相当)
3. Webhook 送信(HTTP POST)を追加
- アクションに HTTP を追加。
- Method:
POST - URI: Workers で自動発行された Webhook URL(Step 1 で控えた URL)
- Headers に共有シークレットを設定(推奨)
- 例:
X-Webhook-Secret: <secret>
- 例:
- Body に JSON を設定(Worker が期待する構造に合わせる)。
- ポイント:
conversation.type/message.id/message.webUrl/message.from.displayName/occurredAtが必須。 - 例(Power Automate の動的コンテンツを埋め込む想定。まずはこの形で通す):
- ポイント:
{ "event": "teams.message.selected", "occurredAt": "@{utcNow()}", "tenantId": "@{triggerOutputs()?['body/teamsFlowRunContext/ChannelData/Tenant/Id']}", "conversation": { "type": "channel", "teamId": "@{triggerOutputs()?['body/teamsFlowRunContext/ChannelData/Team/Id']}", "channelId": "@{triggerOutputs()?['body/teamsFlowRunContext/ChannelData/Channel/Id']}" }, "message": { "id": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/Id']}", "webUrl": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/LinkToMessage']}", "subject": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/Subject']}", "text": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/Body/PlainText']}", "from": { "id": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/From/User/Id']}", "displayName": "@{triggerOutputs()?['body/teamsFlowRunContext/MessagePayload/From/User/DisplayName']}" } }, "mentions": [] }
※ Team.Name / Channel.Name は現状 null で来ているため、Body には含めない(必要なら別途 Graph API 等で解決)。
- 上の
triggerBody()?['...']のキー名は、実際のトリガ出力に合わせて読み替える(不明な場合は、フローのテスト実行で出力 JSON を確認してキーを合わせる)。

ここで保存を押したときに一番上のライセンスが必要というメッセージが出てしまい、プロジェクト終了となりました。
おわりに
ここまで worker を作ったので、webhook 直接呼び出しなしで回避できる手段が取れればいいなと思っています。
https://hkob.notion.site/hkob-16dd8e4e98ab807cbe3cf3cc94cdfe0f?pvs=4