Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Issue Tracking & SLA Enforcement Architecture

A design reference for ticket lifecycle and service-level clock enforcement in a single-codebase ERP system

1. Requirements

1.1 Functional requirements

  • Track a customer-facing support Issue through a status lifecycle (Open, Replied, On Hold, Resolved, Closed) that any agent can move in either direction — no enforced forward-only path.
  • Let an administrator define named Service Level Agreements scoped to a document type (this document covers the Issue tracker), optionally narrowed to a customer, customer group, or territory, or to a boolean condition over the record’s own fields.
  • Resolve, per Issue, exactly one applicable Agreement: a specific (entity- or condition-matched) Agreement outranks a designated default, which is the fallback otherwise.
  • Let a Priority Tier (Low/Medium/High/… — administrator-named) inside an Agreement carry its own first-response and resolution durations, in seconds of support time, not wall-clock time.
  • Compute deadlines against a working-hours calendar — per-weekday support windows plus a holiday list — so a duration measured from Friday evening lands inside Monday’s window, not at a literal wall-clock offset.
  • Let an administrator designate, per Agreement, which status values pause the clock (issue is with the customer) and which fulfill it (issue is done) — two independently configurable lists, not a fixed two-state model.
  • Persist the derived response deadline, resolution deadline, and a four-value Agreement Status (First Response Due / Resolution Due / Fulfilled / Failed) on the Issue, recomputed on every save and whenever a message is linked to it.
  • Track hold time separately: intervals spent paused accumulate into a running total, and both deadlines are pushed out by it, so time waiting on the customer never counts against the support team.
  • Provide an escape hatch to reset an Agreement’s clock to “now” with a recorded reason, and to split one Issue’s later message history into a second Issue with its own fresh clock.
  • Support a separate Warranty Claim record for serialized-item warranty/AMC work, sharing customer/serial-number vocabulary but not the Agreement/clock machinery below.

1.2 Non-functional requirements

  • Extensibility of scope: one clock engine, reused across any document type with a status field, needs no new code to serve another record type.
  • No double-provisioning: enabling tracking on a type must add its fields exactly once; re-running setup must recognize existing fields rather than duplicate them.
  • Idempotent re-evaluation: replaying an unchanged status transition must not double-count hold time or re-fire the first-response comment.
  • Low ceremony for the common case: one clock policy for every Issue should need only a single default Agreement, no entity or condition configuration.
  • Auditability: every deadline recomputation, hold-time accumulation, and pause/resume transition must be traceable from fields already on the record.

1.3 Constraints

  • The Issue tracker is a plain, non-submittable record: nothing stops an agent from moving a resolved ticket back to open, or skipping “Replied” entirely.
  • The clock engine writes fields only where they exist on the target type, checking field presence first.
  • Deadline computation depends on a shared, general-purpose working-hours calculator outside this module — described here by contract, not internals.

2. High-Level Design

2.1 Component diagram

2.2 Data flow — resolving which Agreement applies

This is a short branching lookup rather than a wide graph, so it is written as steps:

  1. Guard: if the module’s tracking switch is off, no Agreement is applied and any previously-applied one is stripped.
  2. Candidate query: fetch every enabled Agreement for the record’s document type; if the record carries a Priority Tier, restrict to Agreements defining that tier.
  3. Split into two pools: the default pool (fallback-flagged Agreements) and the specific pool (every other enabled Agreement, matched on: an Agreement already set on the record, or the record’s own customer/customer-group-ancestors/territory-ancestors against each Agreement’s scope entity, or an Agreement with no scope entity at all).
  4. Condition filter: for each specific-pool candidate carrying a boolean condition (a short expression over the record’s own fields, run through a constrained evaluator), keep it only if the condition evaluates true; candidates with none pass automatically.
  5. Resolve: append the default pool to the filtered specific pool’s end and take the first entry. A specific, condition-passing Agreement always outranks the default — but if two specific Agreements both match, there is no coded tie-break beyond whichever the query returns first, in practice the most recently created, since Agreements are read newest-first by default.
  6. Apply: stamp the resolved Agreement and its default Priority Tier (if none set), then hand off to the clock engine (§3.2).

3. Deep Dive

3.1 Data model

Issue The tracked ticket. Carries a plain status enumeration (Open, Replied, On Hold, Resolved, Closed), the clock fields described below, and reference fields (customer, contact, an optional linked lead, project, and company). A record-splitting action can fork an Issue’s later message history into a brand-new Issue with a reset clock — useful when one ticket bundles two unrelated problems.

Priority and Issue Type Two independent, free-text label registries with no fixed shipped list. Priority drives which Priority-Time row inside an Agreement applies; Issue Type is purely descriptive unless referenced from a condition expression.

Service Level Agreement The configuration root, scoped to exactly one document type. Holds an enable flag, a default-fallback flag, an optional start/end validity window (a daily sweep disables any Agreement past its end date), an optional scope entity (a dynamic link to a customer, customer group, or territory), an optional condition expression, a resolution-tracking toggle (some Agreements track only first response), the Priority-Time table, the Pause and Fulfillment Status Lists, and the working-hours calendar.

Priority-Time row One row per Priority Tier: tier name, first-response duration, resolution duration (both in seconds), and a default-tier flag. Save-time validation rejects a duplicate tier name, a missing duration, a resolution duration shorter than the response duration, and requires exactly one default row.

Working-Hours Calendar A per-weekday list of support windows (duplicate weekdays rejected, start must precede end) plus a linked holiday list — the input to the shared working-hours calculator (§3.2).

Pause Status List / Fulfillment Status List Two child tables, each a bare list of status values, attached to one Agreement, with no fixed membership. The configuration form restricts offered choices to the tracked type’s own status options minus its natural start state (both lists) and, for the pause list only, minus its natural end state too — but which remaining values an administrator ticks is their call. For the Issue tracker the pause list may draw from {Replied, On Hold, Resolved} and the fulfillment list from {Replied, On Hold, Resolved, Closed}, in any combination — a genuinely open configuration, not a fixed two-state model. A status can sit on neither list, either, or in principle both, though the clock engine treats “hold” as outranking “fulfilled” when classifying it. A commonly used example pauses only on “Replied” and fulfills on “Resolved”/“Closed” — one permitted choice, not a hard rule.

Clock fields on the Issue (shipped directly on its schema rather than through the generic field-injection path used for other document types) Response deadline, resolution deadline, first-responded-on timestamp, an “on hold since” timestamp, a running hold-time total, and the four-value Agreement Status. One inconsistency worth flagging: the resolution field carries a distinct prefix the response field lacks — a residue of a schema rename that relabeled the original field. A retired branch of the record-splitting action still assigns to the pre-rename field name on its in-memory copy; since that name matches no real field, the assignment is silently inert rather than an error.

Warranty Claim A separate submittable record for serialized-item warranty/AMC service requests: customer, serial number, a warranty/AMC status fetched from the serial number’s own record, a status enum, and resolution fields. It shares no field, cross-reference, or code path with the Issue/Agreement machinery above — a sibling under the same support area, not a linked pair.

Support configuration A single settings record holds the tracking on/off switch, the permission gate for the clock-reset action, the auto-close window (zero disables it), and a small table of portal search-source definitions federating the customer-portal search box against external help-content APIs — unrelated to clock behavior, noted for completeness.

3.2 Algorithm — deadline computation and status classification

Once an Agreement and Priority Tier are resolved (§2.2), the clock engine runs three passes on every save and whenever a message links to the Issue:

Pass 1 — classify the status transition. The previous status (read from storage, not from the in-memory record) and the new status are each classified as paused (on the Pause Status List), fulfilled (on the Fulfillment Status List), or open (neither). The six meaningful category-to-category transitions each carry a distinct effect:

  • Entering Paused (from Tracking or Fulfilled): stamp “on hold since” and clear the resolution deadline (meaningless while paused); the response deadline clears only if no first response has landed yet.
  • Leaving Paused (to Tracking or Fulfilled): compute elapsed seconds since “on hold since” (or, if previously fulfilled, since the resolution timestamp) and add it to the running hold-time total; clear “on hold since”.
  • Entering Fulfilled (from Tracking, or from Paused when the Agreement tracks resolution): stamp the resolution timestamp and compute the elapsed resolution duration.
  • Any other transition is a no-op for the clock.
  • A transition out of the record’s true start status, either direction, also checks for a first response; if none yet, it stamps the first-response timestamp and computes the elapsed first-response duration.

Pass 2 — recompute deadlines. The response deadline is recomputed from the Agreement’s creation-linked start time plus the tier’s response duration, walked forward through the working-hours calendar, then pushed out by the hold-time total if no first response has landed. The resolution deadline is recomputed the same way from the resolution duration, but only if the Agreement tracks resolution and the record is not paused — it is cleared, not recomputed, while on hold.

Pass 3 — derive Agreement Status. With resolution tracking off, the label reduces to two outcomes off the response deadline alone (First Response Due, then Fulfilled/Failed by comparing the first-response timestamp to it). With it on, the label walks First Response Due → Resolution Due → Fulfilled/Failed by the same comparison against the resolution deadline.

Working-hours walk (behavior, not internals). Given a start timestamp and a duration in seconds, the shared calculator steps day by day, skipping holidays and non-support weekdays, consuming the duration against each day’s support window, and lands the result mid-window or, when the remaining duration spans a whole day or more, at that day’s window close — a quirk worth knowing if a deadline looks a few hours later than a naive walk-forward would suggest.

3.3 Adapter contract — extending clock tracking to another document type

A document type opts in by having an Agreement created against it; the engine requires only a status field (validated at Agreement save time) and — for every type besides the Issue tracker, which ships the fields natively — injects a fixed field set (Agreement link, response deadline, first-responded-on, hold tracking, Agreement Status, resolution deadline/timestamp) onto that type’s schema, checking for an existing field of the same name first so re-running setup is safe. The clock engine checks field presence before touching any clock field, so a type that opted out of one (say, hold tracking) degrades gracefully rather than erroring.

3.4 Error handling

  • Tracking disabled mid-flight: resolution is skipped outright, and any Agreement fields already present are stripped rather than left stale.
  • Malformed condition expression: validated at Agreement save time against a throwaway record; a condition that raises blocks the save rather than failing later against real records.
  • Duplicate default Agreement / scope entity: save-time uniqueness checks block a second claim on either.
  • Reopening a fulfilled record: an ordinary Fulfilled→Tracking/Paused transition (Pass 1) — hold time keeps accumulating correctly because the elapsed-time calculation falls back to the resolution timestamp when “on hold since” is empty.
  • Manual clock reset: gated behind its own permission switch; re-anchors the Agreement’s start time to now, with a recorded reason posted as a comment, and lets the next save recompute both deadlines — it does not touch hold time or status.
  • Late first response: recorded reactively — assigned agents are named in a comment once a late response finally lands; no proactive warning fires before the deadline, and nothing checks approaching deadlines ahead of time.
  • A documented custom-status extension point is unreachable and broken: a thin wrapper exists specifically so a site-level custom script can re-derive Agreement Status outside the two paths above, but it has no callers anywhere in the tree, and it calls the two-argument status-derivation function with only one argument — any script that actually used it would fail immediately with a missing-argument error.

4. Scale and Reliability

The response and resolution deadlines are persisted absolute timestamps, not values recomputed on read, so they do not drift with wall-clock time and a delayed reader still sees the correct deadline — sound regardless of scheduler health.

The important qualifier is what recomputes the derived Agreement Status. Two paths reach it in practice: the generic per-record validation firing on every save, and the message-linking hook firing when a communication attaches to the Issue (a third, site-custom-script entry point exists but is dead — see §3.4). No scheduled job walks open Issues and re-evaluates Agreement Status purely because time has passed. The two scheduled jobs touching this module do something else: a daily sweep disables Agreements past their validity window (configuration-level, unrelated to any one Issue’s clock), and a separate daily sweep auto-closes Issues sitting paused too long — which incidentally forces a save, and therefore a fresh recomputation, only on the records it touches.

The consequence: an Issue that breaches its resolution deadline and then receives no further edit and no further message keeps reporting a stale “Resolution Due” label indefinitely, not “Failed,” because nothing writes to it again to trigger recomputation. Any automation filtering on Agreement Status = Failed under-counts abandoned breaches. This differs from “a scheduled sweep runs late”: there is no sweep to be late — the label is entirely event-sourced off record writes, and an idle breached ticket has no event to source from.

  • Load pattern: deadline computation is cheap and runs inline with the save transaction, scaling with save volume, not open-ticket count.
  • Horizontal scaling: the clock engine holds no state between calls, so ordinary save-path scaling needs no special coordination.
  • Idempotency: replaying an unchanged status is a no-op in Pass 1 (all six branches require the previous and new classifications to differ), so duplicate saves are safe.
  • What genuine breach alerting needs: a new scheduled sweep evaluating deadlines against the current time independent of any save, or a query-time view comparing “now” to the stored deadlines directly, rather than relying on the stored label.

5. Trade-off Analysis

Decision Trade-off
Agreement Status is event-sourced off record writes, not swept by a scheduled job Cheap and always consistent with the record’s edit history, but a breached ticket nobody touches never flips to “Failed” — automation keyed on that label silently misses idle breaches.
Deadlines are absolute persisted timestamps rather than computed on read Immune to scheduler lag; any later reader gets the right answer without recomputation — at the cost of needing an explicit save whenever an input (priority, Agreement, hold time) changes.
Pause/fulfillment membership is a fully open per-Agreement list, not a fixed two-state model Maximizes flexibility across very different status vocabularies, but pushes correctness onto configuration — a status left off both lists silently behaves as “still tracking,” uncaught by validation.
Field injection checks for an existing field before creating one Safe to re-run Agreement setup with no destructive migration, but field definitions can drift from the canonical set if a target type’s own schema later changes independently.
No proactive pre-breach notification, only a reactive post-breach comment Minimal code (one comment, riding an existing notification path) instead of a bespoke alerting pipeline, but there is no early-warning signal — the first sign of trouble is the breach itself.
Issue tracking is a plain, non-submittable record with an unenforced status enumeration Simple to model and matches how support conversations actually move (reopens are common), but nothing stops a direct status write from bypassing the pause/fulfillment classification’s intended meaning.

6. What to Revisit as the System Grows

  • Add a genuine breach sweep. The highest-value addition is a scheduled process that evaluates open Issues’ stored deadlines against the current time independently of any save, so idle breached tickets surface without an incidental edit revealing them. Until it exists, reporting built on Agreement Status alone is a lower bound on breaches, not an exact count.
  • Validate pause/fulfillment list completeness at Agreement save time. A check flagging any status present on neither list, instead of silently treating it as “still tracking,” would catch a likely-unintentional configuration gap early.
  • Introduce proactive, threshold-based alerting ahead of the deadline itself, layered on the existing reactive missed-response comment, once ticket volume makes a purely reactive signal too late to be useful.
  • Reconcile the resolution field’s naming residue. The dead assignment to the pre-rename field name inside the record-splitting action is worth removing before a future rename makes it accidentally live again.
  • Revisit the tie-break among multiple matching specific Agreements once overlapping customer, customer-group, and territory scopes are plausible simultaneously — today’s implicit “most recently created wins” behavior is undocumented.
  • Formalize the Issue↔Warranty-Claim boundary, or connect them. The two record types today share only a support area and a customer vocabulary; if a workflow should link a warranty claim to a tracked Issue, that link would need to be built, not assumed.

Was this page helpful?