| name | approval-workflows |
| description | Configure ServiceNow approval rules and sysapproval_approver records — manager/group/script approvers, multi-level routing, delegation via sys_user_delegate, and parent-record state rollup. |
| license | Apache-2.0 |
| compatibility | Designed for Snow-Code and ServiceNow development |
| metadata | {"author":"serac","version":"1.0.0","category":"servicenow"} |
| tools | ["snow_approval_chain_manage","snow_query_table","snow_artifact_manage","snow_execute_script"] |
Approval Workflows for ServiceNow
Approval workflows route records through configurable approval chains.
Approval Architecture
Record (change_request, sc_req_item, etc.)
↓
Approval Rules (sysapproval_rule)
↓
Approval Records (sysapproval_approver)
↓ Approve/Reject
Record State Updated
Key Tables
| Table | Purpose |
|---|
sysapproval_approver | Individual approval records |
sysapproval_group | Group approval configuration |
sysapproval_rule | Approval rules |
sys_approval_workflow | Approval workflow stages |
Approval Rules (ES5)
Create Approval Rule
var rule = new GlideRecord("sysapproval_rule")
rule.initialize()
rule.setValue("name", "Change Request Manager Approval")
rule.setValue("table", "change_request")
rule.setValue("order", 100)
rule.setValue("active", true)
rule.setValue("conditions", "type=normal^priority<=2")
rule.setValue("approver", "manager")
rule.setValue("approval_type", "and")
rule.setValue("wait_for", true)
rule.insert()
Script-Based Approver Selection
var rule = new GlideRecord("sysapproval_rule")
rule.initialize()
rule.setValue("name", "Cost-Based Approval")
rule.setValue("table", "sc_req_item")
rule.setValue("approver", "script")
rule.setValue(
"script",
"(function getApprovers(current) {\n" +
" var approvers = [];\n" +
' var cost = parseFloat(current.getValue("estimated_cost")) || 0;\n' +
" \n" +
" // Manager approval for all\n" +
" var caller = current.requested_for.getRefRecord();\n" +
" if (caller.manager) {\n" +
" approvers.push(caller.manager.toString());\n" +
" }\n" +
" \n" +
" // Director approval for > $5000\n" +
" if (cost > 5000) {\n" +
" var director = getDirector(caller);\n" +
" if (director) approvers.push(director);\n" +
" }\n" +
" \n" +
" // VP approval for > $25000\n" +
" if (cost > 25000) {\n" +
" var vp = getVP(caller);\n" +
" if (vp) approvers.push(vp);\n" +
" }\n" +
" \n" +
" return approvers;\n" +
"})(current);",
)
rule.insert()
Managing Approvals (ES5)
Create Approval Manually
function createApproval(recordSysId, approverSysId, source) {
var approval = new GlideRecord("sysapproval_approver")
approval.initialize()
approval.setValue("sysapproval", recordSysId)
approval.setValue("approver", approverSysId)
approval.setValue("state", "requested")
approval.setValue("source_table", source.table || "")
return approval.insert()
}
Process Approval Decision
function processApprovalDecision(approvalSysId, decision, comments) {
var approval = new GlideRecord("sysapproval_approver")
if (!approval.get(approvalSysId)) {
return { success: false, message: "Approval not found" }
}
if (approval.getValue("state") !== "requested") {
return { success: false, message: "Approval already processed" }
}
if (approval.getValue("approver") !== gs.getUserID()) {
if (!canActOnBehalf(approval.getValue("approver"))) {
return { success: false, message: "Not authorized to approve" }
}
}
approval.setValue("state", decision)
approval.setValue("comments", comments)
approval.setValue("actual_approver", gs.getUserID())
approval.update()
updateParentApprovalStatus(approval.getValue("sysapproval"))
return {
success: true,
decision: decision,
record: approval.sysapproval.getDisplayValue(),
}
}
function canActOnBehalf(originalApproverId) {
var delegation = new GlideRecord("sys_user_delegate")
delegation.addQuery("user", originalApproverId)
delegation.addQuery("delegate", gs.getUserID())
delegation.addQuery("starts", "<=", new GlideDateTime())
delegation.addQuery("ends", ">=", new GlideDateTime())
delegation.addQuery("approvals", true)
delegation.query()
return delegation.hasNext()
}
Update Parent Record
function updateParentApprovalStatus(recordSysId) {
var approvals = new GlideRecord("sysapproval_approver")
approvals.addQuery("sysapproval", recordSysId)
approvals.query()
var requested = 0
var approved = 0
var rejected = 0
while (approvals.next()) {
var state = approvals.getValue("state")
if (state === "requested") requested++
else if (state === "approved") approved++
else if (state === "rejected") rejected++
}
var overallStatus = "not requested"
if (rejected > 0) {
overallStatus = "rejected"
} else if (requested > 0) {
overallStatus = "requested"
} else if (approved > 0) {
overallStatus = "approved"
}
var parent = new GlideRecord("change_request")
if (parent.get(recordSysId)) {
parent.setValue("approval", overallStatus)
parent.update()
}
}
Group Approvals (ES5)
Configure Group Approval
var groupApproval = new GlideRecord("sysapproval_group")
groupApproval.initialize()
groupApproval.setValue("parent", recordSysId)
groupApproval.setValue("group", groupSysId)
groupApproval.setValue("approval", "any")
groupApproval.setValue("specific_count", 2)
groupApproval.insert()
Group Approval with Minimum
function checkGroupApprovalThreshold(groupApprovalSysId) {
var groupConfig = new GlideRecord("sysapproval_group")
if (!groupConfig.get(groupApprovalSysId)) {
return false
}
var approvalType = groupConfig.getValue("approval")
var groupId = groupConfig.getValue("group")
var parentId = groupConfig.getValue("parent")
var ga = new GlideAggregate("sysapproval_approver")
ga.addQuery("sysapproval", parentId)
ga.addQuery("approver.sys_id", "IN", getGroupMembers(groupId))
ga.addQuery("state", "approved")
ga.addAggregate("COUNT")
ga.query()
var approvedCount = 0
if (ga.next()) {
approvedCount = parseInt(ga.getAggregate("COUNT"), 10)
}
if (approvalType === "any") {
return approvedCount >= 1
} else if (approvalType === "all") {
var memberCount = getGroupMemberCount(groupId)
return approvedCount >= memberCount
} else if (approvalType === "specific_count") {
var required = parseInt(groupConfig.getValue("specific_count"), 10)
return approvedCount >= required
}
return false
}
Approval Delegation (ES5)
Create Delegation
function createDelegation(userId, delegateId, startDate, endDate) {
var delegation = new GlideRecord("sys_user_delegate")
delegation.initialize()
delegation.setValue("user", userId)
delegation.setValue("delegate", delegateId)
delegation.setValue("starts", startDate)
delegation.setValue("ends", endDate)
delegation.setValue("approvals", true)
delegation.setValue("assignments", false)
return delegation.insert()
}
Find Active Delegates
function getActiveDelegates(userId) {
var delegates = []
var now = new GlideDateTime()
var delegation = new GlideRecord("sys_user_delegate")
delegation.addQuery("user", userId)
delegation.addQuery("approvals", true)
delegation.addQuery("starts", "<=", now)
delegation.addQuery("ends", ">=", now)
delegation.query()
while (delegation.next()) {
delegates.push({
delegate: delegation.delegate.getDisplayValue(),
delegate_id: delegation.getValue("delegate"),
ends: delegation.getValue("ends"),
})
}
return delegates
}
Approval Notifications (ES5)
Send Approval Request
function sendApprovalNotification(approvalSysId) {
var approval = new GlideRecord("sysapproval_approver")
if (!approval.get(approvalSysId)) return
var parent = new GlideRecord(approval.source_table)
if (parent.get(approval.getValue("sysapproval"))) {
gs.eventQueue("approval.request", approval, approval.getValue("approver"), "")
}
}
MCP Tool Integration
Available Tools
| Tool | Purpose |
|---|
snow_query_table | Query approvals and approval rules |
snow_approval_chain_manage | Define multi-step approval chains (define_chain) |
snow_artifact_manage | Find approval business rules (action='find') |
snow_execute_script | Test approval scripts |
snow_create_business_rule | Create approval triggers |
Example Workflow
await snow_query_table({
table: "sysapproval_approver",
query: "state=requested^approver=javascript:gs.getUserID()",
fields: "sysapproval,state,sys_created_on",
})
await snow_query_table({
table: "sysapproval_rule",
query: "table=change_request^active=true",
fields: "name,conditions,approver,approval_type",
})
await snow_execute_script({
script: `
var delegates = getActiveDelegates(gs.getUserID());
gs.info('Active delegates: ' + JSON.stringify(delegates));
`,
})
Best Practices
- Clear Conditions - Specific rule conditions
- Logical Order - Rule ordering matters
- Escalation - Handle non-response
- Delegation - Support out-of-office
- Notifications - Timely reminders
- Audit Trail - Track all decisions
- Testing - Test all approval paths
- ES5 Only - No modern JavaScript syntax