0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【連載 第6回】ServiceNow標準機能だけで作る「Pull型」自動化連携 ― Scripted REST APIで結果を登録する

0
Posted at

本記事は連載「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に転記して、依頼者に進捗が見えるようにします。

連載記事


※ 本記事の内容は個人の見解であり、所属する組織の公式な見解ではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?