Reopen & Lifecycle Governance
Reopen governance uniformly controls whether, for how long, by whom and how often a closed entity may be reopened — across tickets, incidents and problems. The rules are the same for all three entity types and configurable per type in the lifecycle config. This also covers auto-close (time-based closing of resolved tickets), WC-auto-resolve (resolving unanswered waiting-customer tickets), stale reminders (inactivity escalation across all three domains), reopen escalation (a quality signal when something is reopened too often) and a dedicated analytics dashboard.
🎫 For Agents: Automatic Ticket Actions
Three time-based automations act on tickets — all configurable and toggleable by an admin in system settings:
- 1. WC auto-resolve (Waiting for Customer → Resolved): If the customer does not reply for X days, the ticket is automatically set to Resolved (code "Auto-Resolved — No Response"); optionally a warning goes to customer and agent Y days earlier. The timer runs from the last public message — whether from the customer or an agent: a reminder email to the customer restarts it. Internal notes and field changes do not count.
- 2. Stale reminder (inactivity): Active tickets (Open, In Progress, Waiting for Support) left untouched too long trigger a reminder (staggered: assignee → group lead → manager). Here every change to the ticket resets the timer — status change, comment, field change, email reply. Status and SLA are never altered.
- 3. Auto-close (Resolved → Closed): Tickets in Resolved are automatically closed after the configured deadline — also with an optional advance warning.
All deadlines live in the per-entity lifecycle config and are disabled by default. Technical details (config keys, notifications, jobs) below.
How a reopen works
A central check decides, based on the lifecycle config, whether a reopen is allowed. The system then resets the closing fields (including resolution code, closedAt/resolvedAt), increments reopenCount and writes a timeline entry, an audit entry and a notification.
Lifecycle-Config (per entity, admin-editable)
│ reopenEnabled · reopenWindowDays · reopenWindowMode · maxReopenCount
│ reasonRequired · notifyOnReopen · slaOnReopenFrom{Resolved,Closed}
│ autoCloseEnabled · autoResolveWaitingEnabled · staleReminderEnabled · reopenEscalationThreshold
▼
Reopen check (window, limit, reason, approval) │ → allowed / refused with error code ▼
Reset per entity type Ticket
Incident (+ approval)
Problem
│ reset closedAt/resolvedAt/resolutionCode · reopenCount++ · reopenedAt
▼
Audit REOPENED · visible timeline entry · Notification (*_REOPENED)
Reopen Evaluation (Policy Steps)
The check runs in a set order. reopenOverride bypasses ONLY window and limit — NOT the reason requirement and NOT the approval requirement (incident). Even an admin needs a reason.
| Step | Check | Override? | Error-Code |
|---|---|---|---|
| 1 | Reopen enabled? (reopenEnabled) | — | REOPEN_DISABLED |
| 2 | Source terminal? (CLOSED/SPAM/…) | — | REOPEN_NOT_TERMINAL |
| 3 | Within reopen window? (reopenWindowDays from closedAt) | ✅ bypassed | REOPEN_WINDOW_EXPIRED |
| 4 | Below maximum? (reopenCount < maxReopenCount) | ✅ bypassed | REOPEN_LIMIT_REACHED |
| 5 | Valid reopen reason? (+ note if requiresNote) | ❌ always required | REOPEN_REASON_REQUIRED / REOPEN_REASON_INVALID / REOPEN_NOTE_REQUIRED |
| 6 | Approval needed? (incident only, on by default) | ❌ never bypassed | REOPEN_REQUIRES_APPROVAL |
onWindowExpired (Ticket): Once a ticket's reopen window has expired, a customer reply by email creates a new ticket linked to the old one (originContext EMAIL_FOLLOWUP) instead of reopening — the customer is never locked out.
reopenWindowMode: The reopen window counts either in calendar days (CALENDAR, default) or business days (BUSINESS). BUSINESS counts according to the business hours of the entity type's default SLA policy; if no default policy or business-hours config exists, it safely falls back to CALENDAR.
Behavior per Domain
| Domain | Source → target | Reopen paths | Approval |
|---|---|---|---|
| Ticket | CLOSED/SPAM → OPEN |
Manual (PATCH), email reply, web comment (system reopen) | — |
| Incident | CLOSED → ACKNOWLEDGED |
Dedicated route POST /:id/reopen | Yes (default) — incidents.approveReopen |
| Problem | CLOSED/RESOLVED → INVESTIGATING |
Manual (PATCH) | — |
Important (ticket): RESOLVED is NOT a reopen source — moving from RESOLVED back to an active status is a normal editStatus change (no reason, no counter), keeping the reopen KPI clean. Reopen sources are exclusively CLOSED and SPAM. Reopening a merged ticket is redirected to the live target ticket (mergedToTicketId).
Feature Matrix per Domain
| Feature | Ticket | Problem | Incident |
|---|---|---|---|
| Reopen governance | ✅ (3 paths) | ✅ (manual) | ✅ (manual + approval) |
| Auto-Close | ✅ | — | — |
| WC-Auto-Resolve | ✅ | — | — |
| Stale-Reminder | ✅ | ✅ | ✅ |
| Reopen escalation | ✅ | ✅ | ✅ |
| Reopen window display | ✅ | ✅ | ✅ |
| SLA config-driven | ✅ | ✅ | ✅ |
All lifecycle automations (auto-close, WC-auto-resolve, stale reminder, reopen escalation) are OFF by default and enabled per entity in the lifecycle config. The related jobs run on schedule but do nothing until the feature is enabled.
Permissions
Reopen is a dedicated permission; editStatus and editAll do not include it. A role with editAll but without reopen cannot reopen.
| Permission key | Effect |
|---|---|
tickets.reopen · incidents.reopen · problems.reopen | May reopen the respective entity (with reason, within window/limit) |
tickets.reopenOverride · incidents.reopenOverride · problems.reopenOverride | Bypasses window + limit (NOT the reason requirement, NOT approval). Admin only by default. |
The built-in default roles are preset as follows: Agent holds reopen, Admin holds reopen and reopenOverride, end user and approver hold neither. A newly created role must be granted both permissions explicitly. Permissions & RBAC
Configuration (Admin)
Two areas drive the governance — Admin Center → Service Configuration → Reopen & Lifecycle (/admin/lifecycle), permission settings.editGeneral:
- Reopen Reasons — CRUD per entity (multilingual label, per-reason note requirement, ordering, soft-delete → 204). Errors: duplicate code 409 REOPEN_REASON_CODE_EXISTS; last active reason of an entity 409 REOPEN_REASON_LAST_ACTIVE; unknown id 404 REOPEN_REASON_NOT_FOUND. Reading inactive reasons (?includeInactive=true) requires settings.editGeneral; revoking this permission takes effect immediately.
GET/POST/PATCH/DELETE /api/reopen-reasons - Lifecycle-Config — window (days + mode CALENDAR/BUSINESS), maximum count, per-role reason requirement, notifyOnReopen, SLA behavior on reopen (slaOnReopenFromResolved/Closed), auto-close, WC-auto-resolve, stale-reminder tiers, escalation threshold. The settings UI shows only the fields relevant per entity per tab (e.g. auto-close/WC-auto-resolve only for tickets).
GET /api/lifecycle-config·PUT /api/lifecycle-config/:entityType
Remaining reopen window: GET /api/lifecycle-config/:entityType/reopen-window returns reopenEnabled/Days/From and only requires a login. The detail sidebar shows the remaining time. The full config requires settings.editGeneral.
Auto-Close
Auto-close closes due tickets on a schedule like a normal status change (closedAt, SLA and activity are set). Auto-close exists for tickets only (autoCloseEnabled, off by default). Incidents and problems are always closed manually, because an approval or a PIR determines the closure there.
| Aspect | Detail |
|---|---|
| Job | lifecycle_auto_close (daily ~02:30) |
| Due time | GREATEST(resolvedAt, latest non-internal message, config.updatedAt) |
| Grace | The deadline counts from the config save at the earliest (config.updatedAt), so that first activation does not close the entire backlog at once. |
| Advance warning | autoCloseWarnDays → notification TICKET_AUTO_CLOSE_WARNING (customer primary); a customer reply bumps the due time → re-warn |
WC-Auto-Resolve (waiting customer)
A step before auto-close: tickets in WAITING_CUSTOMER status the customer does not respond to are automatically set to RESOLVED after a configured deadline (the normal auto-close path may then take over). TICKET only, opt-in.
| Aspect | Detail |
|---|---|
| Job | lifecycle_wc_auto_resolve (daily ~02:00) |
| Due time | latest non-internal (public) message — customer OR agent (fallback updatedAt) + autoResolveWaitingDays (default 14) |
| Advance warning | autoResolveWaitingWarnDays (default 3) → TICKET_WC_RESOLVE_WARNING (customer primary) |
| Resolution-Code | autoResolveWaitingCode (default AUTO_RESOLVED_NO_RESPONSE — notifyCustomer=false, EXCLUDE_FROM_REPORTING) |
| Reset | customer reply → status WAITING_SUPPORT + wcResolveWarnedAt=null (timer restarts) |
Stale Reminder (inactivity)
Inactivity reminder for TICKET, PROBLEM and INCIDENT. Reports records untouched for too long in tiers to assignee (tier 1) → group lead (tier 2) → manager (tier 3), only to people who can see the record. Never changes status or SLA.
- Job:
lifecycle_stale_entity_reminder(daily ~01:30) - Config:
staleReminderEnabled,staleReminderDaysTier1/2/3(per entity) - Notification:
TICKET_STALE_REMINDER/PROBLEM_STALE_REMINDER/INCIDENT_STALE_REMINDER - Exclusions: terminal + per type (ticket: ON_HOLD/WAITING_CUSTOMER/SLA-paused/open incident link; problem: ON_HOLD/WAITING_VENDOR; incident: ON_HOLD/PENDING_CLOSURE)
Reopen Escalation
When an entity is reopened more often than the configured threshold (reopenCount ≥ reopenEscalationThreshold), the responsible people are alerted proactively — a quality signal. Recipients are the assignee plus group lead/manager, each only if they may see the record (as with SLA and stale reminders). If nobody eligible is found, the escalation walks up the management chain; if nobody is found there either, the audit entry REOPEN_ESCALATION_NO_RECIPIENT is written.
- Job:
lifecycle_reopen_escalation(daily ~03:00) - Threshold:
reopenEscalationThreshold(null = off; opt-in per entity) - Dedup:
reopenEscalatedCount— re-escalation only on each further reopen above the threshold - Notification:
REOPEN_ESCALATION(internal, not customer-facing)
Notifications
| Typ | Recipients | Note |
|---|---|---|
TICKET_REOPENED | agent / group / customer / followers | customer gets a generic variant WITHOUT the internal reason/note (redaction) |
INCIDENT_REOPENED | assignee / group / reporter | internal |
PROBLEM_REOPENED | agent / group / reporter | internal |
TICKET_AUTO_CLOSE_WARNING | customer primary | advance warning before auto-close |
REOPEN_ESCALATION | assignee + lead/manager (only with visibility of the record) | internal, quality signal |
TICKET_WC_RESOLVE_WARNING | customer primary | advance warning before WC-auto-resolve |
TICKET_STALE_REMINDER · PROBLEM_STALE_REMINDER · INCIDENT_STALE_REMINDER | assignee → lead → manager (only with visibility of the record) | internal, inactivity escalation |
No duplicate notification: A reopen is also a status change. Recipients receive only the *_REOPENED notification, not the generic status-change notification on top. Linked records are informed via the cascading system (LINKED_*_REOPENED). Cascading System
Fields & Analytics
Each entity carries closedAt, reopenCount, reopenedAt and lastReopenReasonCode. Every reopen creates a visible REOPENED timeline entry (reason + note) and an audit event. This feeds the reopen analytics:
| Endpoint | Permission | Purpose |
|---|---|---|
GET /api/analytics/reopen | reports.viewAll | viewTeam | KPI across all 3 types: everClosed/reopened/reopenRate/cumulativeReopens/top reasons |
GET /api/analytics/reopen/detail | reports.viewAll | viewTeam | time frame + breakdown per agent/group/category + monthly trend |
In the UI: the lifecycle dashboard at /lifecycle-analytics (reporting navigation) with entity tabs, time frame, summary cards, trend and ranked breakdowns; plus a compact reopen-rate widget on the main dashboard.
- Tickets API — reopen paths, merge redirect, auto-close
- Incidents API — POST /:id/reopen + approval flow
- Problems API — manual reopen (PATCH)
- Permissions & RBAC — reopen / reopenOverride
- CronJobs API — lifecycle_auto_close / lifecycle_wc_auto_resolve / lifecycle_stale_entity_reminder / lifecycle_reopen_escalation
- Settings API — reopen reasons + lifecycle config
- Cascading System — LINKED_*_REOPENED
- Notifications — *_REOPENED, REOPEN_ESCALATION