L Harnessで予約・フォーム・ステップ配信を組む:教室の例で一通り

公開 書いた人 大林 諒

L Harnessは、LINE公式アカウントにステップ配信・タグ・フォーム・リマインダー・スコアリングを足すオープンソースの拡張ツールです(GitHub Shudesu/line-harness-oss、MITライセンス)。この記事では、サンプル料理教室の友だち追加から体験予約、リマインダー、入会の案内、問い合わせの通知までを、L Harnessの機能だけで一通り組みます。前提は、L HarnessをCloudflareに入れ終え、管理画面のAPIキーが手元にあることです。入れ方は「L Harnessの入れ方:Cloudflare・LINE Developersの準備から最初の配信まで」にあります。

この記事の内容
  1. 先に決めること
  2. 作るものの一覧
  3. 組む手順
  4. 応用
  5. できないことと、その代わり
  6. よく詰まるところ
  7. 次にやること

先に決めること

  • 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-routesrefCode、tagId
あいさつシナリオ配信POST /api/scenariosfriend_add、0分
リッチメニューリッチメニュー、オートメーションPOST /api/rich-menus、POST /api/automations2500×1686、switch_rich_menu
体験予約フォーム回答POST /api/formsonSubmitTagId、saveToMetadata
前日・当日の連絡リマインダPOST /api/reminders/:id/enroll/:friendId-1440、-120
体験後の案内シナリオ配信POST /api/scenarios/:id/stepstag_added、tag_not_exists
問い合わせ個別チャット、自動返信ルール、WebhookPOST /api/webhooks/outgoingmessage_received
反応のよい人マイルPOST /api/scoring-rulesscore_threshold

手順は本家のドキュメントに合わせてAPIで書きます。

L Harness管理画面のサイドメニュー(友だち管理・個別チャット・シナリオ配信・一斉配信・リマインダ・流入経路・フォーム回答・オートメーションが並ぶところ)
L Harness管理画面のサイドメニュー(友だち管理・個別チャット・シナリオ配信・一斉配信・リマインダ・流入経路・フォーム回答・オートメーションが並ぶところ)

組む手順

curlは最初に次の2行を打ってから使います。返る id は控えます。

BASE=https://line.example.com
KEY=YOUR_API_KEY

1. LINE側の設定を合わせる

  1. LINE Developersで、Messaging APIのチャネルを開く
  2. 「Messaging API設定」タブの「Webhook URL」が https://line.example.com/webhook か確かめる
  3. 「検証」を押して「成功」と出るのを確かめる
  4. 「Webhookの利用」がオンか確かめる
  5. 同じタブの「あいさつメッセージ」の「編集」から管理画面を開き、オフにする
  6. 同じく「応答メッセージ」をオフにする
  7. チャットはオンのままにする
LINE DevelopersのMessaging API設定タブ(Webhook URL・検証・Webhookの利用)
LINE DevelopersのMessaging API設定タブ(Webhook URL・検証・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 を指定できますが、次のあいさつのシナリオと重なるので空にしておきます。

L Harness管理画面の「分析」の「流入経路」(見出し「流入経路分析」)。経路を作る前の空の状態
L Harness管理画面の「分析」の「流入経路」(見出し「流入経路分析」)。経路を作る前の空の状態

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ページ)よくある質問(文字を送る)問い合わせ(文字を送る)
会員次のレッスンの予約今月の日程持ち物・アクセス問い合わせ
  1. POST /api/rich-menus に、size を {"width": 2500, "height": 1686}、areas に4つのタップ範囲(1つが幅1250・高さ843)を入れて送る
  2. 左上の範囲の action は {"type": "uri", "uri": "https://liff.line.me/YOUR_LIFF_ID?formId=FORM_TRIAL_ID"}、左下は {"type": "message", "text": "よくある質問"} にする(フォームのIDは手順5で作ったもの)
  3. POST /api/rich-menus/:id/image に画像を送る(PNGかJPEG、2500×1686、1MB以下)
  4. 体験前のメニューだけ POST /api/rich-menus/:id/default で全員の既定にする
  5. 会員メニューも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 で止めます。

スプレッドシートの体験予約シート(回答ID・日時・名前・基準日時が並んだところ)
スプレッドシートの体験予約シート(回答ID・日時・名前・基準日時が並んだところ)

7. 体験後のステップ配信を作り、入会した人で送り分ける

POST /api/scenarios に "triggerType": "tag_added" と、triggerTagId に体験参加済みタグのIDを入れて作ります。レッスン後にスタッフがタグを付けた人だけに送られます。

順番前のステップから条件内容
160分なし体験のお礼と当日のレシピ
21日(1440)入会済みタグが無い月謝とコースの案内
32日(2880)入会済みタグが無い体験した人からよく出る質問
43日(4320)入会済みタグが無い入会金が無料になる期限
514日(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つで、メニューの切り替え・案内の停止・会員向けシナリオの開始が一度に起きます。

L Harness管理画面の「シナリオ詳細」(「ステップ一覧」に即時・1時間後…と並んだところ)
L Harness管理画面の「シナリオ詳細」(「ステップ一覧」に即時・1時間後…と並んだところ)

8. 問い合わせをチャットで受け、スタッフに知らせる

「よくある質問」のボタンは文字を送るので、自動返信で答えます。POST /api/auto-replies に {"keyword": "よくある質問", "matchType": "exact", "responseContent": "持ち物:エプロン…"} を送ります。matchType は完全一致(exact)か部分一致(contains)です。自動返信はReply APIで返すので通数に数えません。

自動返信に当たらないメッセージは、管理画面の左メニュー「個別チャット」(ページの見出しは「オペレーターチャット」)に「未読」で並びます。タブは「全て」「未読」「対応中」「解決済」です。返信したら「対応中」、終われば「解決済」にし、同じ人がまた送ると「未読」に戻ります。

L Harness管理画面の「オペレーターチャット」の一覧(全て・未読・対応中・解決済のタブ)
L Harness管理画面の「オペレーターチャット」の一覧(全て・未読・対応中・解決済のタブ)

スタッフへの通知は、送信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をつなぐ:友だちとやりとりを顧客管理に入れる」

あわせて読む