Hello World|Labs
Published on
Updated on

n8nを使ったお問い合わせ受付フローの作成(パート2)

パート1ではお問い合わせ内容をDBに登録してチャットに通知するワークフローを作成しました。 本記事で通知を受けた後のフォローアップとして、チケットの登録および顧客管理システム(CRM)への登録を行うワークフローを作成します。

フロー概要

作成するワークフローは以下のようになります。 まず、管理者がチャットに通知されたお問い合わせ内容を確認し、チケットを登録するコマンドをチャットを使ってメッセージ送信します。次に、コマンドで送信されたお問い合わせ番号でDBを照会し、登録されているお問い合わせ情報を取得します。そして、取得したお問い合わせ情報をもとにチケット管理システムでチケットを発行します。CRMへの登録もチケットと同様にコマンドを使って行います。

システム構成

今回のシステム構成は下図のようになります。カスタマーサポートはMattermostし、命令文を使ってお問い合わせ番号をメッセージ送信します。お問い合わせ番号がMattermost経由でn8nに送信され、n8nで作成したワークフローが実行されます。

ResourceUsageHosting TypeLicensing Model
Caddyバックエンドサービスに対するHTTPS通信を中継、SSLオフロードやIPアドレスによるアクセス制限などを行うSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Mattermostお問い合わせ内容の通知先および命令文の実行Self Hosting(Docker container on Hetzner Cloud)Freemium
Open Source
n8nMattermostからのイベントをトリガーにしてワークフローを実行するSelf Hosting(Docker container on Hetzner Cloud)Freemium
Open Source
Zammadチケットの発行・管理を行うSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Twenty顧客情報の管理を行うSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Nocodbお問い合わせDBの外部インターフェースおよび管理UIを提供するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
PostgreSQLお問い合わせ内容、チケット、顧客情報の保存Self Hosting(Docker container on Hetzner Cloud)Free
Open Source

フローの作成

コマンド送信

MattermostはSlash commandsというSlackのスラッシュコマンドと同等の機能を有しています。ビルトインのコマンド以外にユーザー自身が独自のコマンドを追加することができ、特定のURLに対してPOSTまたはGETのHTTPリクエストを送信することができます。今回はチケットの作成とCRMへの登録の2種類のコマンドを作成します。

スラッシュコマンドはIntegrationsからSlash Commandsを選択しAdd Slash Commandで作成することができます。

今回は以下のように設定します。

■チケット作成

NameValue
Title任意のタイトルを設定
Description空白
Command Trigger Wordnew_ticket
Request URLWebフックのURLを設定
Request MethodPOST
Response Username空白
Response Icon空白
Autocompleteチェック
Autocomplete Hint[Contact Number]
Autocomplete Description空白

■CRM登録

NameValue
Title任意のタイトルを設定
Description空白
Command Trigger Wordnew_lead
Request URLWebフックのURLを設定
Request MethodPOST
Response Username空白
Response Icon空白
Autocompleteチェック
Autocomplete Hint[Contact Number]
Autocomplete Description空白

コマンドを作成すると下図のようにトークンが発行されます。このトークンはリクエスト送信時のAuthorizationヘッダーおよびPOSTパラメータとして送信されます。

今回HTTPリクエストの送信先であるn8nは同一サーバー内のローカルネットワーク上にあります。Mattermostはローカルネットワーク間での通信を制限しているため許可リストにn8nを追加する必要があります。許可リストの設定はSystem ConsoleENVIRONMENTからDeveloperを選択しAllow untrusted internal connections toに許可したいホスト名やIPアドレス、CIDRをスペース区切りで入力して行います。

以上でMattermost側の設定は完了です。

コマンドの受信

次にn8nの設定を行います。 まずコマンドからのリクエストを受信するWebフックを作成します。 今回はチケット作成コマンド用のWebフックとCRM登録用のWebフックの2種類を作成します(ノードパネルからOn webhook callを選択)。

ノードを2つ追加して設定パネルでそれぞれ以下のように設定します(設定内容はそれぞれのコマンドでCredentialとPathの値を変更してください)。

NameValue
AuthenticationHeader Auth
Credential for Header AuthNameにAuthorization、ValueにToken <Mattermostでコマンド作成時に発行されたトークン>を設定
HTTP MethodPOST
Path任意のパスを設定
RespondUsing 'Respond to Webhook' Node

お問い合わせ番号照会

コマンドを受信したWebフックノードの出力は以下のようにヘッダー、パラメータ、クエリ、ボディで構成されたJSONデータになります。 ボディ部に実行したコマンドやコマンドのトークン、お問い合わせ番号が格納されています。

[
  {
    "headers": {},
    "params": {},
    "query": {},
    "body": {
      "channel_id": "...",
      "channel_name": "...",
      "command": "/new_ticket",
      "response_url": "...",
      "team_domain": "...",
      "team_id": "...",
      "text": "cs_1234567890",
      "token": "abcdefghijklmnopqrstuvwxyz",
      "trigger_id": "...",
      "user_id": "...",
      "user_name": "..."
    }
  }
]

まずはデータからボディ部のみを取り出すためにCodeノードを追加します(ノードパネルのData transformationから選択)。

ノードを追加して設定パネルのコード入力欄に以下のJavaScriptを入力します。 $inputは現在のノードに対する入力を表す変数で、一つ前のWebhookノードからの出力を参照しています(参考:Current node input)。

newItems = []
 
for (const item of $input.all()) {
  newItems.push(item.json.body)
}
 
return newItems

これで後続のノードは以下のように階層のないフラットな構造のJSONデータで参照できるため記述が簡潔になります。

[
  {
    "channel_id": "...",
    "channel_name": "...",
    "command": "/new_ticket",
    "response_url": "...",
    "team_domain": "...",
    "team_id": "...",
    "text": "cs_1234567890",
    "token": "abcdefghijklmnopqrstuvwxyz",
    "trigger_id": "...",
    "user_id": "...",
    "user_name": "..."
  }
]

次にお問い合わせ番号を使ってDBに登録されているお問い合わせ情報を照会する処理を追加します。 NocoDBと連携するためのノードはn8nに標準で用意されており、データを一意に特定するIDをキーにして1件取得することができます。 ただし、NocoDBのノードではID以外のキーを指定できないため、お問い合わせ番号で照会することができません。 代わりにHTTP RequestノードからNocoDBのREST APIを実行する方法で照会を行います(ノードパネルのHelpersから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
MethodGET
URLhttp://(NocoDBのサービス名):8080/(NocoDB APIの一覧取得エンドポイント)
AuthenticationPredefined Credential Type
Credential TypeNocoDB API Token
Credential for NocoDB API Tokenパート1のお問い合わせ内容登録で作成したクレデンシャルを設定
Send Query Parameters CodeON
Specify Query ParametersUsing Fields Below
Query Parameters (Name1)where
Query Parameters (Value1)(contact_no,eq,{{ $json.text }})

APIのエンドポイントはNocoDBの管理画面で対象テーブルのベースをマウスオーバーして表示される3点リードからSwagger: REST APIsを選択することで確認することができます。

これでお問い合わせ番号での照会ができるようになりましたので、IFノードを追加してデータが存在する場合はワークフローを継続し、存在しない場合はワークフローを終了してエラーレスポンスを返すようにします(ノードパネルのFlowから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
ConditionsNumber
Value 1{{ $json.pageInfo.totalRows }}
OperationEqual
Value 21

$json.pageInfo.totalRowsは該当データの件数を表すAPIレスポンス値で、件数が1の場合はtrue、1以外の場合はfalseとなります。 falseの場合はエラーレスポンスを返すようにするためにfalseのブランチにRespond to Webhookノードを追加します(ノードパネルのFlowから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Respond WithText
Response Body任意のエラーメッセージ

Mattermostでコマンドを実行してお問い合わせ番号照会に失敗した場合、ここで設定した任意のエラーメッセージがMattermostに表示されます。

次にtrueの場合のフローを作成していきます。 チケット発行とCRM登録はフローを分ける必要があるため、IFノードのtrueのブランチにSwitchノードを追加します(ノードパネルのFlowから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
ModeRules
Data TypeString
Value1{{ $('前出Codeノードの名前').item.json.command }}
OperationEqual
Value 2/new_ticket
Output Keynew_ticket
OperationEqual
Value 2/new_lead
Output Keynew_lead

これでコマンド毎に別々のフローを作成できるようになりました。

チケット発行

チケットの発行は以下のようなフローで行います。最初にお問い合わせユーザーがチケット管理システムにユーザー登録されているかどうかを確認します。登録されている場合はチケットを発行し、登録されていない場合はユーザー登録した上でチケットを発行します。

まず、先ほど作成したSwitchノードのnew_ticketとラベリングされたブランチにユーザー照会のためのノードを追加します。 n8nには今回使用するチケット管理システム(Zammad)からユーザーを取得するノードが標準で用意されていますのでそちらを使用します(ノードパネルのAction in an appから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Credential to connect withZammadで発行したアクセストークンとZammadのホストアドレス(http://(Zammadのサービス名):3000/)を設定
ResourceUser
OperationGet Many
Return AllON

アクセストークンはユーザープロファイルから発行することができます。発行するトークンに必要なパーミッションはticket.agentになります。 どのパーミッションが必要になるかはシステムドキュメントの各APIのエンドポイントに記載されていますのでそちらをご参照ください。

Note

今回Zammadのノードで全ユーザーを取得していますが、お問い合わせ情報のメールアドレスを条件にして1件だけ取得することも可能です。 本来は絞り込んで取得すべきですが条件を指定した検索にはElasticsearchが必要となるため今回は全ユーザーを取得しています。

次に、取得した全ユーザーからお問い合わせユーザーのメールアドレスと一致するユーザーを取り出すためにCodeノードを追加します。

ノードを追加して設定パネルのコード入力欄に以下のJavaScriptを入力します。 下記コードでZammadで取得した全ユーザーからお問い合わせ情報のメールアドレスと一致するユーザーを取り出しています。

const user = $input.all().filter(item => item.json.email === $('お問い合わせ番号照会ノードの名前').item.json.list[0].email)
 
return user.length === 0 ? 
  [
    {
      json: {
        exists: false,
        user: null
      },
      pairedItem: 0
    }
  ] : 
  [
    {
      json: {
        exists: true,
        user: user
      },
      pairedItem: 0
    }
  ]

Important

pairedItemはこの出力アイテムのもととなった入力アイテムのインデックスを表すメタデータです。 n8nにはItem linkingという概念があり、ノードによって作成された各出力アイテムには、それらを生成するために使用した入力アイテムにリンクするためのメタデータが含まれています。 n8nでは明示的にメタデータを設定しない限り自動で入力アイテムと出力アイテムの紐づけが行われますが、今回は入力アイテムをもとに全く新しい出力アイテムを作成しており自動紐づけができません(紐づけが行われていないと後続のワークフローでエラーになります)。 そのため明示的にメタデータを設定しています。

次に、IFノードを追加して一致するユーザーがいる場合といない場合の分岐を作成します。

ノードを追加して設定パネルで以下のように設定します。

NameValue
ConditionsBoolean
Value 1{{ $json.exists }}
OperationEqual
Value 2{{ true }}

分岐を作成したらfalseブランチの方にユーザー登録のためのノードを追加します。n8nにはZammadにユーザーを登録するノードが標準で用意されていますのでそちらを使用します(ノードパネルのAction in an appから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Credential to connect withユーザー照会と同じ認証情報を使用
ResourceUser
OperationCreate
First Name{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].first_name }}
Last Name{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].last_name }}
Email Address{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].email }}

次に、trueのブランチの方にチケット発行のためのノードを追加し、先ほど追加したユーザー登録のノードからもリンクします。

Note

n8nにはチケットを作成するノードが標準で用意されていますが、ZammadのチケットAPIで指定できるパラメーターを部分的にしか対応していません。 今回は対応していないパラメーターを使用するためHTTP Requestノードで代替しています。

ノードを追加して設定パネルで以下のように設定します。

NameValue
MethodPOST
URLhttp://(Zammadのサービス名):3000/api/v1/tickets
AuthenticationGeneric Credential Type
Credential TypeHeader Auth
Credential for Header AuthNameにAuthorization、ValueにToken token=<Zammadで発行したアクセストークン>を設定
Send BodyON
Body Content TypeJSON
Specify BodyUsing JSON
JSON下記参照
{
   "title": "任意のチケットタイトル",
   "group": "Zammadで作成したグループ",
   "customer": "{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].email }}",
   "article": {
      "subject": "空白もしくは任意のサブジェクト",
      "body": "{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].message.replace(/\r?\n/g, '\\n') }}",
      "type": "web",
      "internal": true,
      "sender": "Customer"
   }
}

最後にRespond to Webhookノードを追加して完了メッセージを返します。

これでチケット発行のフローは完成です。次にCRM登録のフローを作成します。

CRM登録

CRMへの発行は以下のようなフローで行います。最初にお問い合わせユーザーがCRMにリード登録されているかどうかを確認します。登録されている場合はワークフローを終了し、登録されていない場合はリード登録を行います。

n8nには今回使用するCRM(Twenty)と連携するためのノードは用意されていません。代わりにGraphQLノードからTwentyのGraphQL APIを実行する方法でユーザー照会やリード登録を行います。

TwentyのGraphQL APIを実行するためにはJWTによるトークンベース認証が必要となります。JWTに設定するペイロードは以下の通りです。

ClaimValue
subユーザーID(ワークスペース管理者のID)
workspaceIdワークスペースのID
iatトークンを発行した日時
expトークンの有効期限(期間は任意)

subworkspaceIdにセットする値はTwentyの画面上で確認することができないため、TwentyのDBを直接確認して値をセットします。 iatexpはワークフローの中で動的に算出してセットします。 そこで、最初にSwitchノードのnew_leadとラベリングされたブランチに値を算出するためのCodeノードを追加します。

ノードを追加して設定パネルのコード入力欄に以下のJavaScriptを入力します。

const iat = $now.toSeconds()
const exp = iat + 3600
 
return [
  {
    json: {
      iat: iat,
      exp: exp
    },
    pairedItem: 0
  }
]

ペイロードの値が揃いましたのでJWTの署名を行います。ただし、n8nにはJWTの署名を行うためのノードがありませんので、今回はn8n-nodes-jwtというコミュニティノードを使用します。 公式ドキュメントの手順に従ってコミュニティノードをインストールしてくだい。インストールするとノードパネルのAction in an appJWTノードが追加されますのでフローに追加します。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Credential to connect withKey TypeにPassphrase、Secretに環境変数TWENTY_ACCESS_TOKEN_SECRETの値を設定
OperationSign
AlgorithmHS256
Advanced Claim BuilderON
Claims下記参照
{
  "sub": "userテーブルに登録されている値を入力",
  "workspaceId": "workspaceテーブルに登録されている値を入力",
  "iat": {{ $json.iat }},
  "exp": {{ $json.exp }}
}

Note

n8バージョン1.37.0でJWTノードが標準搭載されるようになったため上記コミュニティノードのインストールは不要です。

JWTの用意ができましたのでGraphQLノードを追加してユーザー照会を行います(ノードパネルのAction in an appから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
AuthenticationNone
HTTP Request MethodPOST
Endpointhttp://(Twenty Serverのサービス名):3000/graphql
Request FormatGraphQL(Raw)
Query下記参照
Response FormatJSON
Headers (Name1)Authorization
Headers (Value1)Bearer {{ $json.token }}
query FindManyPeople {
  people(
    first:1,
    filter:{
      and:[
        {
          email:{
            eq:"{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].email }}"
          }
        }
      ]
  	}
  ) {
    edges {
      node {
        id
      }
    }
  }
}

Note

AuthenticationにHeader Authがありますが、今回は固定値ではなく動的に生成したトークンをセットするため使用していません。 代わりにHeadersにAuthorizationを追加してそちらにトークンをセットしています。

ユーザーが存在する場合は何もせずにワークフローを終了しますのでIFノードを追加してtrueブランチにRespond to Webhookノードを追加します。

IFノードを追加して設定パネルで以下のように設定します。

NameValue
ConditionsNumber
Value 1{{ $json.data.people.edges.length }}
OperationEqual
Value 21

ユーザーが存在しない場合はリード登録を行いますのでGraphQLノードを追加します。

NameValue
AuthenticationNone
HTTP Request MethodPOST
Endpointhttp://(Twenty Serverのサービス名):3000/graphql
Request FormatGraphQL(Raw)
Query下記参照
Response FormatJSON
Headers (Name1)Authorization
Headers (Value1)Bearer {{ $('JWT署名ノードの名前').item.json.token }}
mutation CreatePerson {
  createPerson(
    data:{
      name: {
        firstName: "{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].first_name }}",
        lastName: "{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].last_name }}"
      },
      email: "{{ $('お問い合わせ番号照会ノードの名前').item.json.list[0].email }}"
    }
  )
  {id}
}

動作確認

ワークフローの全体像は以下のようになります。

ワークフローをアクティブにしてMattermostでスラッシュコマンドを実行してみます。

最初にチケットを発行してみます。

チケット発行に成功するとRespond to Webhookノードで設定したメッセージが返ってきます。 Zammadにアクセスするとチケットが発行されていることが確認できます。

n8nにアクセスするとワークフローが実行されていることが確認できます。

次ににリードを登録してみます。

チケット発行に成功するとRespond to Webhookノードで設定したメッセージが返ってきます。 Twentyにアクセスするとリードが登録されていることが確認できます。

n8nにアクセスするとワークフローが実行されていることが確認できます。