0

Update change request

by
Published today

Partially update a change request, optionally moving it to a target state, through the Change Management API. Fails with ServiceNow's message when the transition is not allowed.

Script servicenow Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 6 hours ago
1
//native
2

3
function authHeader(auth: RT.Servicenow) {
4
  return auth.token
5
    ? `Bearer ${auth.token}`
6
    : `Basic ${btoa(`${auth.username}:${auth.password}`)}`
7
}
8

9
// Change Management API fields come back as { value, display_value }.
10
function value(field: any) {
11
  return field != null && typeof field === "object" ? field.value : field
12
}
13

14
function label(field: any) {
15
  return field != null && typeof field === "object"
16
    ? field.display_value
17
    : field
18
}
19

20
/**
21
 * Update Change Request
22
 * Partially update a change request by sys_id through the Change Management API (PATCH /api/sn_chg_rest/change/{sys_id}, documented in the Yokohama and Brazil API references; needs the itil, change_manager or admin role). Optionally move it to a target state: use a state value from Get change next states (e.g. -1 for Implement). Throws with ServiceNow's message when the update is rejected, and also when ServiceNow answers 200 but leaves the state unchanged (transition not allowed, or the field was ignored).
23
 */
24
export async function main(
25
  auth: RT.Servicenow,
26
  sys_id: string,
27
  state: string | undefined,
28
  work_notes: string | undefined,
29
  fields: { [key: string]: any } | undefined
30
) {
31
  const url = new URL(
32
    `${auth.instance_url}/api/sn_chg_rest/change/${encodeURIComponent(sys_id)}`
33
  )
34
  const headers = {
35
    Authorization: authHeader(auth),
36
    Accept: "application/json",
37
  }
38

39
  // Read the state first, so a 200 that silently leaves it unchanged is caught.
40
  let previousState: any
41
  let alreadyInState = false
42
  if (state) {
43
    const current = await fetch(url, { headers })
44
    if (!current.ok) {
45
      throw new Error(`${current.status} ${await current.text()}`)
46
    }
47
    const { result } = (await current.json()) as { result: any }
48
    previousState = value(result.state)
49
    alreadyInState =
50
      String(state) === String(previousState) ||
51
      String(state).toLowerCase() === String(label(result.state)).toLowerCase()
52
  }
53

54
  const body: { [key: string]: any } = { ...(fields ?? {}) }
55
  if (work_notes) body.work_notes = work_notes
56
  if (state) body.state = state
57

58
  const response = await fetch(url, {
59
    method: "PATCH",
60
    headers: { ...headers, "Content-Type": "application/json" },
61
    body: JSON.stringify(body),
62
  })
63

64
  if (!response.ok) {
65
    throw new Error(`${response.status} ${await response.text()}`)
66
  }
67

68
  const { result } = (await response.json()) as { result: any }
69
  const ignoredFields: string[] = result.__meta?.ignoredFields ?? []
70

71
  if (state) {
72
    const newState = value(result.state)
73
    if (
74
      ignoredFields.includes("state") ||
75
      (String(newState) === String(previousState) && !alreadyInState)
76
    ) {
77
      throw new Error(
78
        `ServiceNow did not move change ${value(result.number)} to state ${state}: it is still in ${label(result.state)} (${newState}). Check the allowed transitions with Get change next states.`
79
      )
80
    }
81
  }
82

83
  return {
84
    sys_id: value(result.sys_id),
85
    number: value(result.number),
86
    state: value(result.state),
87
    state_label: label(result.state),
88
    ignored_fields: ignoredFields,
89
    record: result,
90
  }
91
}
92