- LINE WORKS APIのタスク作成POSTで「request body is empty」に苦しんだ原因と解決
- やりたかったこと
- 最初にできていたこと
- 問題:LINE WORKSのタスク作成POSTが通らない
- まず疑ったこと
- httpbinで切り分けた
- OAuth2のAuthentication設定も確認した
- POST権限はあるのか確認した
- 決定的だった原因
- n8nで成功したHTTP Request設定
- CodeノードでBody文字列を作る
- 同期情報はタスク本文に書かない方がいい
- 同期テーブルで管理する
- LINE WORKS側からGoogle Tasksへ作るときの本文も整理
- 今回の結論
- 今回得た教訓
- いまの進捗
- まとめ
LINE WORKS APIのタスク作成POSTで「request body is empty」に苦しんだ原因と解決
Google WorkspaceとLINE WORKSを連携させて、タスク管理を自動化しようとしています。
目的は単純で、Google TasksとLINE WORKSタスクを同期させたいというものです。
たとえば、Google Tasks側でタスクを作成したらLINE WORKSにも作成される。
逆に、LINE WORKSで作成したタスクはGoogle Tasksにも作成される。
さらに、完了状態や変更内容も相互に反映されるようにしたい、という構成です。
将来的には、Claude Codeなどから音声入力でタスクを作成・編集し、それをGoogle WorkspaceをハブにしてLINE WORKSへ流すことも想定しています。
そのための自動化基盤として、今回は n8n を使いました。
ところが、LINE WORKS APIのタスク作成POSTでかなりハマりました。
やりたかったこと
構成としては、ざっくり以下のような流れです。
Google Tasks
↓
n8n
↓
LINE WORKS タスク
逆方向もあります。
LINE WORKS タスク
↓
n8n
↓
Google Tasks
同期情報はPostgreSQLに保存します。
lineworks_google_tasks_sync
この同期テーブルに、Google Tasks側のIDとLINE WORKS側のタスクIDを対応づけて持たせます。
google_task_id
lineworks_task_id
lineworks_title
sync_status
google_status
last_sync_hash
updated_at
最終的には、タスク本文に同期用のIDなどを埋め込まず、DB側だけで同期情報を管理する方針にしました。
最初にできていたこと
まず、LINE WORKS側からタスクを取得することはできていました。
LINE WORKS APIでタスク一覧を取得し、それをGoogle Tasksへ作成する処理は動きました。
また、LINE WORKS側で完了にしたタスクをGoogle Tasks側でも完了にする処理も動きました。
つまり、以下はできていました。
LINE WORKS → Google Tasks 新規作成
LINE WORKS → Google Tasks 更新
LINE WORKS → Google Tasks 完了反映
さらに、LINE WORKSのタスク完了APIもn8nからPOSTできました。
POST /v1.0/tasks/{taskId}/complete
実際にLINE WORKS側のタスクが完了になり、Google Tasks側にも完了が反映されました。
ここまでは良かったのですが、Google Tasks側で作ったタスクをLINE WORKS側へ新規作成する部分で詰まりました。
問題:LINE WORKSのタスク作成POSTが通らない
Google Tasks側のタスクをLINE WORKSに作成しようとして、n8nのHTTP Requestノードで以下のAPIを叩きました。
POST https://www.worksapis.com/v1.0/users/me/tasks
または、明示的にユーザーIDを指定して以下も試しました。
POST https://www.worksapis.com/v1.0/users/<LINE_WORKS_USER_ID>/tasks
Bodyには、以下のようなJSONを送っていました。
{
"title": "POST固定テスト",
"content": "LINE WORKS API POST test",
"completionCondition": "ANY_ONE",
"assignorId": "<LINE_WORKS_USER_ID>",
"assignees": [
{
"assigneeId": "<LINE_WORKS_USER_ID>"
}
]
}
ところが、LINE WORKS側からはこのようなエラーが返ってきます。
Bad request - please check your parameters
The request body is empty.
これが非常に厄介でした。
n8n上ではBodyを設定しているのに、LINE WORKS側では「リクエストボディが空」と言われるわけです。
まず疑ったこと
最初に疑ったのは、n8nのHTTP Requestノードの設定です。
具体的には以下を疑いました。
Body Content Type が間違っているのではないか
JSONではなくRawで送るべきではないか
Content-Typeが正しくないのではないか
OAuth2認証がBodyと干渉しているのではないか
categoryIdの渡し方が違うのではないか
Body Content TypeをJSONにしたり、Rawにしたり、Content-Type: application/json を付けたり外したり、かなり試しました。
それでもエラーは変わりませんでした。
The request body is empty.
httpbinで切り分けた
このままだとLINE WORKS側だけ見ても原因が分かりません。
そこで、n8nが本当にBodyを送れているかを確認するために、送信先を一時的に httpbin に変えました。
POST https://httpbin.org/post
すると、httpbin側では以下のようにBodyが正しく見えていました。
Content-Type: application/json
Content-Length: 243
JSONも正しく渡っていました。
{
"categoryId": "default",
"title": "POST固定テスト",
"content": "LINE WORKS API POST test",
"completionCondition": "ANY_ONE",
"assignorId": "<LINE_WORKS_USER_ID>",
"assignees": [
{
"assigneeId": "<LINE_WORKS_USER_ID>"
}
]
}
つまり、n8nはBodyを送れています。
さらに、OAuth2認証付きでもhttpbinに投げて確認しました。
その場合も、AuthorizationヘッダーとJSON Bodyは正しく送信されていました。
Authorization: Bearer ...
Content-Type: application/json
Content-Length: 243
この時点で、n8nのBody送信そのものは正常だと分かりました。
OAuth2のAuthentication設定も確認した
n8nのOAuth2 credential設定で、Authentication が Body になっていました。
一瞬ここが怪しいと思い、Header に変えて試しました。
しかし、LINE WORKSのOAuth2では、この環境ではHeaderにすると再認証時に失敗しました。
client_id, client_secret or authorization code is not valid.
そのため、OAuth2 credentialのAuthentication設定は Body に戻しました。
ここは少し紛らわしいのですが、この設定はAPI呼び出し時のJSON Bodyではなく、OAuth2トークン取得時に client_id や client_secret をどこに入れるかの設定です。
GETや完了APIが通っていたので、credential自体は大きく間違っていませんでした。
POST権限はあるのか確認した
次に、そもそもLINE WORKSへのPOST権限があるのかを確認しました。
タスク作成APIは失敗しますが、タスク完了APIを試しました。
POST https://www.worksapis.com/v1.0/tasks/{taskId}/complete
このAPIはBodyなしで実行できます。
n8n上では一見、以下のようなエラーになりました。
Invalid JSON in response body
しかしLINE WORKS側を見ると、実際にはタスクが完了していました。
つまり、APIの実行自体は成功しており、n8nが空レスポンスをJSONとして解釈しようとして失敗していただけでした。
これで以下が分かりました。
LINE WORKSへのPOST権限はある
OAuth2のtaskスコープも有効
認証も通っている
つまり、権限不足ではありません。
決定的だった原因
最終的に成功したポイントは、assignees の中に status: "TODO" を入れることでした。
失敗していたBodyはこれです。
{
"assignorId": "<LINE_WORKS_USER_ID>",
"assignees": [
{
"assigneeId": "<LINE_WORKS_USER_ID>"
}
],
"title": "POST固定テスト",
"content": "LINE WORKS API POST test",
"completionCondition": "ANY_ONE",
"categoryId": "default"
}
成功したBodyはこれです。
{
"assignorId": "<LINE_WORKS_USER_ID>",
"assignees": [
{
"assigneeId": "<LINE_WORKS_USER_ID>",
"status": "TODO"
}
],
"title": "POST固定テスト",
"content": "LINE WORKS API POST test",
"completionCondition": "ANY_ONE",
"categoryId": "default"
}
少なくとも私の環境では、この status: "TODO" がないと、LINE WORKS側では以下のような分かりにくいエラーになりました。
The request body is empty.
本来なら、
assignees.status is required
のようなエラーが出てほしいところですが、実際にはそうではありませんでした。
n8nで成功したHTTP Request設定
最終的に成功したn8nのHTTP Requestノード設定は以下です。
Method:
POST
URL:
https://www.worksapis.com/v1.0/users/<LINE_WORKS_USER_ID>/tasks
Authentication:
Generic Credential Type
Generic Auth Type:
OAuth2 API
OAuth2 API:
LINE WORKS用のcredential
Query Parametersは以下です。
categoryId = default
BodyはRawにしました。
Send Body: ON
Body Content Type: Raw
Content Type: application/json; charset=UTF-8
BodyにはCodeノードで作ったJSON文字列を渡しました。
{{ $json.lineWorksCreateBodyText }}
CodeノードでBody文字列を作る
n8n側では、前段のCodeノードでLINE WORKS作成用のBodyを作っています。
// 自分のLINE WORKSユーザーIDに置き換える
const userId = '<LINE_WORKS_USER_ID>';
const categoryId = 'default';
const notes = ($json.notes ?? '').trim();
const body = {
assignorId: userId,
assignees: [
{
assigneeId: userId,
status: 'TODO',
},
],
title: $json.title || '無題タスク',
content: notes || 'empty',
completionCondition: 'ANY_ONE',
categoryId,
};
return [
{
json: {
...$json,
lineWorksUserId: userId,
lineWorksCategoryId: categoryId,
lineWorksCreateBody: body,
lineWorksCreateBodyText: JSON.stringify(body),
syncOrigin: 'google',
},
},
];
ポイントは、lineWorksCreateBodyText として JSON.stringify(body) した文字列を作っていることです。
HTTP Requestノードでは、このJSON文字列をRaw Bodyとして送信します。
同期情報はタスク本文に書かない方がいい
当初は、Google TasksやLINE WORKSの本文に同期情報を書き込んでいました。
例えばGoogle Tasks側には以下のような情報を入れていました。
LINE_WORKS_TASK_ID: ...
LINE_WORKS_MODIFIED_TIME: ...
LINE_WORKS_ASSIGNOR: ...
LINE_WORKS_STATUS: ...
MY_ASSIGNEE_STATUS: ...
GOOGLE_TASKS_STATUS: ...
またLINE WORKS側にも以下のような情報を書いていました。
【同期情報】
SYNC_ORIGIN: google
GOOGLE_TASK_ID: ...
GOOGLE_TASKS_UPDATED: ...
GOOGLE_TASKS_LINK: ...
しかし、これは実運用では良くありません。
理由は単純です。
ユーザーがタスク本文を編集したときに、同期情報を誤って消してしまう可能性があるからです。
また、タスク画面が見づらくなります。
そこで、同期情報はDB側だけに持たせる方針に変えました。
同期テーブルで管理する
同期情報はPostgreSQLのテーブルで管理します。
lineworks_google_tasks_sync
このテーブルに、Google Tasks側のIDとLINE WORKS側のIDを対応づけて保存します。
google_task_id
lineworks_task_id
lineworks_title
sync_status
google_status
last_sync_hash
updated_at
Google側のタスクIDに対して重複作成されないよう、ユニークインデックスも作りました。
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ux_lineworks_google_tasks_sync_google_task_id
ON lineworks_google_tasks_sync (google_task_id)
WHERE google_task_id IS NOT NULL AND google_task_id <> '';
これにより、Google Tasks側の同じタスクを何度もLINE WORKSに新規作成してしまう事故を防ぎやすくなります。
LINE WORKS側からGoogle Tasksへ作るときの本文も整理
LINE WORKSからGoogle Tasksへ同期する場合も、Google Tasksの本文に余計な同期情報を書かないようにしました。
修正前はこのように notes を作っていました。
notes: [
content ? `内容:\n${content}` : '内容: なし',
task.dueDate ? `期限: ${task.dueDate}` : '期限: なし',
assigneeNames ? `担当者: ${assigneeNames}` : '',
'',
`LINE_WORKS_TASK_ID: ${task.taskId}`,
`LINE_WORKS_MODIFIED_TIME: ${task.modifiedTime}`,
`LINE_WORKS_ASSIGNOR: ${task.assignorName}`,
`LINE_WORKS_STATUS: ${task.status ?? ''}`,
`MY_ASSIGNEE_STATUS: ${myAssigneeStatus}`,
`GOOGLE_TASKS_STATUS: ${googleStatus}`,
].filter(Boolean).join('\n')
これだと、Google Tasks側に同期用の内部情報が丸見えになります。
修正後は、Google TasksのnotesにはLINE WORKSの本文だけを入れます。
notes: content
整理用のCodeノードは以下のようにしました。
const rawTasks = $input
.all()
.flatMap(item => item.json.tasks ?? []);
// ページネーションや再取得で同じtaskIdが混ざっても1件にする
const seen = new Set();
const tasks = rawTasks.filter(task => {
if (!task.taskId) return false;
if (seen.has(task.taskId)) return false;
seen.add(task.taskId);
return true;
});
// 自分のLINE WORKS上の表示名に置き換える
const myAssigneeName = '<自分のLINE_WORKS表示名>';
return tasks.map(task => {
const content =
task.content && task.content !== 'empty'
? String(task.content).trim()
: '';
const assignees = task.assignees ?? [];
const assigneeNames = assignees
.map(a => a.assigneeName)
.filter(Boolean)
.join(', ');
const rawTaskStatus = String(task.status ?? '').toUpperCase();
const myAssignee = assignees.find(a => a.assigneeName === myAssigneeName);
const myAssigneeStatus = String(myAssignee?.status ?? '').toUpperCase();
const isCompleted =
rawTaskStatus === 'DONE' ||
rawTaskStatus === 'COMPLETED' ||
rawTaskStatus === 'COMPLETE' ||
rawTaskStatus === 'CLOSED' ||
myAssigneeStatus === 'DONE' ||
myAssigneeStatus === 'COMPLETED' ||
myAssigneeStatus === 'COMPLETE' ||
myAssigneeStatus === 'CLOSED';
const googleStatus = isCompleted ? 'completed' : 'needsAction';
const googleDueDate = task.dueDate
? `${task.dueDate}T00:00:00.000Z`
: undefined;
return {
json: {
lineWorksTaskId: task.taskId,
title: task.title || '無題タスク',
// Google Tasksに表示する本文はLINE WORKSの本文だけ
notes: content,
content: content,
dueDate: task.dueDate,
googleDueDate: googleDueDate,
status: task.status,
rawStatus: task.status ?? '',
myAssigneeStatus: myAssigneeStatus,
googleStatus: googleStatus,
assignees: assignees,
assigneeNames: assigneeNames,
assignorName: task.assignorName,
modifiedTime: task.modifiedTime,
// DB保存・差分判定用。Google Tasks本文には書かない
lastSyncHash: [
task.modifiedTime ?? '',
task.title ?? '',
content,
task.dueDate ?? '',
task.status ?? '',
myAssigneeStatus,
].join('|'),
}
};
});
今回の結論
今回ハマった原因は、見た目には「n8nがBodyを送れていない」ように見えたことです。
LINE WORKS APIからは、以下のようなエラーが返ってきました。
The request body is empty.
しかし、実際にはn8nはBodyを正しく送っていました。
httpbinで確認すると、OAuth2付きでもJSON Bodyは正常に送信されていました。
本当の原因は、LINE WORKSのタスク作成APIに渡す assignees の構造でした。
特に以下が必要でした。
"assignees": [
{
"assigneeId": "<LINE_WORKS_USER_ID>",
"status": "TODO"
}
]
assigneeId だけでは不十分で、status: "TODO" を付けることでタスク作成POSTが通るようになりました。
今回得た教訓
今回のようなAPI連携では、エラーメッセージをそのまま信じすぎるとハマります。
request body is empty
と出ても、本当にBodyが空とは限りません。
そのため、以下の切り分けが重要でした。
1. httpbinでn8nがBodyを送れているか確認する
2. OAuth2付きでもBodyが送れているか確認する
3. GETだけでなくPOST権限があるか確認する
4. BodyなしPOSTのAPIで書き込み権限を確認する
5. 最後にAPI固有のBody構造を疑う
この順番で切り分けたことで、n8nの設定ミスなのか、OAuth2の問題なのか、LINE WORKS APIのパラメータ問題なのかを分けられました。
いまの進捗
現時点でできていることは以下です。
LINE WORKS → Google Tasks 新規作成
LINE WORKS → Google Tasks 更新
LINE WORKS → Google Tasks 完了反映
Google Tasks → LINE WORKS 新規作成
Google Tasks → LINE WORKS 作成時のDB登録
同期情報をタスク本文に書かずDBで管理
まだ残っているのは以下です。
Google Tasks側で変更した内容をLINE WORKS側へ反映する処理
Google Tasks側で完了した場合にLINE WORKS側も完了する処理
Google Tasks側で削除された場合の扱い
既存タスク本文に残っている同期情報の掃除
まとめ
n8nでGoogle TasksとLINE WORKSタスクを同期させること自体は可能です。
ただし、LINE WORKS APIのタスク作成POSTでは、Body構造が少しでも合わないと分かりにくいエラーが返ることがあります。
特に今回のように、
The request body is empty.
と出ても、実際にはBodyが空ではなく、必須項目や構造が不足しているだけというケースがありました。
最終的に成功したポイントは以下です。
/users/{userId}/tasks にPOSTする
Body Content TypeはRaw
Content Typeは application/json; charset=UTF-8
assigneesに assigneeId だけでなく status: "TODO" を入れる
同期情報はタスク本文ではなくDBに持たせる
同じようにn8nとLINE WORKS APIでタスク作成を自動化しようとしている人は、まず assignees.status を確認するとよいと思います。

コメント