この記事は chillSAP 夏の自由研究2021 の記事として執筆しています。
#1. はじめに
SAP BTPのWorkflow Serviceは、SAP Workflow Managementに含まれるサービスの一つです。Workflow Serviceを使うことで、BTP上でユーザの承認を伴うプロセスの実装や、複数システムを連携させてタスクを遂行する、といったことが可能になります。
図:SAP Workflow Management – Live Process Contentより引用
SAP Workflow ManagementにはLive Process Content Packagesと呼ばれる、事前定義されたパッケージが用意されており、これらは設定だけで使うことができます。
一方、独自のワークフローを一から作る場合は以下のようなワークフローエディターでフローを定義していくのですが、こちらはかなり自由度が高く、やりたいことを実現するには知恵と工夫が必要です。
#2. PoCのテーマ
今回のテーマは、**「実際にありそうなワークフローの要件を実現することができるか?」**ということです。
具体的には、以下のような要件です。
- 承認者が後続の承認者を変更、追加する
- 引き戻し:依頼者、または中間承認者がタスクを取り戻して編集し、再申請する
この検証をする前までは、Workflow Serviceではプロセスは一方通行で進むので、特に引き戻しは不可能ではないかと思っていました。
しかし、APIを使ってワークフローのステータスやコンテキストの情報を変更できることがわかり、実現の可能性が見えてきました。
#3. Workflow Serviceにおけるワークフローのライフサイクル
検証にあたり、まずはワークフローのライフサイクルを確認しておきます。
##3.1. ワークフローインスタンス
ワークフローを開始すると、ワークフローインスタンスが登録されます。ワークフローインスタンスとは、ワークフローの定義を参照して登録されるワークフローの実体です。
ワークフローインスタンスのステータスは以下のように遷移します。通常目にするステータスはRUNNING
(実行中)、またはCOMPLETED
(完了)です。
図:Status Changes for Workflow Instancesより引用
API /v1/workflow-instances/{workflowInstanceId}
を使うと、ワークフローのステータスをRUNNING
からSUSPENDED
またはCANCELED
に変更できます。また、SUSPENDED
になったワークフローをRUNNING
に戻すこともできます。
##3.2. タスクインスタンス
ワークフローが開始しユーザータスクのステップに到達すると、タスクインスタンスが登録されます。これにより、承認者の受信ボックスにタスクが入ってきます。
タスクのインスタンスのステータスは以下のように遷移します。タスクが受信ボックスにある状態はREADY
、承認、却下などのアクションにより完了するとCOMPLETED
となります。
図:Status Changes for Task Instancesより引用
API /v1/task-instances/{taskInstanceId}
を使うと、タスクのステータスをCOMPLETED
に変更することができます。
#4. 実現方針
以下の要件をどのように実現するか検討します。
- 承認者が後続の承認者を変更、追加
- 引き戻し
##4.1. 承認者が後続の承認者を変更、追加
###4.1.1. 要件の定義
- 承認者が受信ボックスからタスクを開くと、ワークフローに設定された承認者のリストが表示される
- 承認者は、自分より後の承認者を追加または変更することができる
###4.1.2. 実現方針
ワークフローを以下のように定義することで実現できそうです。
- ワークフローに渡されるコンテキスト(データ)を使って動的に承認者を決定する
- コンテキストはワークフローの開始時に設定し、承認時に上書き可能とする
- 全ての承認ステップが完了するまで、ユーザータスクを繰り返し登録する
これが実現できる前提には、先行のタスクが完了しない限り、次のタスクのインスタンスは登録されないというWorkflow Serviceの特徴があります。このおかげで、次のタスクが開始されるまでは自由に承認者を変更することができます。
##4.2. 引き戻し
###4.2.1. 要件の定義
- 依頼者、または中間承認者は、自分の次のステップの承認者が未承認の段階において、申請を一時的に取り下げることができる(これを「引き戻し」と呼ぶ)
- 引き戻しされると、次ステップの承認者の受信ボックスからタスクが消える
- 引き戻ししたユーザーは、申請内容を変更することができる。この際、次ステップの承認者を変更することも可能とする
- 引き戻ししたユーザーが再度申請を行うことにより、ワークフローが再開する
###4.2.2. 実現方針
引き戻し実現のポイントは、2つあります。
①引き戻しされると、次ステップの承認者の受信ボックスからタスクが消える
これについては、ワークフローのステータスをSUSPENDED
に変えることで実現できます。
②引き戻ししたユーザーが再度申請を行うことにより、ワークフローが再開する
引き戻しの際に次のステップの承認者を変更するケースと変更しないケースに分けて考えてみます。
a. 次のステップの承認者を変更しないケース
このケースでは、単純にワークフローのステータスをRUNNING
に戻すことによってワークフローを再開することができます。これにより受信ボックスから消えていたタスクが戻ってきます。
b. 次のステップの承認者を変更するケース
一度登録されたタスクインスタンスはAPIによって中止、取り消しができません。よって、まず実行中のワークフローのステータスをCANCELED
に変えて中止し、その上で新規のワークフローインスタンスを登録します。2つのワークフローインスタンスは関連を持たないので、承認履歴を新しいワークフローインスタンスのコンテキストに引き継ぐなどの工夫が必要になります。
承認者の変更有無によって方法を変えるのは煩雑なので、b.の方法で統一することにします。
#5. 検証方法
以下の方法で検証を行います。
- 簡単なワークフローを作成
- APIを使用してワークフローを登録したり、ステータスを変更したりする
##5.1. 簡単なワークフローを作成
4.1.2.のイメージでワークフローを作成します。
開始時にコンテキストに承認者のリストを受け取る想定としています。
{
"approvalSteps": [{
"id": "xxxx@gmail.com",
"isComplete": false,
"decision": ""
},
{
"id": "xxxx@gmail.com",
"isComplete": false,
"decision": ""
}]
}
Initializeのスクリプトで次の承認者を決定し、コンテキストのnextApprover
に設定します。
// find next approver
var approvalSteps = $.context.approvalSteps;
var nextApproverIndex;
for (var i = 0; i < approvalSteps.length; i++) {
if (!approvalSteps[i].isComplete) {
nextApproverIndex = i;
break;
}
};
var nextApprover = {
id: approvalSteps[nextApproverIndex].id,
index: nextApproverIndex
}
$.context.nextApprover = nextApprover;
$.context.isComplete = false;
$.context.decision = "";
ユーザータスクの直後のProcessResultのスクリプトで完了済のステップにisComplete
フラグを設定します。また、却下された場合はそこでプロセス終了とします。
//check approval result
var isApproved = false;
if ($.context.decision === "approve" || $.usertasks.usertask1.last.decision === "approve") {
isApproved = true;
}
//set current approval status to complete
var approvalSteps = $.context.approvalSteps;
var index = $.context.nextApprover.index;
approvalSteps[index].isComplete = true;
approvalSteps[index].decision = isApproved ? "approve" : "reject";
$.context.approvalSteps = approvalSteps;
//check if this is the final step
if(approvalSteps.length === index + 1 || !isApproved) {
$.context.isComplete = true;
$.context.finalDecision = approvalSteps[index].decision;
}
##5.2. サービスインスタンスの登録
APIを呼ぶためには、各エンドポイント/メソッドに対応した権限が必要になります。必要な権限はAPI Business Hubで確認することができます。
必要な権限を持ったサービスインスタンスを作成します。このためにまず、持たせたい権限を設定したJSONファイルを作成します。
※以下のファイルでは今回使用する権限よりも多くを設定しています
{
"authorities": [
"WORKFLOW_INSTANCE_START",
"WORKFLOW_INSTANCE_GET_CONTEXT",
"WORKFLOW_INSTANCE_GET",
"TASK_GET",
"TASK_COMPLETE",
"TASK_UPDATE",
"TASK_GET_CONTEXT",
"WORKFLOW_INSTANCE_UPDATE_CONTEXT",
"WORKFLOW_INSTANCE_RETRY_RESUME",
"WORKFLOW_INSTANCE_SUSPEND",
"WORKFLOW_INSTANCE_CANCEL",
"WORKFLOW_INSTANCE_GET_EXECUTION_LOGS"
]
}
以下のコマンドでワークフローのサービスインスタンスを作成します。
cf create-service workflow lite wf_sample -c authorities.json
APIを呼ぶために必要なサービスキーを作成します。
cf create-service-key wf_sample key1
サービスキーを表示します。
cf service-key wf_sample key1
##5.3. APIを使用した検証
Postmanを使用してAPIを呼びます。まず、"Authorization"タブで認証の設定をしておきます。
項目 | 設定値 |
---|---|
Type | OAuth 2.0 |
Grant Type | Client Credentials |
Access Token URL | サービスキーのuaa.url + /oauth/token |
Client ID | サービスキーのuaa.clientid |
Client Secret | サービスキーのuaa.secret |
上記項目を設定したら、"Get New Access Token"をクリックしてトークンを取得します。 |
###5.3.1. 承認者変更、追加の検証
####5.3.1.1. ワークフローを開始
以下のリクエストを発行し、ワークフローを開始します。
項目 | 設定値 |
---|---|
メソッド | POST |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances |
ヘッダ | application/json |
ボディ |
{
"definitionId": "<ワークフローの定義ID>",
"context": {
"approvalSteps": [{
"id": "xxxx.01@gmail.com",
"isComplete": false,
"decision": ""
},
{
"id": "xxxx.03@gmail.com",
"isComplete": false,
"decision": ""
}
]
}
}
※idはSAP ID Serviceに登録されているメールアドレス
レスポンスの例
{
"id": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
"definitionId": "multilevelapproval",
"definitionVersion": "31",
"subject": "multilevelApproval",
"status": "RUNNING",
"businessKey": "",
"parentInstanceId": null,
"rootInstanceId": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
"applicationScope": "own",
"startedAt": "2021-07-27T00:14:57.069Z",
"startedBy": "sb-clone-52964022-f257-4e19-a621-463a969e3e70!b54386|workflow!b10150",
"completedAt": null
}
####5.3.1.2. ユーザータスクのインスタンスを取得
以下のリクエストを発行し、ユーザータスクのインスタンスを取得します。
項目 | 設定値 |
---|---|
メソッド | GET |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/task-instances?workflowInstanceId=<ワークフローインスタンスのID> |
※ワークフローインスタンスのIDは前のステップのレスポンスに設定されたid
|
レスポンスの例
[
{
"activityId": "usertask1",
"claimedAt": null,
"completedAt": null,
"createdAt": "2021-07-27T00:14:58.022Z",
"description": null,
"id": "ad4431b2-ee6f-11eb-ba43-eeee0a9045ab",
"processor": null,
"recipientUsers": [
"xxxx.01@gmail.com
],
"recipientGroups": [],
"status": "READY",
"subject": "Multi Level Approval Test",
"workflowDefinitionId": "multilevelapproval",
"workflowInstanceId": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
"priority": "MEDIUM",
"dueDate": null,
"createdBy": "sb-clone-52964022-f257-4e19-a621-463a969e3e70!b54386|workflow!b10150",
"definitionId": "usertask1@multilevelapproval",
"lastChangedAt": "2021-07-27T00:14:58.022Z",
"applicationScope": "own"
}
]
####5.3.1.3. 承認
承認の際に自分の後の承認者を追加することを想定します。このために、コンテキストのapprovalSteps
を変更して渡します。
項目 | 設定値 |
---|---|
メソッド | PATCH |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/task-instances/<タスクインスタンスのID> |
ヘッダ | application/json |
※タスクインスタンスのIDは前のステップのレスポンスに設定されたid
|
ボディ
{
"status": "COMPLETED",
"context": {
"decision": "approve",
"approvalSteps": [{
"id": "xxxx.01@gmail.com", ←現在のステップ
"isComplete": false,
"decision": ""
},
{
"id": "xxxx.02@gmail.com", ←追加したステップ
"isComplete": false,
"decision": ""
},
{
"id": "xxxx.03@gmail.com",
"isComplete": false,
"decision": ""
}
]
}
}
このあとユーザータスクを再取得すると、1件目のタスクがCOMPLETED
になり、2件目のタスクができています。2件目の承認者は追加したステップの承認者となっているはずです。
これで、承認者の変更、追加が可能なことが確認できました。
###5.3.2. 引き戻しの検証
5.3.1.の最後の状態では、ステップ1が承認済、ステップ2の承認者のところにタスクがあります。ここからステップ1の承認者が取り戻しを行い、ステップ2の承認者を変更することを想定した操作を行ってみます。
#####5.3.2.1 ワークフローを一時停止
ワークフローのステータスをSUSPENDED
に変更し、ワークフローを一時停止します。
項目 | 設定値 |
---|---|
メソッド | PATCH |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID> |
ボディ
{
"status": "SUSPENDED",
"cascade": false
}
このあと、ステップ2の承認者を変更し、再申請を行ったと想定します。
#####5.3.2.2 ワークフローをキャンセル
ワークフローのステータスをCANCELED
に変更し、ワークフローをキャンセルします。
※SUSPENDEDを経ずにキャンセルしてもよいのですが、「取戻ししたけどやっぱりやめる」ケースもあるかもしれないので一応SUSPENDEDを入れています
項目 | 設定値 |
---|---|
メソッド | PATCH |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID> |
ボディ
{
"status": "CANCELED",
"cascade": false
}
#####5.3.2.3 キャンセルしたワークフローのコンテキストを取得
以下のリクエストを発行し、キャンセルしたワークフローのコンテキストを取得します。
項目 | 設定値 |
---|---|
メソッド | GET |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID>/context |
レスポンスの例
{
"approvalSteps": [
{
"decision": "approve",
"id": "xxx.01@gmail.com",
"isComplete": true
},
{
"decision": "",
"id": "xxx.02@gmail.com",
"isComplete": false
},
{
"decision": "",
"id": "xxx.03@gmail.com",
"isComplete": false
}
],
"decision": "",
"nextApprover": {
"id": "xxx.02@gmail.com",
"index": 1.0
},
"isComplete": false
}
#####5.3.2.4 新規ワークフローを開始
新規のワークフローを開始します。この際、承認済のステップについては前のワークフローから引き継ぐようにします。ここではステップ2、3の承認者を入れ替えました。
項目 | 設定値 |
---|---|
メソッド | POST |
URL | https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances |
ヘッダ | application/json |
ボディ |
{
"definitionId": "multilevelapproval",
"context": {
"approvalSteps": [{
"decision": "approve", ←承認済のステップ(引継ぎ)
"id": "xxx.01@gmail.com",
"isComplete": true
},
{
"decision": "",
"id": "xxx.03@gmail.com", ←承認者変更
"isComplete": false
},
{
"decision": "",
"id": "xxx.02@gmail.com", ←承認者変更
"isComplete": false
}
]
}
}
この結果、ワークフローがステップ2の承認者から開始します。ワークフローのインスタンスは分かれていますが、ユーザからは同じワークフローとして見えると思います。
#6. まとめ
ワークフローの設計、およびAPIでのステータス更新によって、以下の要件が実現できそうなことがわかりました。
- 承認者が後続の承認者を変更、追加
- 引き戻し:依頼者、または中間承認者がタスクを取り戻して編集し、再申請
ただし、実際に使う場合はワークフローだけでは完結せず、アプリ側でのコントロールも必要になります。
たとえば、承認者の追加、変更では、すでに承認済のステップを変更したり、自分の前に承認ステップを追加したりできないような制御が必要です。
引き戻しに関しては、ワークフローインスタンスが複数登録されることになるので、ユーザーからは一つのワークフローに見えるように、新規インスタンスの登録時に履歴を引き継ぐ工夫が必要です。履歴はコンテキストに持ってもいいですが、アプリケーションからの利用のしやすさを考えるとテーブルに格納したほうがよいかもしれません。