フォームからの問い合わせをn8nのWebhookで受けて、HTTP RequestノードでCRM APIにPOSTするワークフローを組んだ。テストでは通っていたのに本番切替の日にCRMが400エラーを返し続け、問い合わせがCRMに届かないまま1時間が過ぎた。実行データを見て原因が判明した。JSON Bodyフィールドに {{ $json.body.email }} と書いた式が評価されず、波括弧ごと文字列のまま送信されていたのだ。この記事では、再現手順、私が壊した設定、3つの正しい直し方(比較表つき)、インポート可能なworkflowの完全実装まで書く。
何が起きたか: 実行データに残った「式ではなく文字列」
環境は n8n(1.x系・self-hosted)。構成は Webhook → HTTP Request の2ノード構成です。
| 時刻 | 起きたこと |
|---|---|
| 09:00 | Webhookは正常に受信。入力データに email / name / company_id は入っている |
| 09:00 | HTTP RequestがCRMにPOST → 400 Bad Request |
| 09:05 | 実行データの送信Bodyを確認。値は "email": "{{ $json.body.email }}" と波括弧付きのまま |
| 09:40 | Body組み立ての3方式(Body Parameters / JSON本文式 / Codeノード事前構築)を実機で比較テストし、Codeノード方式で確定 |
| 10:00 | 本番切替完了。以降400はゼロ |
CRMの側の受信ログを見ると、emailフィールドに「{{ $json.body.email }}」という29文字の文字列が届いていた。式は一度も評価されていない。n8nのUI上では問題なく見えるのに、出力だけが文字列になる。これがこの事故の厄介な点です。
失敗した設計: 私はこれで壊した
壊した設定はこれだけです。HTTP RequestノードのSpecify Bodyを「Using JSON」にした状態で、JSON本文に式をそのまま直書きした。
{
"parameters": {
"method": "POST",
"url": "https://crm.example.com/api/v1/leads",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"email\": \"{{ $json.body.email }}\",\n \"name\": \"{{ $json.body.name }}\"\n}"
}
}
注目してほしいのは "jsonBody" の値です。先頭に = がない。n8nでは式を評価するフィールドは先頭に = を付けた式モードであり、先頭に = のない文字列は式を含んでいても一切評価されません。結果、{{ $json.body.email }} はそのまま29文字の文字列として送信され、CRMは「emailの形式が不正」で400を返していた。
テスト環境で通っていたのは、CRM側が未知の値をWARNだけで通していたからです。本番でバリデーションが厳しくなった瞬間に破綻しました。テストで動くBodyは本番でも通るとは限らない。送信側だけ直して済む事故ではありますが、症状がどこに出るか(400と出るか、壊れたデータで登録されるか)は受け手の実装次第です。
症状と正体の対応表
実行データで見えた3症状と正体です。
| 症状 | 実行データで見えること | 正体 |
|---|---|---|
| 式が文字列で残る |
"{{ $json.body.email }}" がそのまま送信される |
jsonBodyの先頭に=が無い(式モードでない) |
| JSONが部分的に壊れる | 片方のキーだけ引用符エラー・undefined | 式の結果の文字列連結・二重エスケープ。式内でJSONを手で組み立てている |
| 400 Invalid JSON | 送信Bodyが email=x&name=y の形 |
Specify BodyがFields(KeyPair)のまま。JSON扱いになっていない |
Body組み立ての3方式比較
| 方式 | 設計 | 向いているケース | 気をつける点 |
|---|---|---|---|
| A: Body Parameters(Using Fields Below) | フィールドごとにName/Valueを追加し、Valueに式を書く | 送信フィールドが3個以下の小さなBody | Content-TypeはsendBodyの指定から自動付与される。手動で乱さないこと |
| B: JSON本文を1つの式にする |
jsonBody の先頭に = を付け、式の中でJSON全体を組み立てる |
中〜大規模なBody。定義を1箇所に集めたい | 引用符のネスト禁止。文字列化はJSON.stringify 1回だけ |
| C: Codeノードで事前構築(本採用) | 前段のCodeノードで完全なオブジェクトを作り、HTTPノードはstringifyするだけ | 値の正規化・fallback・日付加工が実務では必ず必要 | ノードが1つ増えるが、テストとデバッグが最も速い |
実務はCに寄せるのが安全です。理由は3つ: ①値の変換ミスがHTTPノードの責任から切り離される、②Codeノードだけで回帰テストが回せる、③式のネストがゼロになる。
直し方A: Body Parameters(Using Fields Below)
{
"parameters": {
"method": "POST",
"url": "https://crm.example.com/api/v1/leads",
"sendBody": true,
"specifyBody": "keypair",
"bodyParameters": {
"parameters": [
{ "name": "email", "value": "={{ $json.body.email }}" },
{ "name": "name", "value": "={{ $json.body.name }}" }
]
}
}
}
直し方B: JSON本文を先頭に = を付けて式モードにする
{
"parameters": {
"method": "POST",
"url": "https://crm.example.com/api/v1/leads",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ email: $json.body.email, name: $json.body.name, sent_at: $now.toISO() }) }}"
}
}
先頭の = が式モードの印です。これだけで波括弧が文字列として出力される事故は消えます。
直し方C(本採用): Codeノードで正規化してから送る
// Codeノード: 値の正規化とfallbackをここで完結させる
const lead = $input.first().json;
const body = lead.body ?? {};
const pick = (v, f) => (v === null || v === undefined || v === '' ? f : v);
return [{
json: {
crm_payload: {
email: pick(body.email, 'unknown@example.com'),
name: pick(body.name, 'no-name'),
company_id: Number(pick(body.company_id, 0)),
sent_at: new Date().toISOString()
}
}
}];
{
"parameters": {
"method": "POST",
"url": "https://crm.example.com/api/v1/leads",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.crm_payload) }}"
}
}
完全実装: インポート可能なworkflow JSON
n8nのキャンバス上で Import from Clipboard にそのまま貼り付けられる完全形です。
{
"name": "form-to-crm-expression-proof",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "lead",
"responseMode": "lastNode"
},
"id": "5f0e7a90-1111-4111-8111-000000000001",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [-200, 0],
"webhookId": "lead-hook"
},
{
"parameters": {
"jsCode": "// 送信Bodyをここで完全に組み立てる。ExpressionをHTTPノードに直書きしない\nconst lead = $input.first().json;\nconst body = lead.body ?? {};\nconst pick = (v, f) => (v === null || v === undefined || v === '' ? f : v);\n\nreturn [{\n json: {\n crm_payload: {\n email: pick(body.email, 'unknown@example.com'),\n name: pick(body.name, 'no-name'),\n company_id: Number(pick(body.company_id, 0)),\n sent_at: new Date().toISOString()\n }\n }\n}];"
},
"id": "5f0e7a90-2222-4222-8222-000000000002",
"name": "Build CRM Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [0, 0]
},
{
"parameters": {
"method": "POST",
"url": "https://crm.example.com/api/v1/leads",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.crm_payload) }}"
},
"id": "5f0e7a90-3333-4333-8333-000000000003",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [200, 0],
"alwaysOutputData": true
}
],
"connections": {
"Webhook": {
"main": [[{ "node": "Build CRM Payload", "type": "main", "index": 0 }]]
},
"Build CRM Payload": {
"main": [[{ "node": "HTTP Request", "type": "main", "index": 0 }]]
}
},
"active": false,
"settings": {},
"pinData": {}
}
送信先URLはご自身のCRMのエンドポイントに差し替えてください。responseMode: "lastNode" はHTTP Requestの応答をWebhookの呼び出し元に返すための設定です(active: false のまま保存し、本番切替時に上げてください)。
検算: 直ったことをどう確認するか
- HTTP Requestノードの入力パネルで
crm_payloadがオブジェクトとして展開されていること。文字列"{\"email\":"x",\"name\":"y"}"の形なら式が正しく評価されていない証拠 - CRM側のアクセスログで、Bodyが
{"email":"foo@example.com"}の形のJSONオブジェクトとして届いていること - もし
{{ }}が再び文字列で出てきたら、jsonBodyの先頭の=の付け忘れを最初に見る
まとめ: 次にやること
- JSONに式を書く場所は1つに集める。本番はCodeノードで正規化 →
={{ JSON.stringify($json.crm_payload) }}で送る構成に寄せる - 既存のHTTP Requestノードを全部開き、
jsonBodyに先頭の=があるかを今日確認する。無いものは事故予備として扱う - 受け手(送信先API)のバリデーション設定を変えた日を、ワークフロー側の回帰テスト日としてカレンダーに登録する
そのとき注意: 式モードはUI上「Expression」トグルで現れるためエディタの見た目だけでは区別が難しい実装です。実行データで実際に送信されたBodyを見るのが唯一の確実な確認手段です。
保存用チェックリスト: HTTP RequestのBodyを今日から守る6点
-
jsonBodyの先頭に=がある -
=が無いjsonBodyは事故予備として扱う -
式の中でJSONを手組み立てしない /
JSON.stringifyは1回だけに絞る - 送信フィールドが3個を超えるなら Codeノードでの事前構築に切り替える
- 正規化とfallbackと型変換はHTTPノードに書かずCodeノードに集約する
- 本番切替前に実行データで実際に送信されたBodyを1件目視確認する
読者特典(無料)
本記事のような実装パターンを横断的にまとめたチートシートと、Obsidian・Notionのテンプレート集を無料配布しています。8冊の内容から「何をどのツールでやるか」の判断チャートも含みます。
関連記事(同テーマ)
- n8nのWebhookが二重実行されていた — テストURLと本番URLの違いを知らずに深夜アラートが11通飛んだ話
- n8nのリトライ設定が失敗を5倍に増やした — retryOnFailとエラーワークフローの正しい切り分け
📗 n8n × AI自動化実践入門 — ノーコードで作るAIワークフロー(Kindle・Kindle Unlimited読み放題対象)