L Harnessは、LINE公式アカウントにステップ配信・タグ・フォーム・リマインダー・スコアリングを足すオープンソースの拡張ツールです(GitHub Shudesu/line-harness-oss、MITライセンス)。この記事では、サンプル料理教室の友だち追加から体験予約、リマインダー、入会の案内、問い合わせの通知までを、L Harnessの機能だけで一通り組みます。前提は、L HarnessをCloudflareに入れ終え、管理画面のAPIキーが手元にあることです。入れ方は「L Harnessの入れ方:Cloudflare・LINE Developersの準備から最初の配信まで」にあります。
先に決めること
- Webhookの受け口をL Harnessにする。「Webhook URL」に入れられるURLは1つだけなので、n8nやスプレッドシートへはL Harnessから渡します。チャネルアクセストークン(長期)は新しく発行すると古いものが無効になります
- あいさつと自動応答を、LINE Official Account Manager(管理画面)の「あいさつメッセージ」「応答メッセージ」でやるか、L Harnessでやるか、片方にします
- 友だち追加の入口を
/auth/line?ref=〜のURLに揃える。流入経路が記録されるのはこの入口から来た人だけで、元のQRコードやID検索から来た人には付きません。LINEログインのチャネルが要ります - 体験予約を「フォーム+リマインダー」で受けるか、Googleカレンダー連携の予約で受けるか。この記事は前者で組みます。後者はGitHubのドキュメントにある機能で、2026年10月時点の管理画面の左メニューには予約の項目が出ていないので、応用で触れるだけにします
- タグの名前。シナリオ・リッチメニュー・配信の条件になるので、あとで変えると条件を全部直します
- 月の通数。ステップ配信・リマインダー・L Harnessのチャットからの返信はプッシュメッセージで、通数に数えます。管理画面の「あいさつメッセージ」「応答メッセージ」、チャットの送受信、Reply APIで返す自動返信は数えません(2026年10月時点)
サンプル料理教室(友だち300人・毎月の新規40人・体験申込20人)では、あいさつ40通、リマインダー40通、体験後のステップ100通、一斉配信300通で月480通ほどです。コミュニケーションプランの月200通では足りないので、ライトプラン(月5,000円・5,000通、税別)にしています(2026年10月時点)。
使うタグは次の7つです。
| タグ名 | 付くタイミング |
|---|---|
| 流入_チラシ/流入_Instagram | その入口から友だち追加した |
| 体験申込 | 体験予約フォームを送信した |
| 体験参加済み | レッスン後にスタッフが付ける |
| 入会済み | 入会手続きの後にスタッフが付ける |
| 見込み高 | スコアが30点以上になった |
| 退会 | 退会手続きの後にスタッフが付ける |
作るものの一覧
APIは、Workerを https://line.example.com に置いた例です。
| 段階 | 管理画面 | API | 主な設定値 |
|---|---|---|---|
| 流入経路のタグ | 流入経路 | POST /api/entry-routes | refCode、tagId |
| あいさつ | シナリオ配信 | POST /api/scenarios | friend_add、0分 |
| リッチメニュー | リッチメニュー、オートメーション | POST /api/rich-menus、POST /api/automations | 2500×1686、switch_rich_menu |
| 体験予約 | フォーム回答 | POST /api/forms | onSubmitTagId、saveToMetadata |
| 前日・当日の連絡 | リマインダ | POST /api/reminders/:id/enroll/:friendId | -1440、-120 |
| 体験後の案内 | シナリオ配信 | POST /api/scenarios/:id/steps | tag_added、tag_not_exists |
| 問い合わせ | 個別チャット、自動返信ルール、Webhook | POST /api/webhooks/outgoing | message_received |
| 反応のよい人 | マイル | POST /api/scoring-rules | score_threshold |
手順は本家のドキュメントに合わせてAPIで書きます。

組む手順
curlは最初に次の2行を打ってから使います。返る id は控えます。
BASE=https://line.example.com
KEY=YOUR_API_KEY
1. LINE側の設定を合わせる
- LINE Developersで、Messaging APIのチャネルを開く
- 「Messaging API設定」タブの「Webhook URL」が
https://line.example.com/webhookか確かめる - 「検証」を押して「成功」と出るのを確かめる
- 「Webhookの利用」がオンか確かめる
- 同じタブの「あいさつメッセージ」の「編集」から管理画面を開き、オフにする
- 同じく「応答メッセージ」をオフにする
- チャットはオンのままにする

2. タグと流入経路を作る
タグは POST /api/tags に {"name": "流入_チラシ"} と名前だけ送ります。7つ作り、GET /api/tags で一覧を出して id を控えます。
流入経路は「このrefで来た人にこのタグを付ける」という設定です。管理画面では左メニュー「分析」の「流入経路」で見られます(見出しは「流入経路分析」、説明は「ref コード別の友だち獲得・クリック実績」)。タグには左メニューの項目が無いので、作るのも一覧を見るのもAPIで行います。
curl -X POST $BASE/api/entry-routes -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"refCode": "flyer", "name": "店頭チラシ", "tagId": "TAG_FLYER_ID"}'
友だち追加のURLは https://line.example.com/auth/line?ref=flyer になり、チラシにはこのURLのQRコードを刷ります。Instagram用も refCode を instagram にして同じく作ります。流入経路にも scenarioId を指定できますが、次のあいさつのシナリオと重なるので空にしておきます。

3. あいさつのシナリオを作る
POST /api/scenarios に {"name": "あいさつ", "triggerType": "friend_add"} を送り、返ってきたIDの /steps に {"stepOrder": 1, "delayMinutes": 0, "messageType": "text", "messageContent": "…"} を送ります。
stepOrder は1から数え、最初が0分なら追加した瞬間に送られます。2通目以降は前のステップからの分数(1日後は 1440)です。
4. リッチメニューを2枚作り、人ごとに出し分ける
全員に出す体験前のメニューと、入会済みの人だけに出す会員メニューを作ります。
| メニュー | 左上 | 右上 | 左下 | 右下 |
|---|---|---|---|---|
| 体験前 | 体験レッスンの予約(フォーム) | レッスン日程(Webページ) | よくある質問(文字を送る) | 問い合わせ(文字を送る) |
| 会員 | 次のレッスンの予約 | 今月の日程 | 持ち物・アクセス | 問い合わせ |
POST /api/rich-menusに、sizeを{"width": 2500, "height": 1686}、areasに4つのタップ範囲(1つが幅1250・高さ843)を入れて送る- 左上の範囲の
actionは{"type": "uri", "uri": "https://liff.line.me/YOUR_LIFF_ID?formId=FORM_TRIAL_ID"}、左下は{"type": "message", "text": "よくある質問"}にする(フォームのIDは手順5で作ったもの) POST /api/rich-menus/:id/imageに画像を送る(PNGかJPEG、2500×1686、1MB以下)- 体験前のメニューだけ
POST /api/rich-menus/:id/defaultで全員の既定にする - 会員メニューも1〜3で作る(既定にはしない)
出し分けは、オートメーションで「入会済みタグが付いたら会員メニューに替える」と書きます。
curl -X POST $BASE/api/automations -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "入会したら会員メニュー", "eventType": "tag_change",
"conditions": {"tag_id": "TAG_MEMBER_ID"},
"actions": [{"type": "switch_rich_menu", "params": {"richMenuId": "RICHMENU_MEMBER_ID"}}]}'
表示の優先順位は、人ごとのメニュー、Messaging APIの既定、管理画面のメニューの順なので、L Harnessで既定を決めると管理画面のメニューは出なくなります。切り替えはすぐ効きます。退会した人を戻すときは、入会済みタグを外さず、退会タグで remove_rich_menu を動かします。

5. 体験予約のフォームを作る
フォームはLINEの中で開くWebフォーム(LIFF)で、送った人は自動で分かります。送信したら体験申込タグを付け、回答を友だちの情報(メタデータ)に残します。
curl -X POST $BASE/api/forms -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "体験レッスン申込", "onSubmitTagId": "TAG_TRIAL_ID", "saveToMetadata": true,
"fields": [
{"name": "full_name", "label": "お名前", "type": "text", "required": true},
{"name": "tel", "label": "電話番号", "type": "tel", "required": true},
{"name": "slot", "label": "希望の日時", "type": "radio", "required": true,
"options": ["2026-11-12 19:00 平日夜", "2026-11-14 10:00 土曜午前"]}]}'
開くURLは https://liff.line.me/YOUR_LIFF_ID?formId=返ってきたID で、これをリッチメニューの左上に入れます。希望日時は次の手順で読むので、選択肢は 2026-11-14 10:00 の形で始めます。日程は PUT /api/forms/:id で差し替えます。

6. 前日・当日のリマインダーを作る
リマインダーは「基準の日時の何分前に送るか」を決めたものです。POST /api/reminders に {"name": "体験レッスン"} を送ってから、前日(-1440)と2時間前(-120)のステップを足します。
curl -X POST $BASE/api/reminders/REMINDER_TRIAL_ID/steps -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"offsetMinutes": -1440, "messageType": "text",
"messageContent": "明日は体験レッスンです。エプロンだけお持ちください。"}'
申込者の登録は POST /api/reminders/:id/enroll/友だちのID に {"targetDate": "2026-11-14T10:00:00+09:00"} を送ります。これを毎回手で打たないよう、回答を5分おきに読んでシートに書き、登録まで済ませるGAS(Google Apps Script)を置きます。「体験予約」シートを作り、スクリプトプロパティに LH_API_KEY を入れ、sync に5分おきのトリガーを付けます。
const BASE = 'https://line.example.com', FORM = 'FORM_TRIAL_ID', REM = 'REMINDER_TRIAL_ID';
function sync() {
const h = { Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('LH_API_KEY') };
const sh = SpreadsheetApp.getActive().getSheetByName('体験予約');
const done = new Set(sh.getRange('A:A').getValues().flat());
const res = UrlFetchApp.fetch(BASE + '/api/forms/' + FORM + '/submissions', { headers: h });
JSON.parse(res.getContentText()).data.forEach(function (s) {
const m = String(s.data.slot).match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/);
if (done.has(s.id) || !s.friendId || !m) return;
const target = m[1] + 'T' + m[2] + ':00+09:00';
UrlFetchApp.fetch(BASE + '/api/reminders/' + REM + '/enroll/' + s.friendId, {
method: 'post', contentType: 'application/json', headers: h,
payload: JSON.stringify({ targetDate: target })
});
sh.appendRow([s.id, s.createdAt, s.friendId, s.data.full_name, s.data.tel, target]);
});
}
A列の回答IDで二重登録を防ぎます。キャンセルの連絡があったら、GET /api/friends/友だちのID/reminders で登録のIDを調べて DELETE /api/friend-reminders/そのID で止めます。

7. 体験後のステップ配信を作り、入会した人で送り分ける
POST /api/scenarios に "triggerType": "tag_added" と、triggerTagId に体験参加済みタグのIDを入れて作ります。レッスン後にスタッフがタグを付けた人だけに送られます。
| 順番 | 前のステップから | 条件 | 内容 |
|---|---|---|---|
| 1 | 60分 | なし | 体験のお礼と当日のレシピ |
| 2 | 1日(1440) | 入会済みタグが無い | 月謝とコースの案内 |
| 3 | 2日(2880) | 入会済みタグが無い | 体験した人からよく出る質問 |
| 4 | 3日(4320) | 入会済みタグが無い | 入会金が無料になる期限 |
| 5 | 14日(20160) | 入会済みタグが無い | 翌月の体験日程 |
curl -X POST $BASE/api/scenarios/SCENARIO_AFTER_ID/steps -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"stepOrder": 2, "delayMinutes": 1440, "messageType": "text",
"messageContent": "月謝は月4回で12,000円です。https://cooking.example.com/course",
"conditionType": "tag_not_exists", "conditionValue": "TAG_MEMBER_ID"}'
条件に合わない人はそのステップを飛ばして次へ進みます。管理画面の「シナリオ詳細」にはステップの一覧だけが出て、条件は表示されないので、条件はAPIで入れ、GET で返ってくる中身で確かめます。入会した人には、入会済みタグで始まる別のシナリオで持ち物と振替のルールを送ります。タグ1つで、メニューの切り替え・案内の停止・会員向けシナリオの開始が一度に起きます。

8. 問い合わせをチャットで受け、スタッフに知らせる
「よくある質問」のボタンは文字を送るので、自動返信で答えます。POST /api/auto-replies に {"keyword": "よくある質問", "matchType": "exact", "responseContent": "持ち物:エプロン…"} を送ります。matchType は完全一致(exact)か部分一致(contains)です。自動返信はReply APIで返すので通数に数えません。
自動返信に当たらないメッセージは、管理画面の左メニュー「個別チャット」(ページの見出しは「オペレーターチャット」)に「未読」で並びます。タブは「全て」「未読」「対応中」「解決済」です。返信したら「対応中」、終われば「解決済」にし、同じ人がまた送ると「未読」に戻ります。

スタッフへの通知は、送信Webhookでメッセージを受けたGASからメールとシートに残します。GASの doPost が受け取る中身の一覧にはリクエストヘッダーが無く、L Harnessの署名(X-Webhook-Signature)を確かめる手が無いので、URLに合言葉を付けて照合します。画像やスタンプでは message_received が動かず、届くのは文字だけです。
function doPost(e) {
const b = JSON.parse(e.postData.contents), ev = b.data.eventData || {};
const ok = e.parameter.token === PropertiesService.getScriptProperties().getProperty('HOOK_TOKEN');
if (!ok || b.event !== 'message_received' || ev.matched) return ContentService.createTextOutput('skip');
SpreadsheetApp.getActive().getSheetByName('問い合わせ').appendRow([b.timestamp, b.data.friendId, ev.text]);
MailApp.sendEmail('staff@example.com', 'LINEに問い合わせ', String(ev.text));
return ContentService.createTextOutput('ok');
}
GASをウェブアプリとしてデプロイし(アクセスできるユーザーは「全員」)、URLに ?token=合言葉 を付けて登録します。
curl -X POST $BASE/api/webhooks/outgoing -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "問い合わせ通知", "eventTypes": ["message_received"],
"url": "https://script.google.com/macros/s/YOUR_DEPLOY_ID/exec?token=YOUR_TOKEN"}'
matched で自動返信に当たったものを外すので、「よくある質問」を押しただけでは通知が鳴りません。L Harnessのチャットからの返信は通数に数えます。管理画面のチャットから返せば数えませんが、L Harnessの記録には残りません。
9. kintone・スプレッドシートに書き出す
体験予約と問い合わせはここまででシートに残ります。ほかを書き出す口は、送信Webhook(eventTypes に friend_add・tag_change などを並べるか * で全部。中身は event・timestamp・data)と、API(GET /api/friends は50件ずつ・tagId で絞り込み可、GET /api/friends/:id はタグとメタデータ付き)の2つです。kintoneに入れるなら、送信Webhookの先をn8nやGASにして、友だちのIDを主キーにしたアプリへ足すか更新します。重複の防ぎ方は「LINEとkintoneをつなぐ:友だちとやりとりを顧客管理に入れる」、1件ずつ記録する作りは「LINEの問い合わせをスプレッドシートに貯める:Webhookで1件ずつ記録する」で書いています。
応用
スコアリングで反応のよい人を見つける
行動ごとに点を付け、合計が一定を超えた人にタグを付けます。管理画面では「マイル」という名前です。サンプル料理教室は、日程リンクのクリック(url_click)10点、フォーム送信(form_submit)20点、メッセージ受信(message_received)3点です。ルールは POST /api/scoring-rules に {"name": "日程クリック", "eventType": "url_click", "scoreValue": 10} を送って作り、30点で見込み高タグを付けます。
curl -X POST $BASE/api/automations -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "見込み高", "eventType": "score_threshold", "conditions": {"score_threshold": 30},
"actions": [{"type": "add_tag", "params": {"tagId": "TAG_HOT_ID"}}]}'
クリックで点が付くのは、L Harnessのトラッキングリンクを通したリンクだけです。見込み高の人は GET /api/friends?tagId=TAG_HOT_ID で一覧にし、スタッフが週1回声をかけます。点の内訳は GET /api/friends/:id/score で見られるので、入会した人の内訳を1か月分見て重みを直します。
AIのエージェントから操作する(MCP・SDK)
Claude CodeなどのAIのエージェントから操作する入口として、MCPサーバー(@line-harness/mcp-server)とTypeScriptのSDK(@line-harness/sdk)があります。MCPはプロジェクトの .mcp.json に書きます。
{"mcpServers": {"line-harness": {
"command": "npx", "args": ["-y", "@line-harness/mcp-server@latest"],
"env": {"LINE_HARNESS_API_URL": "https://line.example.com", "LINE_HARNESS_API_KEY": "YOUR_API_KEY"}}}}
つなぐと、「体験申込タグの友だちを一覧にして」と頼めば list_friends がタグで絞って返します。配信や送信のツールは本家のREADMEも確認が要るものとして挙げているので、送る操作は文面と対象を人が見てから通します。APIキーは配信までできる鍵なので、.mcp.json をGitに入れません。
Googleカレンダー連携の予約に移す
L HarnessのGitHubのドキュメントには、メニュー・担当スタッフ・曜日ごとの受付時間・Googleカレンダーの予定から空き枠を出す予約管理も書かれています。ドキュメントでは「予約管理」でメニューを作り、スタッフの「受付時間」を保存し、「Googleアカウントで接続」でつなぐと、確定した予約に前日と1時間前のリマインダーが付く流れです。ただ、2026年10月27日時点の管理画面の左メニューには「予約管理」が出ていません。使うときは、そのときの版で画面に出ているかを先に確かめます。本家の手順はウェビナーからの相談予約向けで、Google CloudでOAuthクライアントの用意も要ります。定員を機械で守る必要が出たら移す位置づけです。
できないことと、その代わり
| できないこと | 代わりの手 |
|---|---|
| フォームの枠ごとの定員管理 | スタッフがチャットで確定を送る。機械で守るなら外部の予約システム(L Harnessの予約管理は管理画面に出ているかを先に確かめる) |
| 決めた時刻ちょうどの送信 | 定期処理が5分おきなので、数分のずれを前提にした文面にする |
| 元のQRコードやID検索から来た人の経路の記録 | 配る入口を全部 /auth/line?ref=〜 とそのQRにする |
| Webhookを別のツールと同時に受ける | L Harnessの送信Webhookで先のツールに渡す |
| 無料枠を超える量のデータ | Cloudflare D1の無料枠は1日あたり読み取り500万行・書き込み10万行で、超えると止まる。Workers Paid(月5ドル)にする(2026年10月時点) |
管理画面の拡張ツールと比べて手間がかかるところ
Lステップやエルメのような月額の拡張ツールは、ここまでを画面の操作で組め、聞ける窓口もあります。L Harnessは窓口が無く、次の手間も増えます。
- オートメーションの条件、Flexメッセージ、リッチメニューのタップ範囲をJSONで書き、メニューの画像も自分で作る
- 更新を自分で当てる。データベースの変更を伴う版もあり、当て忘れると新しい画面だけ出て動かないことがある
- データの控えを取る。D1は
wrangler d1 executeでテーブルごとに書き出せるが、戻すのは手作業 - スプレッドシートへの記録や通知を、GASやn8nで自分でつなぐ
逆に、月額0円、全機能をAPIで触れる、データが自分のCloudflareに残る、は画面だけのツールにない点です。比べ方は「L Harnessとは:月額0円で使えるLINEの拡張ツール。Lステップとの違いと、かかる費用」に書いています。
よく詰まるところ
| 症状 | 原因 | 直し方 |
|---|---|---|
| 友だち追加してもL Harnessに出てこない | Webhook URLの誤り、「Webhookの利用」がオフ、別のツールが受けている | Messaging API設定でURLが /webhook で終わるかと「検証」の結果を見る |
| あいさつが2通届く | 管理画面の「あいさつメッセージ」とL Harnessの友だち追加シナリオが両方動いている | 片方をオフにする |
| 流入経路のタグが付かない | 元のQRコードやID検索から追加された | 入口を /auth/line?ref=〜 のURLとそのQRに差し替える |
| リッチメニューが変わらない | 画像を上げていない、サイズか容量が要件外 | 2500×1686か2500×843・1MB以下で上げ直し、既定の設定まで済ませる |
| 入会済みタグを外したら会員メニューに戻った | 付けるのも外すのも同じ tag_change で、ドキュメントにある条件はタグのIDだけ | 外さずに退会タグを付け、remove_rich_menu を動かす |
| 見込み高の人に同じメッセージが何度も届く | 点数の条件は「指定値以上」なので、超えたあとも点が動くたびに当てはまりうる | 点数のオートメーションはタグ付けだけにする(同じタグは重ねて付かない) |
| フォームを送ったのにタグが付かない | LINEの外のブラウザで開き、誰が送ったか分からなかった。タグ付けが失敗しても送信は成功扱い | liff.line.me のURLでLINEの中から開く。回答一覧で friendId が空の回答を確かめる |
| リマインダーやステップが数分遅れる | 定期処理が5分おき。ステップ配信は前後5分のずれをわざと入れている | 「本日19時からです」のように、ずれても困らない文面にする |
| 月の途中で配信が止まった | 無料メッセージ通数を使い切った。コミュニケーション・ライトは追加で送れない | 通数を見直すか、プランを上げる |
原因が分からないときは、まず GET /api/automations/:id/logs でアクションごとの成否を見ます。管理画面の左メニュー「オートメーション」には「+ 新規ルール」のボタンとルールの一覧が出ますが、ルールが0件のうちは実行ログの画面は出ません。
次にやること
- 入れる前の比較と費用は「L Harnessとは:月額0円で使えるLINEの拡張ツール。Lステップとの違いと、かかる費用」
- まだ入れていなければ「L Harnessの入れ方:Cloudflare・LINE Developersの準備から最初の配信まで」
- 文面と送る間隔の決め方は「ステップ配信の作り方:友だち追加から自動で送るシナリオ」
- 友だちとやりとりを顧客管理に入れるなら「LINEとkintoneをつなぐ:友だちとやりとりを顧客管理に入れる」