Changes API
The Changes API manages IT changes following ITIL: a state machine driven by dedicated transition endpoints, approval via the unified approval framework, structured change tasks (implementation/test/rollback) with individual or group assignment, plus calendar invites (.ics) for scheduled work.
Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
GET | /api/changes | List with filters ({data, pagination}; RBAC-filtered) |
GET | /api/changes/stats | Tab counters (all / my / assigned) — accepts the same filters as the list |
GET | /api/changes/:id | Get single change (by ID or number) |
POST | /api/changes | Create new change (status always DRAFT) → 201 |
PATCH | /api/changes/:id | Update change fields (NO status, version mandatory) |
DELETE | /api/changes/:id | Delete change (soft-delete, critical action) |
POST | /api/changes/:id/restore | Restore deleted change (trash listing: ?deleted=1) |
PATCH | /api/changes/:id/assign | Set/clear change manager → {change, assignmentChanged} |
GET | /api/changes/:id/field-permissions | Editable/locked/required fields + allowed transitions for current status |
POST | /api/changes/:id/activity | Add comment (the activity log itself comes via GET /:id) |
Enum values in upper case: Status, type, priority, risk, impact and urgency are sent and returned in upper case (NORMAL, VERY_HIGH, PENDING_APPROVAL …), including filters and sorting. Lower-case values return 400.
Who sees the change: Besides the requestor (viewOwn) and global visibility (viewAll), involvement also opens access: the assigned change manager, an open approval and whoever holds a change task (directly, via the assigned group or as a substitute) and has changes.viewOwn resp. viewPendingApprovals. This applies to the list, detail, search, reports, attachments and the link cards. Edit and task-management rights are unaffected. Without viewAll, viewOwn or viewPendingApprovals the list and the counters answer 403.
Transition Endpoints (Status Changes)
Important: Statuses are set exclusively through these dedicated endpoints. PATCH /api/changes/:id does NOT accept a status field. Each endpoint has its own permission and required body fields.
| Endpoint | Transition | Permission | Body |
|---|---|---|---|
POST /:id/submit | DRAFT → SUBMITTED | changes.submit | changeManagerId, notes? |
POST /:id/route-to-approval | SUBMITTED → PENDING_APPROVAL (or APPROVED if skipApproval) | changes.manageWorkflow + assigned change manager | skipApproval?, notes? |
POST /:id/schedule | APPROVED → SCHEDULED | changes.schedule | scheduledStartTime, scheduledEndTime |
POST /:id/start-implementation | SCHEDULED → IN_PROGRESS | changes.startImplementation | notes? |
POST /:id/complete | IN_PROGRESS → COMPLETED | changes.markCompleted | resolution (≥20), closerId, actualEndTime? |
POST /:id/fail | IN_PROGRESS → FAILED | changes.markFailed | resolution (≥20), closerId, backoutPerformed? |
POST /:id/initiate-rollback | IN_PROGRESS → ROLLING_BACK | changes.backout | reason (≥20) |
POST /:id/backout | ROLLING_BACK → BACKED_OUT | changes.backout | resolution (≥20), closerId |
POST /:id/close | COMPLETED/FAILED/BACKED_OUT → CLOSED | changes.close | reviewNotes (≥20), successCriteriaMet? |
POST /:id/return-to-draft | * → DRAFT | changes.returnToDraft | reason (≥10) |
Errors when routing to approval: A STANDARD change needs no approval round; the attempt returns 400 CHANGE_APPROVAL_NOT_REQUIRED. Use skipApproval: true instead. If no eligible approver is found, the route answers 400 CHANGE_NO_APPROVERS_AVAILABLE.
A complete task set as a precondition: The lifecycle transitions require the complete set of change tasks until closure (one task each of kinds IMPLEMENTATION, TEST and ROLLBACK). A change without tasks can neither be started nor closed — the way back is return-to-draft.
Approval Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/changes/:id/approvers | Assign approver (creates an approval entry) |
POST | /api/approvals/:id/decide | Approver decision (unified approval framework) |
Approvers make their decision (approve/reject) via the Approvals API. See Approvals API.
Change Tasks
All routes live under /api/changes/:changeId/tasks:
| Method | Endpoint | Description |
|---|---|---|
GET | / | List the change's tasks |
POST | / | Create task |
POST | /reorder | Reorder (per kind, with version) |
POST | /bulk-assign | Bulk-assign tasks |
POST | /bulk-skip | Bulk-skip tasks |
POST | /bulk-delete | Bulk-delete tasks |
PATCH | /:taskId | Update task (version required) |
DELETE | /:taskId?version=N | Delete task (version as query param) |
POST | /:taskId/assign | Assign user OR group |
POST | /:taskId/start | Start task (→ IN_PROGRESS) |
POST | /:taskId/complete | Complete task (completionNote required) |
POST | /:taskId/skip | Skip task (skipReason required) |
POST | /:taskId/fail | Mark task failed (failureReason required, ≥10 characters) |
POST | /:taskId/retry | Retry a failed task (from FAILED only, reason ≥10 characters) |
POST | /:taskId/comments | Comment on task (≥10 characters) → 201 |
Change Templates /api/changes/templates
Templates model recurring standard changes (ITIL): predefined default values, locked or user-required fields and their own task set. Templates are versioned and run through their own approval workflow. Changes are created from an approved template — the task set is cloned from the approved version snapshot.
Template Status
DRAFT → SUBMITTED(submit) → PENDING_APPROVAL → APPROVED → RETIRED(retire) Side paths:PENDING_APPROVAL → DRAFT (withdraw) APPROVED → DRAFT (new-version — new draft version) DRAFT(discarded) → APPROVED (discard-draft — revert to last approved version) Editing (PATCH, tasks) is only possible in status DRAFT.Approval and rejection run through the unified approval framework.
Endpoints
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /templates | List (filters + offset/limit pagination) | changes.viewTemplates |
GET | /templates/approved | Approved templates only (for selector) | changes.viewTemplates |
GET | /templates/statistics | Template statistics | changes.viewTemplates |
GET | /templates/:id | Single template | changes.viewTemplates |
GET | /templates/:id/versions | Version history | changes.viewTemplates |
GET | /templates/:id/versions/:version | Specific version | changes.viewTemplates |
GET | /templates/:id/changes | Changes using this template (limit/offset) | changes.viewTemplates |
GET | /templates/:id/activity | Activity history (limit) | changes.viewTemplates |
GET | /templates/:id/tasks | List template tasks | changes.viewTemplates |
POST | /templates | Create template (201, status DRAFT) | changes.createTemplates |
PATCH | /templates/:id | Update template (DRAFT only) | changes.editOwnTemplates / editAllTemplates |
DELETE | /templates/:id | Delete template (DRAFT only, 204) | changes.deleteTemplates |
POST | /templates/:id/submit | DRAFT → PENDING_APPROVAL | changes.submitTemplates |
POST | /templates/:id/withdraw | PENDING_APPROVAL → DRAFT withdraw | changes.submitTemplates |
POST | /templates/:id/discard-draft | Discard DRAFT → last approved version | changes.editOwnTemplates / editAllTemplates |
POST | /templates/:id/retire | → RETIRED (body reason?) | changes.retireTemplates |
POST | /templates/:id/new-version | APPROVED → new DRAFT version (body changeReason + fields) | changes.editOwnTemplates / editAllTemplates |
POST | /templates/:id/create-change | Create change from template (201) | changes.create |
POST | /templates/:id/tasks | Add template task (DRAFT only, 201) | changes.editOwnTemplates / editAllTemplates |
PATCH | /templates/:id/tasks/:taskId | Update template task (DRAFT only) | changes.editOwnTemplates / editAllTemplates |
DELETE | /templates/:id/tasks/:taskId | Delete template task (DRAFT only, 204) | changes.editOwnTemplates / editAllTemplates |
POST | /templates/:id/tasks/reorder | Reorder tasks (DRAFT only, 204) | changes.editOwnTemplates / editAllTemplates |
Write/action routes additionally check ownership (creator OR editAllTemplates). A template in status DRAFT is visible only to its creator — unless they hold changes.viewDraftTemplates OR an approved version already exists: then the template shows its approved revision to every viewTemplates holder. The same rule governs list, detail and the sub-routes (/versions, /changes, /activity, /tasks); a template outside your visibility returns 404 TEMPLATE_NOT_FOUND, just like a non-existent one. Approval/rejection runs exclusively via Approvals API (
POST /api/approvals/:id/decide).
Template Fields
| Field | Type | Description |
|---|---|---|
name | string | Slug, required (only a–z, 0–9, hyphen) |
displayName | string | Display name, required |
description | string | Description, required |
type | enum | STANDARD, NORMAL, EMERGENCY, MAJOR (default STANDARD) |
categoryId | string? | Change category |
defaultTitle / defaultDescription / defaultJustification | string? | Pre-filled content of the created change |
defaultRiskLevel / defaultImpact | enum | LOW, MEDIUM, HIGH, VERY_HIGH (default LOW) |
defaultUrgency / defaultPriority | enum | LOW, MEDIUM, HIGH, CRITICAL (default LOW) |
defaultRiskAssessment | any? | Pre-filled risk assessment |
defaultAffectedServices / defaultAffectedAssets | string[] | Pre-filled affected items |
defaultPlannedDuration | number? | Pre-filled planned duration (minutes) |
lockedFields | string[] | Fields locked when creating from the template |
requiredUserFields | string[] | Fields the user must fill in when creating |
Pagination of the template endpoints: The template list and the list of a template's changes use offset/limit pagination (default 20, max 100), the activity history default 50 / max 100 — unlike the change list (page/per). Enum values are upper case here as everywhere.
Template Tasks
Template tasks define the task set cloned when a change is created. Manageable only while the template is in DRAFT.
| Field | Values | Description |
|---|---|---|
kind | IMPLEMENTATION, TEST, ROLLBACK | Kind (not editable after creation — delete + recreate) |
phase | PREP, EXECUTE, VALIDATE, POSTCHECK | Phase (default EXECUTE) |
title / description | string | Title (required) / description |
sortOrder | number | Order |
estimatedMinutes | number? | Estimated duration |
requiredPermission | string? | Permission required to execute |
roleHint | string? | Hint for the responsible role |
suggestedGroupId | string? | Suggested agent group |
dependsOnTemplateTaskIds | cuid[] | Predecessor template tasks |
Create Change from Template
POST /api/changes/templates/:id/create-change
{
"title": "Upgrade PostgreSQL on cluster-prod-2",
"scheduledStartTime": "2026-02-01T02:00:00Z",
"scheduledEndTime": "2026-02-01T04:00:00Z",
"assignedGroupId": "clx-dba-group-id"
}
All body fields are optional and override the template defaults: title, description, justification, affectedServices, affectedAssets, scheduledStartTime, scheduledEndTime, plannedDuration, categoryId, assignedToId, assignedGroupId (plus requestorId for API-key auth). Locked fields (lockedFields) stay unchanged; the task set is cloned from the approved version snapshot.
Change Categories
| Method | Endpoint | Description |
|---|---|---|
GET | /api/changes/categories | All categories |
POST | /api/changes/categories | Create category |
PUT | /api/changes/categories/:id | Update category |
DELETE | /api/changes/categories/:id | Delete category |
State Machine
Changes go through 12 statuses. Each transition happens via a dedicated transition endpoint:
DRAFT → SUBMITTED → PENDING_APPROVAL → APPROVED → SCHEDULED → IN_PROGRESS → COMPLETED → CLOSED Alternative paths: PENDING_APPROVAL → REJECTED (approver rejects) * → DRAFT (via return-to-draft) IN_PROGRESS → FAILED (via fail) IN_PROGRESS → ROLLING_BACK → BACKED_OUT (via initiate-rollback + backout) COMPLETED/FAILED/BACKED_OUT → CLOSED (via close) Status descriptions: • DRAFT = Draft, not yet submitted • SUBMITTED = Submitted, change manager assigned • PENDING_APPROVAL = Waiting for approvals (unified approval framework) • APPROVED = Approved • REJECTED = Rejected • SCHEDULED = Scheduled for maintenance window • IN_PROGRESS = Implementation in progress (change tasks being executed) • ROLLING_BACK = Rollback in progress • COMPLETED = Successfully completed • FAILED = Failed • BACKED_OUT = Rolled back • CLOSED = Closed & archived
Change Types
| Type | Description | Approval Requirement |
|---|---|---|
| STANDARD | Routine changes, low risk (often template-based) | Often pre-approved |
| NORMAL | Standard changes, medium risk | Regular approval process |
| EMERGENCY | Emergency changes (e.g., urgent security updates) | Fast-track, post-implementation review |
| MAJOR | Major changes, high risk | Extended approvals |
Create Change
Request
POST /api/changes
Requestor (requestorId): For a signed-in user the server sets the requestor to the caller; a value sent in the body is ignored. Only API-key calls must supply requestorId. The status on creation is always DRAFT; everything beyond that runs through the transition endpoints.
{
"title": "Upgrade PostgreSQL to version 17",
"description": "Upgrade production database from PostgreSQL 16 to 17 for performance and security.",
"justification": "Security patches for CVE-2024-xxx. Performance improvements.",
"type": "NORMAL",
"categoryId": "clx-category-id",
"priority": "HIGH",
"riskLevel": "MEDIUM",
"impact": "HIGH",
"urgency": "MEDIUM",
"scheduledStartTime": "2026-02-01T02:00:00Z",
"scheduledEndTime": "2026-02-01T04:00:00Z",
"plannedDuration": 120,
"affectedServices": ["Database", "API"],
"affectedAssets": ["clx-asset-id"],
"riskAssessment": "Standard maintenance-window upgrade, rollback tested.",
"assignedToId": "clx-change-manager-id",
"tasks": [
{ "clientKey": "impl-1", "kind": "IMPLEMENTATION", "phase": "EXECUTE", "title": "Run pg_upgrade" },
{ "kind": "TEST", "phase": "VALIDATE", "title": "Run integration tests", "dependsOnTaskKeys": ["impl-1"] },
{ "kind": "ROLLBACK", "phase": "POSTCHECK", "title": "Restore from backup if needed" }
]
}
Response (201 Created)
{
"id": "clx...",
"number": "CHG-2026-000042",
"title": "Upgrade PostgreSQL to version 17",
"status": "DRAFT",
"type": "NORMAL",
"priority": "HIGH",
"riskLevel": "MEDIUM",
"impact": "HIGH",
"urgency": "MEDIUM",
"requestor": { "id": "clx...", "name": "John Doe", "email": "john@example.com" },
"assignedTo": { "id": "clx...", "name": "Change Manager" },
"createdAt": "2026-01-27T15:00:00.000Z"
}
Fields Overview
| Field | Type | Required? | Description |
|---|---|---|---|
title | string | ✓ | Short title (5–200 characters) |
description | string | ✓ | Detailed description (20–5000 characters) |
justification | string | ✓ | Justification for the change (20–2000 characters) |
type | enum | ✓ | STANDARD, NORMAL, EMERGENCY, MAJOR |
categoryId | string | ✓ | Change category (ID, required) |
priority | enum | ✓ | LOW, MEDIUM, HIGH, URGENT, CRITICAL |
riskLevel | enum | ✓ | LOW, MEDIUM, HIGH, VERY_HIGH |
impact | enum | ✓ | LOW, MEDIUM, HIGH, VERY_HIGH |
urgency | enum | ✓ | LOW, MEDIUM, HIGH, CRITICAL |
requestorId | string | (API key) | With user auth = logged-in user; required for API-key auth |
tasks | array | Inline change tasks (see Change Tasks); not combined with templateId | |
scheduledStartTime | DateTime | Scheduled start | |
scheduledEndTime | DateTime | Scheduled end | |
plannedDuration | number | Planned duration/downtime (minutes) | |
affectedServices | string[] | Affected services | |
affectedAssets | string[] | Affected assets | |
riskAssessment | string | Risk assessment (free text) | |
assignedToId / assignedGroupId | string | Change manager (user or group) | |
relatedTickets / relatedProblems | string[] | Linked tickets/problems — require the linking permission and visibility of the other side, signed-in users only | |
templateId | string | Template reference (tasks cloned from snapshot) | |
fromProblemId | string | Cross-entity creation from a problem |
Links to tickets and problems: relatedTickets and relatedProblems require the same permission as the linking endpoint (changes.linkToTickets resp. changes.linkToProblems, otherwise 403), plus visibility of the respective other side. A save that leaves the links unchanged does not require the permission. They are bound to a signed-in user (API key: 400 CHANGE_LINKS_REQUIRE_USER), and no new links can be added to a closed change. Every link writes a timeline entry on BOTH sides.
Approval Workflow
Approvals run through the central unified approval framework. On the change you only assign approvers; the actual decision is made via the Approvals API.
Step 1: Submit (DRAFT → SUBMITTED)
POST /api/changes/:id/submit
{
"changeManagerId": "clx-change-manager-id",
"notes": "Ready for review"
}
Step 2: Assign Approvers
POST /api/changes/:id/approvers
{
"userId": "clx-manager-id"
}
Response (201 Created)
{
"id": "clx-approval-id",
"userId": "clx-manager-id",
"required": true,
"decision": null,
"decisionAt": null,
"comment": null,
"user": { "id": "clx-manager-id", "name": "Jane Manager", "email": "jane@example.com" },
"sourceGroup": { "id": "clx-cab-group", "name": "cab", "displayName": "Change Advisory Board" },
"isManual": true,
"sequence": 0
}
- Permission
changes.addApproverplus visibility of this change (the same check as for detail and comment). - Only possible while the change is in SUBMITTED or PENDING_APPROVAL (otherwise
APPROVER_INVALID_STATUS). - Four-eyes principle: requestor and assigned change manager cannot be approvers (403
CONFLICT_OF_INTEREST). - A manually added approver is attached to the running approval round — sourceGroup is filled accordingly and their decision counts towards its evaluation.
Step 3: Route to approval (SUBMITTED → PENDING_APPROVAL)
POST /api/changes/:id/route-to-approval
{
"skipApproval": false,
"notes": "Routing to CAB"
}
With skipApproval: true the change jumps straight to APPROVED (requires changes.manageWorkflow).
Step 4: Approvers Decide
POST /api/approvals/:id/decide
The decision is made via the central approval framework. Once all required approvers have approved, the change automatically moves to APPROVED; on rejection to REJECTED. For details, request/response format and the changes.approve permission see Approvals API.
Change Tasks in Detail
Change tasks model the concrete implementation. Each task has a kind, a phase, a status, and is assigned either to a single user or an agent group.
| Field | Values | Description |
|---|---|---|
kind | IMPLEMENTATION, TEST, ROLLBACK | Type of task |
phase | PREP, EXECUTE, VALIDATE, POSTCHECK | Phase (default: EXECUTE) |
status | PENDING, BLOCKED, IN_PROGRESS, DONE, SKIPPED, FAILED | BLOCKED while dependencies are open |
assignedToId XOR assignedGroupId | cuid | User OR group – never both |
dependsOnTaskIds | cuid[] | Predecessor tasks (cycle check server-side) |
estimatedMinutes / actualMinutes | number | Time tracking |
completionNote / skipReason / failureReason | string | Required on complete / skip / fail |
handoverNotes | string | Handover to dependent tasks |
version | number | Optimistic lock – required on every mutation |
Create Task
POST /api/changes/:changeId/tasks
{
"kind": "IMPLEMENTATION",
"phase": "EXECUTE",
"title": "Run pg_upgrade on primary",
"description": "Execute pg_upgrade and verify cluster starts",
"assignedGroupId": "clx-dba-group-id",
"estimatedMinutes": 45,
"dependsOnTaskIds": ["clx-prep-task-id"]
}
Task Lifecycle
# Assign user OR group (version required)
POST /api/changes/:changeId/tasks/:taskId/assign
{ "assignedToId": "clx-user-id", "version": 1 }
# Start – on group assignment the starting agent is atomically set as assignedToId
POST /api/changes/:changeId/tasks/:taskId/start
{ "version": 2 }
# Complete (completionNote required)
POST /api/changes/:changeId/tasks/:taskId/complete
{ "completionNote": "pg_upgrade completed, cluster healthy", "actualMinutes": 38, "version": 3 }
# Skip / fail
POST /api/changes/:changeId/tasks/:taskId/skip { "skipReason": "Not needed, already on v16", "version": 2 }
POST /api/changes/:changeId/tasks/:taskId/fail { "failureReason": "pg_upgrade aborted: incompatible cluster", "version": 2 }
Permissions: Managing (create/update/delete/reorder/assign/skip/bulk/retry) requires changes.manageTasks OR (changes.editOwn as requestor) OR change-assignee/active group member. Executing (start/complete/fail) requires changes.manageTasks OR (changes.executeTask with involvement). Commenting needs either of the two levels. Note: changes.editAll alone grants NO task rights.
The task structure is frozen during approval: While a change sits in PENDING_APPROVAL, tasks can neither be created, changed, deleted nor reordered — the board assesses an unchangeable proposal. Skipping is only possible in the currently active phase. A failed task can be resumed via retry.
Calendar Invites (.ics)
When a change task is assigned to a person and the change has scheduledStartTime and scheduledEndTime, Eviworx sends a calendar invite (.ics, METHOD:REQUEST) to the assignee. On reassignment/cancellation a CANCEL invite goes to the previous assignee.
- .ics REQUEST only when the change is in one of the statuses
SCHEDULED,APPROVED,IN_PROGRESSand start/end are set. - CANCEL is sent regardless of status as soon as schedule data exists.
Update Change
PATCH /api/changes/:id
PATCH updates data fields only – NOT status (transition endpoints handle that). Requires changes.editAll OR changes.editOwn (as requestor). Changing assignedToId/assignedGroupId additionally requires changes.assign. The version field is MANDATORY (optimistic locking, 409 CHANGE_VERSION_CONFLICT on a stale state); null clears a field, an omitted key leaves it unchanged. Which fields are editable/locked/required in the current status is returned by GET /:id/field-permissions — sending a locked field ends with 400 LOCKED_FIELD_MODIFICATION.
List Changes
GET /api/changes?f.status=in:PENDING_APPROVAL,APPROVED&f.type=NORMAL&sort=scheduledStartTime:asc&page=1&per=20
The list is RBAC-filtered (viewAll / viewOwn / viewPendingApprovals) and supports filters and sorting. A filter has the form f.<field>=<operator>:<value>; without an operator prefix equality applies. Response:
{
"data": [ /* changes */ ],
"pagination": { "page": 1, "limit": 20, "total": 137, "totalPages": 7, "hasMore": true }
}
Query Parameters
| Parameter | Description |
|---|---|
f.status, f.type, f.priority, f.riskLevel, f.impact | Enum filters (eq/neq/in/notIn, values in upper case) |
f.number, f.title | Text filters (eq/contains/startsWith) |
f.requestorId, f.assignedToId, f.categoryId | Assignment filters (eq/in, partly isNull/isNotNull) |
f.scheduledStartTime, f.scheduledEndTime, f.createdAt, f.updatedAt | Time filters (gt/gte/lt/lte/between/relative) |
q | Search across number, title, description |
page / per / sort | Paging (per defaults to 25, max 200) |
deleted=1 | Trash: ONLY deleted changes (requires changes.viewDeleted; accepts the value 1 only) |
includeDeleted=true | Mixed list incl. deleted ones (requires changes.viewDeleted) |
relatedTicketId, relatedProblemId | Filter by linkage |
Error Handling
Self-Approval Blocked (Four-Eyes)
{
"errorCode": "CONFLICT_OF_INTEREST",
"message": "Requestor and assigned change manager cannot be an approver of this change."
}
Approver in Invalid Status (400)
{
"errorCode": "APPROVER_INVALID_STATUS",
"message": "Cannot add an approver while the change is in status SCHEDULED",
"status": "SCHEDULED"
}
Required Field Missing (Transition, 400)
{
"error": "VALIDATION_ERROR",
"details": [
{ "field": "resolution", "message": "Resolution notes must be at least 20 characters" }
]
}
Code Example: Complete Lifecycle (JavaScript)
// ===================================================
// COMPLETE CHANGE LIFECYCLE
// Status NEVER set via PATCH — always dedicated endpoints.
// ===================================================
const API_URL = 'https://your-instance.com/api';
const headers = { 'Content-Type': 'application/json' };
const post = (path, body) => fetch(`${API_URL}${path}`, {
method: 'POST', credentials: 'include', headers,
body: body ? JSON.stringify(body) : undefined,
}).then(r => r.json());
// 1. Create change (UPPER enum values, inline tasks)
const change = await post('/changes', {
title: 'Upgrade PostgreSQL to version 16',
description: 'Database upgrade for security and performance reasons',
justification: 'Security patches for CVE-2024-xxx, performance improvements',
type: 'NORMAL',
categoryId: 'clx-category-id',
priority: 'HIGH',
riskLevel: 'MEDIUM',
impact: 'HIGH',
urgency: 'MEDIUM',
scheduledStartTime: '2026-02-01T02:00:00Z',
scheduledEndTime: '2026-02-01T04:00:00Z',
plannedDuration: 120,
tasks: [
{ clientKey: 'impl', kind: 'IMPLEMENTATION', title: 'Run pg_upgrade' },
{ kind: 'TEST', title: 'Run integration tests', dependsOnTaskKeys: ['impl'] },
{ kind: 'ROLLBACK', title: 'Restore from backup if needed' },
],
});
console.log('Created:', change.number); // CHG-2026-000042
// 2. Submit (DRAFT → SUBMITTED), set change manager
await post(`/changes/${change.id}/submit`, { changeManagerId: 'clx-cm-id' });
// 3. Assign an approver (must not be requestor/change manager)
const approver = await post(`/changes/${change.id}/approvers`, { userId: 'clx-cab-member' });
// 4. Route to approval (SUBMITTED → PENDING_APPROVAL)
await post(`/changes/${change.id}/route-to-approval`, { skipApproval: false });
// 5. Approver decides via the unified approvals framework (different user!)
await post(`/approvals/${approver.id}/decide`, { decision: true, comment: 'Looks good' });
// → all required approvers done ⇒ change auto-transitions to APPROVED
// 6. Schedule (APPROVED → SCHEDULED) — .ics invites go out for assigned tasks
await post(`/changes/${change.id}/schedule`, {
scheduledStartTime: '2026-02-01T02:00:00Z',
scheduledEndTime: '2026-02-01T04:00:00Z',
});
// 7. Start implementation (SCHEDULED → IN_PROGRESS)
await post(`/changes/${change.id}/start-implementation`, { notes: 'Window opened' });
// 8. Work the tasks (start → complete, version-locked)
const tasks = await fetch(`${API_URL}/changes/${change.id}/tasks`, { credentials: 'include' }).then(r => r.json());
for (const t of tasks) {
await post(`/changes/${change.id}/tasks/${t.id}/start`, { version: t.version });
await post(`/changes/${change.id}/tasks/${t.id}/complete`, {
completionNote: 'Done and verified', version: t.version + 1,
});
}
// 9. Complete the change (IN_PROGRESS → COMPLETED)
await post(`/changes/${change.id}/complete`, {
resolution: 'Upgrade completed successfully, all tests green',
closerId: 'clx-cm-id',
});
// 10. Close (COMPLETED → CLOSED)
await post(`/changes/${change.id}/close`, {
reviewNotes: 'Post-implementation review passed, no incidents',
successCriteriaMet: true,
});
Rollback Scenario
If implementation goes wrong, there are two paths:
# A) Controlled rollback: IN_PROGRESS → ROLLING_BACK → BACKED_OUT
POST /api/changes/:id/initiate-rollback
{ "reason": "Integration tests failed, queries timing out — rolling back to v15" }
POST /api/changes/:id/backout
{ "resolution": "Restored from backup, cluster back on v15 and healthy", "closerId": "clx-cm-id" }
# B) Failure without rollback: IN_PROGRESS → FAILED
POST /api/changes/:id/fail
{ "resolution": "pg_upgrade aborted: incompatible cluster versions", "closerId": "clx-cm-id", "backoutPerformed": false }
# Then close: FAILED/BACKED_OUT → CLOSED
POST /api/changes/:id/close
{ "reviewNotes": "Root cause documented, retry planned for next window" }
- ✓ Status only via transition endpoints
- ✓ Approval via unified approval framework
- ✓ Four-eyes principle enforced
- ✓ Structured change tasks with optimistic lock
- ✓ Calendar invites (.ics) for scheduled work
changes.viewAll/viewOwn/viewPendingApprovalschanges.create/editAll/editOwnchanges.assign/addApproverchanges.submit/manageWorkflow/schedulechanges.startImplementation/markCompleted/markFailedchanges.backout/close/returnToDraftchanges.manageTasks/executeTaskchanges.delete/restorechanges.viewTemplates/viewDraftTemplates/createTemplateschanges.editOwnTemplates/editAllTemplates/submitTemplates/retireTemplates/deleteTemplates
Auth/role model: Permissions & RBAC
Problems API →
Learn more about the Problems API
Entity Linking API →
Link changes with tickets/problems/incidents/assets