Example 02 / Catalog Approval
A request in.
An approved change out.
Turn a service catalog request into an approved change: collect the details, ask the right manager, and update the record only after approval.
Download the manifest ↓Request
Choose an existing record and enter the proposed changes in a catalog form.
Approve
Route the request to the manager of the person it is requested for, including requests placed on someone else’s behalf.
Fulfill
Apply the change after approval. A rejected request leaves the record unchanged.
Eighteen steps. One connected catalog workflow.
A catalog item, a manager’s decision and a flow that carries out the approved work. The working example changes an existing cost center’s name and code; the same request → approval → fulfillment pattern applies to other business-record changes.
| Part | Purpose |
|---|---|
| Catalog form | Select a record and collect the proposed values. |
| Form rules | Show required fields when relevant and check inputs before submission. |
| Approval flow | Ask the requested-for user’s manager and branch on the decision. |
| Fulfillment | Update the selected record only in the approved branch. |
| Test data | Create a sample record, manager and requestor for repeatable checks. |
Inspect the complete JSON manifest
{
"schemaVersion": 1,
"scope": {
"prefix": "global",
"name": "Global",
"create": false
},
"updateSet": "Cost Center Maintenance Example",
"steps": [
{
"id": "catalog_item",
"skill": "catalog",
"op": "create_item",
"inputs": {
"active": true,
"description": "Select an existing cost center and enter its proposed name and code. Your manager must approve before the cost center is updated.",
"name": "Cost Center Maintenance",
"shortDescription": "Request a cost center name or code change",
"sysScope": "$scope"
}
},
{
"id": "cost_center",
"skill": "catalog",
"op": "add_variable",
"inputs": {
"catalogItem": "$catalog_item",
"name": "cost_center",
"label": "Cost center",
"type": "Reference",
"order": 100,
"mandatory": true,
"sysScope": "$scope",
"reference": "cmn_cost_center",
"attributes": "ref_auto_completer=AJAXTableCompleter,ref_ac_columns=code,ref_ac_columns_search=true"
}
},
{
"id": "new_name",
"skill": "catalog",
"op": "add_variable",
"inputs": {
"catalogItem": "$catalog_item",
"name": "new_name",
"label": "New name",
"type": "String",
"order": 200,
"mandatory": true,
"sysScope": "$scope"
}
},
{
"id": "new_code",
"skill": "catalog",
"op": "add_variable",
"inputs": {
"catalogItem": "$catalog_item",
"name": "new_code",
"label": "New code",
"type": "String",
"order": 300,
"mandatory": true,
"sysScope": "$scope"
}
},
{
"id": "details_policy",
"skill": "catalog",
"op": "add_ui_policy",
"inputs": {
"catalogItem": "$catalog_item",
"shortDescription": "Show proposed values after selecting a cost center",
"conditions": "cost_centerISNOTEMPTY",
"actions": [
{
"variable": "new_name",
"visible": true,
"mandatory": true
},
{
"variable": "new_code",
"visible": true,
"mandatory": true
}
],
"sysScope": "$scope"
}
},
{
"id": "validate_values",
"skill": "catalog",
"op": "add_client_script",
"inputs": {
"catalogItem": "$catalog_item",
"name": "Cost Center Maintenance validate values",
"type": "onSubmit",
"uiType": 10,
"sysScope": "$scope",
"script": "function onSubmit() {\n var name = g_form.getValue('new_name').trim();\n var code = g_form.getValue('new_code').trim();\n if (!g_form.getValue('cost_center') || !name || !code || name.length > 100 || code.length > 40) {\n g_form.addErrorMessage('Select a cost center and enter a name (1-100 characters) and code (1-40 characters).');\n return false;\n }\n g_form.setValue('new_name', name);\n g_form.setValue('new_code', code);\n return true;\n}"
}
},
{
"id": "flow_create",
"skill": "flow",
"op": "create_flow",
"inputs": {
"internalName": "cost_center_maintenance",
"name": "Cost Center Maintenance",
"sysScope": "$scope",
"triggerType": "service_catalog",
"triggerValues": {
"run_flow_in": "background"
}
}
},
{
"id": "flow_add_trigger",
"skill": "flow",
"op": "add_trigger",
"inputs": {
"flow": "$flow_create",
"run_flow_in": "background",
"triggerDefinitionId": "c43a1011c36813002841b63b12d3ae15"
}
},
{
"id": "flow_get_vars",
"skill": "flow",
"op": "add_action",
"inputs": {
"actionType": "330ba3abc31013002841b63b12d3aee8",
"flow": "$flow_create",
"order": 1,
"values": {
"requested_item": "{{Service Catalog_1.request_item}}",
"template_catalog_item": "$catalog_item"
}
}
},
{
"id": "flow_ask_approval",
"skill": "flow",
"op": "add_action",
"inputs": {
"actionType": "f8f2e9920b10030085c083eb37673abd",
"flow": "$flow_create",
"order": 2,
"values": {
"approval_conditions": "ApprovesRejectsAnyU[{{Service Catalog_1.request_item.requested_for.manager}}]",
"approval_field": "approval",
"approval_reason": "Approve cost center maintenance",
"journal_field": "approval_history",
"record": "{{Service Catalog_1.request_item}}",
"table": [
"sc_req_item",
"Requested Item"
]
}
}
},
{
"id": "flow_approval_branch",
"skill": "flow",
"op": "add_logic",
"inputs": {
"condition": "{{approval-action-2.approval_state}}=approved",
"condition_name": "Approval granted",
"definitionId": "af4e1945c3e232002841b63b12d3ae3e",
"flow": "$flow_create",
"order": 3
}
},
{
"id": "flow_update_record",
"skill": "flow",
"op": "add_action",
"inputs": {
"actionType": "f9d01dd2c31332002841b63b12d3aea1",
"flow": "$flow_create",
"order": 4,
"uiId": "update-record-action",
"values": {
"record": "{{get-cat-vars-action-1.cost_center}}",
"table_name": [
"cmn_cost_center",
"Cost Center"
],
"values": "name={{get-cat-vars-action-1.new_name}}^code={{get-cat-vars-action-1.new_code}}"
},
"parentOrder": 3
}
},
{
"id": "flow_rebuild_label_cache",
"skill": "flow",
"op": "rebuild_label_cache",
"inputs": {
"flow": "$flow_create"
}
},
{
"id": "flow_publish",
"skill": "flow",
"op": "publish",
"inputs": {
"flow": "$flow_create"
}
},
{
"id": "catalog_bind_fulfillment_flow",
"skill": "catalog",
"op": "bind_item_flow",
"inputs": {
"catalogItem": "$catalog_item",
"flow": "$flow_publish",
"sysScope": "$scope"
}
},
{
"id": "fixture_cost_center",
"skill": "record",
"op": "upsert",
"inputs": {
"table": "cmn_cost_center",
"identity": "name=SN Example Lifecycle Cost Center",
"data": {
"name": "SN Example Lifecycle Cost Center",
"code": "SN-CC-LIFECYCLE-FIXTURE"
}
}
},
{
"id": "fixture_manager",
"skill": "record",
"op": "upsert",
"inputs": {
"table": "sys_user",
"identity": "user_name=sn.cc.lifecycle.manager",
"data": {
"user_name": "sn.cc.lifecycle.manager",
"first_name": "SN Example",
"last_name": "Lifecycle Manager",
"active": true,
"email": ""
}
}
},
{
"id": "fixture_requestor",
"skill": "record",
"op": "upsert",
"inputs": {
"table": "sys_user",
"identity": "user_name=sn.cc.lifecycle.requestor",
"data": {
"user_name": "sn.cc.lifecycle.requestor",
"first_name": "SN Example",
"last_name": "Lifecycle Requestor",
"active": true,
"email": "",
"manager": "$fixture_manager"
}
}
}
],
"test": {
"structural": true,
"catalogLifecycle": [
{
"item": "$catalog_item",
"variables": {
"cost_center": "$fixture_cost_center",
"new_name": "SN Example Lifecycle Approved",
"new_code": "SN-CC-LIFECYCLE-APPROVED"
},
"verifyVariables": true,
"requestedFor": "$fixture_requestor",
"approval": {
"approver": "$fixture_manager",
"decision": "approved"
},
"flow": "$flow_create",
"timeoutSeconds": 90,
"recordAssertions": [
{
"table": "cmn_cost_center",
"record": "$fixture_cost_center",
"expected": {
"name": "SN Example Lifecycle Approved",
"code": "SN-CC-LIFECYCLE-APPROVED"
}
}
]
},
{
"item": "$catalog_item",
"variables": {
"cost_center": "$fixture_cost_center",
"new_name": "SN Example Must Not Apply",
"new_code": "SN-CC-MUST-NOT-APPLY"
},
"verifyVariables": true,
"requestedFor": "$fixture_requestor",
"approval": {
"approver": "$fixture_manager",
"decision": "rejected"
},
"flow": "$flow_create",
"timeoutSeconds": 90,
"recordAssertions": [
{
"table": "cmn_cost_center",
"record": "$fixture_cost_center",
"expected": {
"name": "SN Example Lifecycle Approved",
"code": "SN-CC-LIFECYCLE-APPROVED"
}
}
]
},
{
"item": "$catalog_item",
"variables": {
"cost_center": "$fixture_cost_center",
"new_name": "SN Example Lifecycle Cost Center",
"new_code": "SN-CC-LIFECYCLE-FIXTURE"
},
"verifyVariables": true,
"requestedFor": "$fixture_requestor",
"approval": {
"approver": "$fixture_manager",
"decision": "approved"
},
"flow": "$flow_create",
"timeoutSeconds": 90,
"recordAssertions": [
{
"table": "cmn_cost_center",
"record": "$fixture_cost_center",
"expected": {
"name": "SN Example Lifecycle Cost Center",
"code": "SN-CC-LIFECYCLE-FIXTURE"
}
}
]
}
]
}
}The download is the verified Cost Center Maintenance source example. Its lifecycle tests use catalog approval-testing fields that older sn releases do not support. Run sn --version, and sn upgrade to the current release before running them.
1. Create and inspect
Download the manifest into a durable project folder and use an authorized development target named dev. It deploys into Global with a dedicated update set and provisions three clearly named test records. Keep the manifest and its local results together.
sn preflight manifest.sn.json --target dev --instance-checks
sn plan manifest.sn.json --target dev
sn execute manifest.sn.json --target dev --yes
sn validate manifest.sn.json --target devThe sample cost center and two users are instance data. Execute the manifest to provision them on another development instance; moving the metadata update set does not transport them. Set up your instance first if needed.
2. Request. Approve. Read back.
The built-in tests submit a request on behalf of the sample requestor, verify the manager approval, approve it through the API and wait for the fulfillment flow. They then read the selected record to confirm both proposed fields were saved.
sn test manifest.sn.json --target dev --yes --json3. Reject a change. Restore the original.
The same test run rejects a second proposal and checks that the approved values remain unchanged. A final approved request restores the original values. Each run creates three requests and checks the stored outcomes, including flow completion.
If a case fails, restoration is not guaranteed. Inspect the reported request, approval and record results before retrying.
4. Rerun and check for drift
sn execute manifest.sn.json --target dev --yes --resume
sn validate manifest.sn.json --target dev
sn drift manifest.sn.json --target devAn unchanged resume reuses the recorded deployment. Validation and drift checks confirm the expected configuration and fixture values. When adapting the workflow, review the target record, approver and fields together; use sn patch for changes to shipped configuration.
Observed
Working software. Inspectable results.
Live checks inspected approval routing, completed flows and persisted records on one development instance.
3/3
lifecycle cases passed
Approve the change, reject a different proposal, then restore the original values.
18/18
deployment steps resumed
The unchanged deployment reused all recorded results without writes.
0
drift changes or errors
Final checks found the expected configuration and restored fixture values.
Tests used the configured API account, without manager impersonation. The flow runs as system; form checks are client-side. Catalog tasks and request-closing automation are outside this example.