結論
- 「Power Automate は GUI(デザイナー)でしか作れない」と思い込んでいましたが、Dataverse Web API で
workflowテーブルのclientdata(フロー定義のJSON)を GET/PATCH すれば、コードだけでフローを作ったり直したりできます。 - 認証は Azure CLI でトークンを取ります。このとき audience を Dataverse の環境URLと完全に一致させます。
- 開発には Claude Code(PowerShell上で起動)と Python(標準ライブラリだけ)を使いました。
まずは完成形です。以下は、コード(API)だけで作ったフローが Power Automate ポータルの一覧に並んでいる画面です。

対象読者・ゴール
- 対象読者:Power Automate を GUI で触ったことがあり、AIコーディングツールでフローを作成したい人
- ゴール:トークン取得 → 既存フローの clientdata 取得 → 編集 → PATCH で反映、まで一周できること
※Power Platform のAPIは環境・テナント設定に依存します。本記事は
【2026年6月時点/Dataverseの開発環境】で動作確認した内容です。
なぜ GUI ではなく API なのか
きっかけは「Claude Code(AIコーディング支援)で Power Automate を作れないか」という思いつきでした。
当初は「フローはGUI専用、コードからは無理」と思っていたのですが、調べてみると次のことが分かりました。
- Power Automate のクラウドフローは、Dataverse の
workflowテーブルの1レコードにすぎません - フロー定義の実体は、その
clientdataカラムに入った JSON 文字列です
このカラムを Dataverse Web API で読み書きすれば、GUIなしでフローを操作できます。
以下は、フローの正体が Dataverse の1レコード(workflow テーブルの1行)であることを示す画面です。

前提条件
| 項目 | 内容 |
|---|---|
| Dataverse 環境 | dev |
| 権限 | System Customizer 以上 |
| 認証 | Azure CLI でユーザートークン取得 |
| 実行環境 | Windows / PowerShell + Python 3.8+(標準ライブラリ urllib のみ、pip不要) |
| AIアシスタント | Claude Code |
全体像
以下は、これから行う処理の全体像(トークン取得 → GET → 編集 → PATCH の流れ)です。

手順
Step 1. トークン取得
Azure CLI で、Dataverse 環境URLを audience に指定してトークンを取得します。
具体的には、az account get-access-token の --resource に環境URLを渡します。
ここで指定した値が、そのままトークンの aud(宛先)になります。
az login
az account get-access-token `
--resource https://yourorg.crm7.dynamics.com `
--query accessToken -o tsv > dataverse_token.txt
yourorg.crm7.dynamics.comはダミー表記です(実際の値を入力してください)。
1つ目のつまずきポイントがここでした。
--resource は環境URLとぴったり一致していないと通りません。(当たり前ではありますが💦)
https://disco.crm.dynamics.com のような汎用URLだと、後の PATCH が 401/403 で落ちます。
トークンの aud クレームをデコードして環境URLと一致するか確認する小さな検証コードを用意しておくと、この手のつまずきを防げます。
Step 2. 操作対象フローの workflowId を取得
操作対象フローの workflowId を取得します。
なお、$select や $filter を付けたURLは、$ や空白・シングルクォートといった記号を含みます。これらをそのまま送るとサーバー側で解釈できず 400(Bad Request)になるため、クエリ部分だけを安全にURLエンコードしてから送ります。
import json, urllib.request, urllib.parse
BASE = "https://yourorg.crm7.dynamics.com/api/data/v9.2" # ← ダミー表記(実際の値を入力してください)
TOKEN = open("dataverse_token.txt").read().strip()
HEAD = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}
# $filter の値(フロー名)もダミー表記(実際の値を入力してください)
query = "$select=name,workflowid,statecode&$filter=name eq 'MyFlow'"
# $ や空白などを安全にエンコード(= & $ , ( ) ' は残す)
safe_q = urllib.parse.quote(query, safe="=&$,()'")
url = f"{BASE}/workflows?{safe_q}"
req = urllib.request.Request(url, headers=HEAD)
flows = json.load(urllib.request.urlopen(req))["value"]
print(flows[0]["workflowid"])
ここでの2つ目のつまずきが404でした。
GUIはURLにすぐGUIDを表示しますが、「保存」するまで Dataverse に実体(workflowレコード)は作られません。
そのため受け取ったGUIDがことごとく404になり、原因が分からず時間を食いました。
API操作の前に、GUIで一度保存しておく必要があります。
※以下は、空フローに名前と最低限のトリガー・アクションだけ付けて「保存」→「公開」する画面です。今回は「demo」フローを作成しました。


Step 3. 現在の clientdata を取得
次に、PATCHの前に必ずGETしてバックアップを取ります。
編集前のものを flows/ に残しておけば、おかしくなってもすぐ元に戻せます。
WID = "<あなたのworkflowId>" # ← Step 2 で取得した値を入力してください
url = f"{BASE}/workflows({WID})?$select=name,clientdata"
cur = json.load(urllib.request.urlopen(urllib.request.Request(url, headers=HEAD)))
open("clientdata_backup.json", "w", encoding="utf-8").write(cur["clientdata"])
Step 4. clientdata を編集
ここが一番ハマりやすいところです。
前提、clientdata には JSON オブジェクトそのものではなく、JSONを文字列化したものが入っています。
中身は Azure Logic Apps の Workflow Definition スキーマ(triggers / actions / outputs)で、Power Automate のクラウドフローは内部的に Logic Apps と同じ定義言語を採用しているため、@{} の式表現などもそのまま使えます。
編集の基本は「取得した文字列を json.loads でオブジェクトに戻す → 必要なアクションを足し引きする → clientdata_new.json に保存する」という流れです。ここで保存したファイルを、次の Step 5 で書き戻します。
例として、「Dataverse のレコードを1件更新するアクション」を1つ組み込んでみます。
ポイントは、entityName と operationId でアクションの種類が決まること、item に更新したいフィールドをまとめて渡せること、@{} の式で他アクションやトリガーの出力を参照できることです(列名・値はダミー表記です)。
# ① Step 3 で取得した cur["clientdata"] は「文字列」。loads でオブジェクトに戻す
definition = json.loads(cur["clientdata"])
# ② 目的のアクションを組み込む(ここでは「レコードを更新」を1つ追加)
definition["properties"]["definition"]["actions"]["レコードを更新"] = {
"runAfter": {},
"type": "OpenApiConnection",
"inputs": {
"parameters": {
"organization": "current",
"entityName": "your_entities", # 更新先テーブル(ダミー表記)
"recordId": "@triggerOutputs()?['body/your_recordid']",
"item": { # 更新する列をまとめて渡す
"your_status": 100000002,
"your_note": "@triggerBody()?['text']"
}
},
"host": {
"apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
"connectionName": "shared_commondataserviceforapps",
"operationId": "UpdateRecordWithOrganization" # アクションの種類
},
"authentication": "@parameters('$authentication')"
}
}
# ③ 編集後のオブジェクトを保存(Step 5 で読み込みます)
json.dump(definition, open("clientdata_new.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
item は「フィールド名: 値」のオブジェクトを丸ごと受け取れるので、更新したい列が増えても、この中にキーを足すだけで済みます(UIで組むとフィールドごとに1行ずつ増えていきます)。
上のコードで行っているのは「文字列 → オブジェクト(json.loads)」までで、逆の「オブジェクト → 文字列(json.dumps)」は Step 5 の書き戻し時に行います。
なお、アクションをゼロから手書きするのは大変です。
特に Teams のような動的スキーマ(投稿先や投稿者の選び方で、必要なパラメータの構造そのものが変わるアクション)は、GUIで一度だけ構成して保存し、生成された clientdata をコピーして使うのが確実です(推測で組むとGUI側に警告が出ます)。
Step 5. PATCH で書き戻す
step 4で言及した書き戻しを行います。
new = json.load(open("clientdata_new.json", encoding="utf-8"))
body = json.dumps({"clientdata": json.dumps(new, ensure_ascii=False)},
ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(f"{BASE}/workflows({WID})", data=body, method="PATCH",
headers={**HEAD, "Content-Type": "application/json", "If-Match": "*"})
print(urllib.request.urlopen(req).status) # 204 = 成功
PATCH が 204 を返せば成功です。Power Automate ポータルで反映を確認します。
反映されていたら、無事完了です。
最後に
初投稿かつAIにサポートしてもらって投稿しました。
誤りや「うちの環境では違った」といった点があれば、コメントで教えてください!
読んでいただきありがとうございました!