本記事は連載「ServiceNow標準機能だけで作るPull型自動化連携」の第6回です。
第5回では、Scripted REST APIでSCTASKの詳細を取得し、autoengine1 → autoengine1running の条件付き更新で実行開始を宣言しました。
今回は、同じScripted REST Serviceに結果登録APIを追加します。自動化エンジンが返した結果をServiceNowに記録し、成功なら次の作業へ進め、失敗なら人の担当グループへ引き継ぎます。
この回で作るもの
| status | State | Assignment group | Assigned to | その後 |
|---|---|---|---|---|
SUCCESS |
Closed Complete | 変更しない | 変更しない | Execution Planが次の作業へ進む |
FAILED |
In Progress |
failure_assignment_group の値 |
空欄 | 人の担当グループのキューに入る |
1. このAPIの設計方針
| 方針 | 内容 |
|---|---|
| POSTで登録する | 結果は1回ごとの「登録」として扱う |
| SCTASK番号で指定する | 外部からsys_idを受け取らない |
| statusは2値だけ |
SUCCESS または FAILED 以外は400 |
jobidは必須 |
成功・失敗どちらも、外部ジョブを追跡できるようにする |
| 担当者の条件付き確認はしない |
expected_current_assigned_to を要求しない |
| 結果は履歴として追記する | RITMの execution_result_json に、SCTASKごとの結果を配列で積み上げる |
| 失敗時の引継ぎ先は依頼ごとに決める | RITMの failure_assignment_group(Reference型)を使う |
| 不正な状態では更新しない | 引継ぎ先を特定できない、既存の履歴が壊れている、といった場合はエラーを返し、何も更新しない |
1.1 なぜ結果登録では条件付き確認をしないのか
第5回の実行開始では、二重実行を防ぐために条件付き更新を使いました。一方、結果登録では使いません。
作業が終わったのに、担当者の状態が想定と違うという理由で結果を受け付けないと、実行した事実がServiceNowに残らなくなるからです。結果登録では、記録を残すことを優先しています。
その代わり、同じ結果を2回送ると、履歴とWork Notesも2回追記されます。呼び出し側は、不要な再送をしないようにしてください。
1.2 なぜ結果を「履歴」として持つのか
execution_result_json は、1つのSCTASKの結果ではなく、同じRITMで実行されたSCTASKの結果を、実行した順に並べた配列です。
第2回で紹介したように、Execution Planでは作業1 → 作業2 → 作業3と順番に進みます。結果を履歴として残しておけば、作業3の自動化処理が、作業1の結果を第5回の詳細取得APIで読み取れます。
2. Request Body
2.1 成功時
{
"sctask": "SCTASK0010006",
"result": {
"status": "SUCCESS",
"jobid": 1234,
"message": "Linux log collected at /LogRetrieve/SCTASK0010006/server01/Apps/XXX/logs/app.log.",
"data": {
"target_host": "server01",
"files": [
"/Apps/XXX/logs/app.log"
]
}
}
}
2.2 失敗時
{
"sctask": "SCTASK0010006",
"result": {
"status": "FAILED",
"jobid": 1234,
"message": "Unable to reach target server.",
"data": {
"error_details": "detailed SSH log file"
}
}
}
2.3 入力項目
| 項目 | 必須 | 内容 |
|---|---|---|
sctask |
必須 | SCTASK番号 |
result |
必須 | 実行結果のオブジェクト |
result.status |
必須 |
SUCCESS または FAILED
|
result.jobid |
必須 | 外部の自動化ジョブを識別するID。文字列でも数値でもよい |
result.message |
必須 | 結果のメッセージ |
result.data |
必須 | 機械処理用の任意のJSONオブジェクト(配列は不可) |
result.data には、パスワード、トークン、秘密鍵などの機密情報や、長大な標準出力を入れないでください。大きなログは外部に保管し、data にはその保管先だけを入れます。
3. 結果の記録先
結果は、次の3か所に記録されます。
| 記録先 | 形式 | 主な読み手 |
|---|---|---|
RITMの execution_result_json
|
改行なしのJSON配列(履歴) | 後続の自動化処理 |
| SCTASKの Work Notes | 見出し付きテキスト + 整形したJSON | 運用者 |
RITMの execution_result_display
|
Work Notesと同じテキストを追記 | 依頼者 |
3.1 execution_result_json
各SCTASKの結果を、次の形で配列の末尾に追加します。task_name には、SCTASKのShort description(処理キー)が入ります。
{
"sctask": "SCTASK0010006",
"task_name": "REQTYPE_LINUXLOG_ACTION1",
"status": "SUCCESS",
"jobid": 1234,
"message": "Linux log collected.",
"data": { "target_host": "server01" }
}
2件目を追記すると、次のようになります。
[{"sctask":"SCTASK0010006","task_name":"REQTYPE_LINUXLOG_ACTION1","status":"SUCCESS","jobid":1234,"message":"Linux log collected.","data":{"target_host":"server01"}},{"sctask":"SCTASK0010007","task_name":"REQTYPE_LINUXLOG_ACTION2","status":"SUCCESS","jobid":1235,"message":"Linux log archived.","data":{"archive_file":"/archive/app-log.zip"}}]
既存の値の扱いは次のとおりです。
| 既存の値 | 動作 |
|---|---|
未設定、空文字、[]
|
空の履歴として扱い、1件目を追加する |
| JSON配列 | 末尾に追加する |
| JSONとして不正 | 409 INVALID_EXISTING_RESULT_JSON。上書きしない |
JSONだが配列ではない(例: {}) |
409 INVALID_EXISTING_RESULT_FORMAT。上書きしない |
既存の履歴が壊れている場合に上書きしてしまうと、それまでの結果が失われます。そのため、ここではエラーにして人の確認を待ちます。
3.2 Work Notes
運用者が読みやすいように、項目ごとに見出しを付け、Result Data だけをインデント付きのJSONにします。
=== Automation Execution Result ===
Message: Linux log collected at /LogRetrieve/SCTASK0010006/server01/Apps/XXX/logs/app.log.
SCTASK: SCTASK0010006
Task: REQTYPE_LINUXLOG_ACTION1
Status: SUCCESS
Job ID: 1234
Result Data:
{
"target_host": "server01",
"files": [
"/Apps/XXX/logs/app.log"
]
}
4. Resourceを作成する
第5回で作った Automation Integration API(API ID: automation_integration)を開き、Resources関連リストで New を選択します。
| フィールド | 設定値 |
|---|---|
| Name | Update Task Result |
| HTTP method | POST |
| Relative path | /task_result |
| Requires authentication | true |
| Active | true |
POST /api/<namespace>/automation_integration/v1/task_result
バージョン付きのパスにするかどうかは、第5回と同じくインスタンスの設定に合わせてください。
4.1 APIサービスユーザーに必要な権限
第3回で作成した svc_autoengine1_api に、少なくとも次の権限が必要です。
-
sc_taskの参照 -
sc_taskのstate、assignment_group、assigned_to、work_notesの更新 -
sc_req_itemとCatalog Variableの参照・更新 - Scripted REST API Resourceの実行
開発中に広いロールを一時的に付けた場合も、本番化の前に最小権限に絞ってください。
5. Resource Script
Script欄に、次のコードを貼り付けます。
-
CLOSED_COMPLETE_STATEとIN_PROGRESS_STATEは、対象インスタンスのsc_task.stateの内部値に合わせてください。サンプルでは Closed Complete=3、In Progress=2 です。 - 貼り付けるときは、
&&や<がHTMLエスケープされていないことを確認してください。
(function process(request, response) {
'use strict';
var CLOSED_COMPLETE_STATE = '3';
var IN_PROGRESS_STATE = '2';
var body = request.body && request.body.data
? request.body.data
: {};
var sctaskNumber = normalize(body.sctask);
var result = body.result;
if (!sctaskNumber) {
return fail(response, 400, 'INVALID_REQUEST',
'sctask is required.');
}
if (!/^SCTASK[0-9]+$/i.test(sctaskNumber)) {
return fail(response, 400, 'INVALID_SCTASK',
'sctask must be an SCTASK number.');
}
if (!result || typeof result !== 'object' || isArray(result)) {
return fail(response, 400, 'INVALID_RESULT',
'result must be a JSON object.');
}
var status = normalize(result.status).toUpperCase();
var message = normalize(result.message);
var jobId = result.jobid;
if (status !== 'SUCCESS' && status !== 'FAILED') {
return fail(response, 400, 'INVALID_STATUS',
'result.status must be SUCCESS or FAILED.');
}
if (jobId === null || jobId === undefined ||
normalize(jobId) === '') {
return fail(response, 400, 'INVALID_JOBID',
'result.jobid is required.');
}
if (!message) {
return fail(response, 400, 'INVALID_MESSAGE',
'result.message is required.');
}
if (!result.hasOwnProperty('data') ||
result.data === null ||
typeof result.data !== 'object' ||
isArray(result.data)) {
return fail(response, 400, 'INVALID_DATA',
'result.data must be a JSON object.');
}
var task = new GlideRecordSecure('sc_task');
task.addQuery('number', sctaskNumber);
task.setLimit(2);
task.query();
if (!task.next()) {
return fail(response, 404, 'SCTASK_NOT_FOUND',
'The specified SCTASK was not found.');
}
if (task.hasNext()) {
return fail(response, 409, 'SCTASK_NOT_UNIQUE',
'The specified SCTASK could not be resolved uniquely.');
}
if (!task.canRead()) {
return fail(response, 403, 'READ_FORBIDDEN',
'The API user cannot read the specified SCTASK.');
}
if (task.request_item.nil()) {
return fail(response, 409, 'RITM_NOT_LINKED',
'The specified SCTASK is not linked to an RITM.');
}
var ritm = task.request_item.getRefRecord();
if (!ritm || !ritm.isValidRecord()) {
return fail(response, 404, 'RITM_NOT_FOUND',
'The RITM linked to the SCTASK was not found.');
}
var failureGroupId = '';
var failureGroupName = '';
if (status === 'FAILED') {
var failureGroup = resolveFailureGroup(ritm);
if (!failureGroup.ok) {
return fail(response, failureGroup.httpStatus,
failureGroup.code, failureGroup.message);
}
failureGroupId = failureGroup.id;
failureGroupName = failureGroup.name;
}
if (!task.work_notes.canWrite() ||
!task.state.canWrite()) {
return fail(response, 403, 'TASK_UPDATE_FORBIDDEN',
'The API user cannot update work_notes or state.');
}
if (status === 'FAILED' &&
(!task.assignment_group.canWrite() ||
!task.assigned_to.canWrite())) {
return fail(response, 403, 'TASK_ROUTING_FORBIDDEN',
'The API user cannot update assignment_group or assigned_to.');
}
var historyResult = readExecutionHistory(ritm);
if (!historyResult.ok) {
return fail(response, 409,
historyResult.code,
historyResult.message);
}
var history = historyResult.history;
var taskName = task.getValue('short_description') || '';
history.push({
sctask: sctaskNumber,
task_name: taskName,
status: status,
jobid: jobId,
message: message,
data: result.data
});
var executionHistoryJson;
try {
executionHistoryJson = JSON.stringify(history);
} catch (e) {
return fail(response, 400,
'RESULT_SERIALIZATION_FAILED',
'The execution result history could not be serialized.');
}
if (!saveExecutionHistory(ritm, executionHistoryJson)) {
return fail(response, 500, 'RESULT_SAVE_FAILED',
'execution_result_json could not be updated.');
}
var newWorkNotes;
newWorkNotes = buildWorkNotes(
sctaskNumber,
taskName,
status,
jobId,
message,
result.data
);
task.work_notes = newWorkNotes;
var executionHistoryDisplay;
executionHistoryDisplay = appendExecutionHistoryDisplay(ritm, newWorkNotes);
if (!saveExecutionHistoryDisplay(ritm, executionHistoryDisplay)) {
return fail(response, 500, 'RESULT_SAVE_FAILED',
'execution_result_display could not be updated.');
}
if (status === 'SUCCESS') {
task.setValue('state', CLOSED_COMPLETE_STATE);
} else {
task.setValue('state', IN_PROGRESS_STATE);
task.setValue('assignment_group', failureGroupId);
task.setValue('assigned_to', '');
}
if (!task.update()) {
return fail(response, 500, 'TASK_UPDATE_FAILED',
'The SCTASK could not be updated.');
}
response.setStatus(200);
response.setBody({
updated: true,
sctask: sctaskNumber,
status: status,
jobid: jobId,
state: task.getDisplayValue('state') || '',
assignment_group: status === 'FAILED'
? failureGroupName
: getReferenceName(task, 'assignment_group')
});
function appendExecutionHistoryDisplay(ritmRecord, newValue) {
var rawValue = '';
try {
rawValue = normalize(
ritmRecord.variables.execution_result_display
);
} catch (e) {
rawValue = '';
}
return rawValue + "\n" + newValue;
}
function readExecutionHistory(ritmRecord) {
var rawValue = '';
try {
rawValue = normalize(
ritmRecord.variables.execution_result_json
);
} catch (e) {
rawValue = '';
}
if (!rawValue) {
return {
ok: true,
history: []
};
}
var parsed;
try {
parsed = JSON.parse(rawValue);
} catch (e) {
return {
ok: false,
code: 'INVALID_EXISTING_RESULT_JSON',
message: 'The existing execution_result_json is not valid JSON.'
};
}
if (!isArray(parsed)) {
return {
ok: false,
code: 'INVALID_EXISTING_RESULT_FORMAT',
message: 'The existing execution_result_json must be a JSON array.'
};
}
return {
ok: true,
history: parsed
};
}
function saveExecutionHistory(ritmRecord, jsonValue) {
try {
ritmRecord.variables.execution_result_json = jsonValue;
return !!ritmRecord.update();
} catch (e) {
gs.error('Result Upload API: failed to update ' +
'execution_result_json for ' +
ritmRecord.getValue('number') + ': ' + e.message);
return false;
}
}
function saveExecutionHistoryDisplay(ritmRecord, displayValue) {
try {
ritmRecord.variables.execution_result_display = displayValue;
return !!ritmRecord.update();
} catch (e) {
gs.error('Result Upload API: failed to update ' +
'execution_result_display for ' +
ritmRecord.getValue('number') + ': ' + e.message);
return false;
}
}
function resolveFailureGroup(ritmRecord) {
var variable = ritmRecord.variables.failure_assignment_group;
var groupId = variable ? normalize(variable.toString()) : '';
if (!groupId) {
return {
ok: false,
httpStatus: 409,
code: 'FAILURE_ASSIGNMENT_GROUP_NOT_SET',
message: 'failure_assignment_group is not set on the RITM.'
};
}
var group = new GlideRecordSecure('sys_user_group');
if (!group.get(groupId) || !group.isValidRecord()) {
return {
ok: false,
httpStatus: 409,
code: 'FAILURE_ASSIGNMENT_GROUP_NOT_FOUND',
message: 'failure_assignment_group could not be resolved.'
};
}
if (group.isValidField('active') &&
group.getValue('active') !== 'true') {
return {
ok: false,
httpStatus: 409,
code: 'FAILURE_ASSIGNMENT_GROUP_INACTIVE',
message: 'failure_assignment_group is inactive.'
};
}
return {
ok: true,
id: group.getUniqueValue(),
name: group.getValue('name') || ''
};
}
function buildWorkNotes(sctask, taskName,
status, jobId, message, data) {
return [
'=== Automation Execution Result ===',
'Message: ' + message,
'SCTASK: ' + sctask,
'Task: ' + taskName,
'Status: ' + status,
'Job ID: ' + jobId,
'',
'Result Data:',
JSON.stringify(data, null, 2)
].join('\n');
}
function getReferenceName(record, fieldName) {
if (!record || !record.isValidRecord() ||
record.getElement(fieldName).nil()) {
return '';
}
var referenced = record.getElement(fieldName).getRefRecord();
return referenced && referenced.isValidRecord()
? referenced.getValue('name') || ''
: '';
}
function normalize(value) {
return value === null || value === undefined
? ''
: value.toString().trim();
}
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}
function fail(res, httpStatus, code, message) {
res.setStatus(httpStatus);
res.setBody({
error: {
code: code,
message: message
}
});
return;
}
})(request, response);
5.1 コードのポイント
更新する前に、すべての検証を終える
処理は「入力チェック → SCTASKとRITMの特定 → 失敗時の引継ぎ先の確認 → 権限の確認 → 既存履歴の確認 → 書き込み」の順に進みます。検証のどこかで止まった場合、RITMの履歴、Work Notes、SCTASKのいずれも更新されません。
引継ぎ先は、Reference型変数の値から特定する
第3回で、failure_assignment_group をReference型にし、Default valueにグループのsys_idを設定しました。resolveFailureGroup() は、その値(sys_id)で sys_user_group を検索し、グループが存在してActiveであることを確かめます。
| 状況 | HTTP | code |
|---|---|---|
| 値が設定されていない | 409 | FAILURE_ASSIGNMENT_GROUP_NOT_SET |
| グループが見つからない | 409 | FAILURE_ASSIGNMENT_GROUP_NOT_FOUND |
| グループが非アクティブ | 409 | FAILURE_ASSIGNMENT_GROUP_INACTIVE |
sys_idはServiceNow内部でだけ使い、外部のリクエストにもレスポンスにも出しません。レスポンスには、グループの名前を返します。
失敗時はAssigned toを空欄にする
autoengine1running のままだと、人の担当グループのキューに入っても「誰かが実行中」に見えてしまいます。Assigned toを空欄にして、グループ内の誰かが拾える状態にします。
6. レスポンス
6.1 SUCCESS(200)
{
"result": {
"updated": true,
"sctask": "SCTASK0010006",
"status": "SUCCESS",
"jobid": 1234,
"state": "Closed Complete",
"assignment_group": "autoengine1group"
}
}
6.2 FAILED(200)
{
"result": {
"updated": true,
"sctask": "SCTASK0010007",
"status": "FAILED",
"jobid": 1235,
"state": "In Progress",
"assignment_group": "Application Development"
}
}
assignment_group には、引継ぎ先のグループ名が入ります。
6.3 エラー
| HTTP | code | 発生条件 |
|---|---|---|
| 400 | INVALID_REQUEST |
sctask が未指定 |
| 400 | INVALID_SCTASK |
sctask がSCTASK番号の形式でない |
| 400 | INVALID_RESULT |
result がJSONオブジェクトでない |
| 400 | INVALID_STATUS |
status が SUCCESS・FAILED 以外 |
| 400 | INVALID_JOBID |
jobid がない |
| 400 | INVALID_MESSAGE |
message がない |
| 400 | INVALID_DATA |
data がJSONオブジェクトでない |
| 400 | RESULT_SERIALIZATION_FAILED |
履歴をJSONに変換できない |
| 403 | READ_FORBIDDEN |
SCTASKを読めない |
| 403 | TASK_UPDATE_FORBIDDEN |
work_notes または state を更新できない |
| 403 | TASK_ROUTING_FORBIDDEN |
失敗時に assignment_group または assigned_to を更新できない |
| 404 | SCTASK_NOT_FOUND |
SCTASKが存在しない |
| 404 | RITM_NOT_FOUND |
紐づくRITMが見つからない |
| 409 | SCTASK_NOT_UNIQUE |
SCTASK番号から一意に特定できない |
| 409 | RITM_NOT_LINKED |
SCTASKがRITMに紐づいていない |
| 409 | FAILURE_ASSIGNMENT_GROUP_* |
失敗時の引継ぎ先を特定できない(5.1参照) |
| 409 |
INVALID_EXISTING_RESULT_JSON / INVALID_EXISTING_RESULT_FORMAT
|
既存の履歴が壊れている |
| 500 | RESULT_SAVE_FAILED |
RITM変数を保存できない |
| 500 | TASK_UPDATE_FAILED |
SCTASKを更新できない |
7. curlで試す
7.1 接続設定
export SN_INSTANCE="https://<instance>.service-now.com"
export SN_USER="svc_autoengine1_api"
export SN_PASSWORD='********'
export RESULT_API_PATH="/api/<namespace>/automation_integration/v1/task_result"
export SCTASK_NUMBER="SCTASK0010006"
7.2 SUCCESS
curl --silent --show-error --include \
--request POST \
--user "${SN_USER}:${SN_PASSWORD}" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "{
\"sctask\": \"${SCTASK_NUMBER}\",
\"result\": {
\"status\": \"SUCCESS\",
\"jobid\": 1234,
\"message\": \"Linux log collected.\",
\"data\": {
\"target_host\": \"server01\",
\"files\": [\"/Apps/XXX/logs/app.log\"]
}
}
}" \
"${SN_INSTANCE}${RESULT_API_PATH}"
確認すること:
- HTTP 200が返る
-
execution_result_jsonに1件目の履歴が入り、改行が含まれていない - Work Notesに、見出し付きの結果と整形された
Result Dataが追記される - SCTASKがClosed Completeになり、Execution Planの次のSCTASKが生成される
続けて、次のSCTASKの番号で同じようにSUCCESSを送ると、1件目を残したまま2件目が末尾に追加されることを確認できます。
7.3 FAILED
failure_assignment_group が設定されている、実行中のSCTASKを使います。
curl --silent --show-error --include \
--request POST \
--user "${SN_USER}:${SN_PASSWORD}" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "{
\"sctask\": \"${SCTASK_NUMBER}\",
\"result\": {
\"status\": \"FAILED\",
\"jobid\": 1236,
\"message\": \"Unable to reach target server.\",
\"data\": {
\"error_details\": \"detailed SSH log file\"
}
}
}" \
"${SN_INSTANCE}${RESULT_API_PATH}"
確認すること:
- HTTP 200が返る
- 既存の履歴を残したまま、FAILEDの結果が末尾に追加される
- StateがIn Progressのまま、Assignment groupが引継ぎ先に変わり、Assigned toが空欄になる
7.4 異常系
| テスト | 方法 | 期待結果 |
|---|---|---|
| 不正なstatus |
status に BUSINESS_FAILURE を指定 |
400 INVALID_STATUS
|
| jobidなし |
jobid を省略 |
400 INVALID_JOBID
|
| 引継ぎ先なし |
failure_assignment_group が未設定のRITMのSCTASKへFAILEDを送る |
409 FAILURE_ASSIGNMENT_GROUP_NOT_SET
|
| 履歴が配列でない |
execution_result_json を一時的に {} にして送る |
409 INVALID_EXISTING_RESULT_FORMAT
|
いずれの場合も、RITMの履歴、Work Notes、SCTASKは更新されないことを確認します。
8. 自動化エンジン側の組み込み
第5回の実行開始の後、作業を実行し、その結果を送ります。
upload_result() {
local task_number="$1"
local execution_result="$2"
local job_id payload
job_id="shell-$(date -u +%Y%m%dT%H%M%SZ)-${task_number}"
payload="$(jq -n \
--arg sctask "$task_number" \
--arg jobid "$job_id" \
--argjson execution "$execution_result" \
'{sctask:$sctask,result:{status:$execution.status,jobid:$jobid,message:$execution.message,data:$execution.data}}')"
api --header "Content-Type: application/json" \
--request POST \
--data "$payload" \
"${SN_INSTANCE}${RESULT_API_PATH}"
}
execution_result は、作業の実行結果として {status, message, data} を返すJSONです。処理キーに対応する処理がない場合は、FAILED として返し、人に引き継ぎます。
*)
jq -n --arg task_name "$task_name" \
'{status:"FAILED",message:("Unsupported task type: " + $task_name),data:{error_code:"UNSUPPORTED_TASK_TYPE",task_name:$task_name}}'
;;
9. 動作確認
- SUCCESSで、SCTASKがClosed Completeになる
- SUCCESSで、Assignment groupとAssigned toが変わらない
- SUCCESSの後、Execution Planの次のSCTASKが生成される
- FAILEDで、StateがIn Progressのまま、引継ぎ先のグループに移り、Assigned toが空欄になる
-
execution_result_jsonに、SCTASKごとの結果が実行順に追加される -
保存された
execution_result_jsonに改行が含まれていない -
Work Notesに、見出し付きの結果と整形された
Result Dataが表示される -
execution_result_displayに結果が追記される - 不正な入力や引継ぎ先の不備では、何も更新されない
-
レスポンスが
result.resultのような二重構造になっていない - リクエストとレスポンスにsys_idが含まれていない
10. 実機で確認しておきたいこと
-
sc_task.stateの Closed Complete と In Progress の内部値 - Closed Completeにしたとき、Execution Planが次の作業を始めること
- Reference型変数
failure_assignment_groupのtoString()が、グループのsys_idを返すこと - RITMの変数に、改行なしのJSON配列を保存できること
-
execution_result_jsonに保存できる最大長と、必要な保持期間
次回予告
次回は、Business Ruleを1本追加し、SCTASKの「確保」「実行開始」「正常完了」をRITMのActivityに転記して、依頼者に進捗が見えるようにします。
連載記事
- 第1回 なぜ今Pull型連携なのか
- 第2回 全体アーキテクチャと設計方針
- 第3回 Catalog Item・Variable Set・Execution Planの準備
- 第4回 Table APIで実行待ちタスクを取得・確保する
- 第5回 Scripted REST APIでタスク詳細取得と実行開始
- 第6回 Scripted REST APIで結果を登録する(本記事)
※ 本記事の内容は個人の見解であり、所属する組織の公式な見解ではありません。