Cases
The customer support ticket — subject, priority, status, resolution and escalation.
Cases
A case is a customer support ticket. Every customer-reported issue, question or feature request lives as a case so the team can route it, work it, and resolve it consistently.
The 7 case statuses
| Status | What it means |
|---|---|
| ⚪ New | Just received, not yet assigned to an agent (default) |
| 🟠 In Progress | An agent is actively working on it |
| 🟡 Waiting on Customer | Pending the customer's response |
| 🔵 Waiting on Support | Pending another team (engineering, finance, etc.) |
| 🔴 Escalated | Bumped to a senior agent or management |
| 🟢 Resolved | Fix delivered, awaiting customer confirmation |
| 🟢 Closed | Case is done |
A typical resolved-quickly case: New → In Progress → Resolved → Closed. A typical stuck case: New → In Progress → Waiting on Customer → In Progress → Resolved → Closed.
The 4 priority levels
| Priority | SLA target (SMB → Strategic) | When to use |
|---|---|---|
| 🔵 Low | 7 days → 4 days | Cosmetic issue, low-impact question (default) |
| 🟠 Medium | 2 days → 24 hours | Standard issue, work continues |
| 🔴 High | 8 hours → 6 hours | Significant impact, customer is upset |
| 🔴 Critical | 4 hours, every tier | Production down, immediate response needed |
Every row has a clock behind it now. The case_sla_defaults hook stamps sla_due_date on every case from a priority × account-tier matrix, so the target tightens for a Strategic or Enterprise customer and the hourly breach sweep applies at all four priorities — see SLA & Escalation for the full table and the mechanics. Those are calendar hours: this app has no business-hours calendar, so nights, weekends and holidays count against the target.
When a case is set to Critical, the Case Escalation flow alerts the case owner by inbox message and email — its recipient list is the single entry {caseRecord.owner_id}, and there is no support_manager@example.com recipient anywhere in this app. A High case is never escalated automatically, whatever the account is: no escalation condition reads the account at all, and raising one is the manual Escalate Case button.
Case types
| Type | Use for |
|---|---|
| Question | How-to enquiries |
| Problem | Something isn't working |
| Feature Request | Customer is asking for new functionality |
| Bug | Confirmed software defect |
Case origins
Where the case came from: Email · Phone · Web · Chat · Social Media.
This drives reporting (which channels generate the most volume) and is auto-filled when the case is created from an inbound channel (e.g., the email-to-case integration sets origin = Email).
What a case record stores
crm_case declares 27 fields (src/objects/case.object.ts), and three different things organise them: the object's own field groups, the case detail screen, and the case form. Only the first of the three has six sections, and no two of them agree — so it is worth knowing which one you are being shown.
The object's field groups
The six names below are real, but not one of them is a screen: they are crm_case's field groups (fieldGroups), the object's own filing scheme for its fields. Neither the detail screen nor the form renders them — each declares sections of its own, listed below. This is, though, the one list that accounts for every field the object has:
| Field group | Fields |
|---|---|
| Case Information | Case Number, Subject, Display Title, Description, Account, Contact, Status, Case Type |
| Origin & Routing | Case Owner, Case Origin |
| SLA & Priority | Priority, Created Date, Closed Date, First Response Date, Resolution Time (Hours), SLA Due Date, SLA Violated |
| Resolution | Resolution, Resolved by Article, Customer Satisfaction, Customer Feedback |
| Escalation (collapsed by default) | Escalated, Escalated Date, Escalation Reason |
| System (collapsed by default) | Internal Notes, Is Closed |
Five of those six rows used to be written differently on this page, and the differences were not cosmetic: Status and Case Type are in Case Information, not SLA & Priority; Closed Date is in SLA & Priority, not Resolution; and System holds Internal Notes and Is Closed — there is no audit trail field on a case at all, and the timestamps sit in SLA & Priority. Only Escalation was right as it stood. Priority Rank, the hidden sort key that orders My Open Cases, is in no group at all.
The detail screen
The Details tab of the case detail page (src/pages/case_detail.page.ts) declares three sections, holding 16 of the 27 fields:
| Section | Fields |
|---|---|
| Case Information | Case Number, Subject, Account, Contact, Case Type, Case Origin |
| Status & SLA | Status, Priority, Case Owner, Escalated, Escalation Reason, SLA Due Date, SLA Violated, Resolution Time (Hours) |
| Description (collapsible) | Description, Resolution |
Description is the only one of the three that can be collapsed, and nothing marks it collapsed to begin with, so it opens along with the rest.
The form
The case form (src/views/case.view.ts) is tabbed, and has three sections of its own — different names, different contents, 22 of the 27 fields:
| Form tab | Fields |
|---|---|
| Case | Case Number, Subject, Account, Contact, Status, Priority, Case Origin, Case Owner, Description |
| SLA | Created Date, First Response Date, SLA Due Date, Resolution Time (Hours), SLA Violated, Escalated, Escalation Reason |
| Resolution | Resolution, Internal Notes, Customer Satisfaction, Customer Feedback, Closed Date, Is Closed |
The two are not one screen wearing different chrome. Case Type is on the detail screen and nowhere else — the form carries it in no tab. The public support form at /forms/support is a third layout again: a single section with four fields (subject, description, type, priority).
What happens automatically
- First response date (
first_response_date) — stamped the first time an interaction that already took place is recorded against the case. Its single writer is theevent_activity_bubblehook (src/objects/event.hook.ts), which fires on anycrm_eventreaching Held status with the case as its related record — so Log a Call and Log a Meeting stamp it, and so does an interaction entered any other way, including straight onto the case's activity list. It reads the stored value back before writing, so a second interaction never overwrites the first. Two things are deliberately not a first response: a status change (an agent can move a case to In Progress and investigate for an hour while the customer hears nothing) and a meeting merely scheduled, which isplannedrather than held. On a case worked entirely through comments the field stays empty. - Closed flag — set automatically when status changes to Closed.
- Closed date — stamped to NOW when status changes to Closed.
- Resolution time (hours) — calculated as the difference between Created date and Closed date.
- SLA due date — stamped on every case with a recognised priority.
case_sla_defaultslooks up the case's priority against its account's Customer Tier and fills the field with now + the matching number of calendar hours, when a case is created and whenever the field is still empty. It is written once and never recomputed, and a date somebody typed in by hand is never overwritten. - SLA Violated (
is_sla_violated) — there is no field called SLA Breached?. The hourlycase_sla_monitorsweep sets it to true on cases that are still open (status is neither Resolved nor Closed) and whose SLA Due Date has already passed. It never compares resolution time against the target, so a case with no due date is never a candidate, and a case resolved late but before the next sweep is never flagged. - Account's last activity date — bumped to today when the case is updated.
Built-in rules
- Closed cases must have a resolution — you can't close a case with an empty Resolution field.
- Escalated cases must have an escalation reason — explains why a senior agent was pulled in.
- Status progression is advice, not a gate — the transition table says New leads to In Progress, Waiting on Customer, Escalated or Closed, so jumping straight to Resolved is outside it. That does not stop the save: the case is stored as Resolved and the engine writes one line to the server log,
WARN Validation rule 'case_status_progression' (warning): Invalid status transition. Unlike the two rules above — botherrorseverity, both of which do reject the save — this one only advises. See State Machines for what the table is good for.
Flow and hook automation
- Notify on Critical — when priority is set to Critical, the Case Escalation flow's
notifynode sends an inbox message and an email to the case owner alone: its recipient list is the single entry{caseRecord.owner_id}. There is nosupport_manager@example.comrecipient anywhere in this app. - Notify on Escalation — when the status changes to Escalated, no email goes out at all:
escalation_team@example.comexists nowhere in this app, and neither does an escalation-team recipient. What the escalated status fires is thecase_status_side_effectshook, which opens an Urgent follow-up task due tomorrow for the account owner — and only for cases that have an account, since the task is keyed to it. - Calculate resolution time — automatic on close.
- Auto-set closed date and flag — on status change.
Case escalation
The Case Escalation flow runs on exactly one condition: the case is at Critical priority. There is no High-priority branch, and no escalation condition reads the account — a Customer's High case and a prospect's High case are treated identically: neither escalates on its own, and raising one is the manual Escalate Case button.
It performs:
- Flag as escalated, stamp the escalated date.
- Set status to Escalated.
- Hand the case over — ownership moves to the service manager carrying the fewest open cases.
- Notify the case owner — inbox and email, carrying the case number and the priority.
The hand-off goes to a pool, not up a chain. Positions in this app are flat, so "the owner's manager" is not something it can resolve; the case goes to whoever holds the Service Manager position and has the fewest open (neither Resolved nor Closed) cases, which spreads consecutive escalations across the team on its own. Three cases where it deliberately does nothing: nobody holds the position — the state a fresh install is in — so the case keeps its owner and the escalation completes exactly as before; the case is already owned by a pool member, so it stays with the manager working it; or the same edit names an owner itself, which wins over the pool.
The status change to Escalated then fires the case_status_side_effects hook, which opens an Urgent follow-up task due tomorrow for the account owner — and only for cases that have an account, since the task is keyed to it.
Two things this list deliberately does not claim, because the escalation does not do them:
- The notification goes to the previous case owner alone. Its recipient list is the single entry
{caseRecord.owner_id}, read before the hand-off — so it reaches the agent the case is being taken from, which is the point. Not the manager, and not a broadersupport-team@example.comlist, an address that exists nowhere in this app. - The follow-up task is not the original agent's. It belongs to the account owner, not to the agent who was handling the case and not to a senior agent. The flow carries no task node at all — the hook is the single owner of escalation follow-up tasks, so there is exactly one task per escalation.
See SLA & Escalation for the full mechanics.
Sharing — who can see which case
- By default, cases are private — only the owner. Positions are flat and nothing rolls up, so no manager sees a case by sitting above its owner.
- Critical open cases are auto-shared by two rules, one per rung: Escalated Cases Sharing grants read/edit to
service_manager, and Escalated Cases — Service Director grants read toservice_director. Both rungs are named explicitly because neither inherits from the other.
Field-level security
- SLA Violated (
is_sla_violated) and Resolution time are read-only for agents — these are auto-calculated and can't be hand-fudged. The lock is field-level security: the Service Agent profile masks both as readable but not editable.
Standard list views
crm_case ships eight views (src/views/case.view.ts), and the case list surfaces them as tabs:
| View | What it shows | Tab |
|---|---|---|
| All Cases | Every case, sorted by priority then SLA due date, with a row tint keyed on priority | All |
| Service Workflow | Kanban of open cases, grouped by status | Workflow |
| SLA Calendar | Cases laid out on their SLA Due Date — every case carries one now, so the calendar shows the whole open backlog | SLA |
| Case Timeline | Cases on a created → closed timeline, grouped by owner | Timeline |
| Escalated Cases | is_escalated is true | Escalated |
| Unassigned — triage | Open cases with no owner at all, urgent first then soonest due — where a web-to-case submission lands when nobody holds the Service Agent position | Triage |
| ⏰ SLA at Risk | Open High and Critical cases, soonest due date first | SLA at Risk |
| My Open Cases | Your cases that are not closed | My Cases |
Six names this section used to list are not views at all:
- Critical Cases and Breached SLA are metric tiles, not lists. Service Overview — the dashboard's own title is Customer Service — carries Critical Cases (open and Critical) and SLA Violations (
is_sla_violatedis true); no list view in this app filters on a breach. For a list you can actually work through, use Escalated Cases — the SLA sweep escalates every case it flags, so each breach lands there. - Service Board is not this app's name for anything. The kanban is Service Workflow, on the Workflow tab.
- Cases Due Today, Recently Closed and By Account do not exist in any form: no view filters on today's due date or on
closed_date, andcrm_casehas no grouped view.
The case detail layout
When you open a case, you'll see:
- Header — the case number and subject as the title (
{case_number} · {subject}), the account as the subtitle, plus an icon, a breadcrumb and the action buttons. That is the whole header: there is no status badge, no priority badge and no SLA countdown on it. Nothing in this app computes time remaining or time over — not here, not anywhere else on the case — so there is no countdown to render. See SLA & Escalation for what the two SLA fields do instead. - Key Information — the highlights strip directly under the header, and where Status and Priority actually live: ordinary fields rather than badges, side by side with SLA Due Date, SLA Violated, Owner and Account.
- Status path — a Salesforce-style horizontal flow showing New → In Progress → Waiting on Customer → Escalated → Resolved → Closed, so an agent can see at a glance where the case stands and what the next stop is.
- Customer panel — there is no such panel. The page (
src/pages/case_detail.page.ts) declares two regions and nothing else: a header holding the three components above, and a main region holding the tab strip below. What customer context there is sits in those: the account appears three times over — the header subtitle, the Key Information strip, and a field in the Details tab — and the primary contact is a field in that same tab's Case Information section. Contract tier and open cases this month are on no case surface at all. A case links to no contract, andcrm_contractcarries no tier field either; the nearest real field is Customer Tier on the account record, which this page does not show. Nothing in this app counts a customer's cases by month. - Three tabs:
- Details — the case description and resolution in full, but not "all metadata fields": 16 of the object's 27, in the three sections listed under What a case record stores above. The eleven that are not on the tab are First Response Date, Created Date, Closed Date, Escalated Date, Is Closed, Customer Satisfaction, Customer Feedback, Internal Notes, Priority Rank, Display Title and Resolved by Article. Seven of them are on the case form instead; the remaining four are on no case screen at all — Escalated Date is written by the escalation flows and displayed nowhere, Priority Rank is the sort key behind My Open Cases, Display Title is the object's
nameField, the title formula that names a case wherever one is referenced rather than a field you read here, and Resolved by Article is read by the deflection measures rather than by a screen. First Response Date is the omission worth knowing about: What happens automatically above explains at length how it gets stamped, and this is not the tab you can read it on. - Related — one list, not four: Open Tasks — the
crm_taskrecords pointing at this case through Related Case (related_to_case), filtered to those whose status is not Completed, ten at a time. That is the whole tab. There is no attachments list: files can be attached to a case (the object enables them), but no component on this page lists them. There is no linked-opportunity list, becausecrm_casehas no opportunity relationship in either direction. And there is no milestone list — the case's three milestones, escalated, resolved and closed, are emitted as entries in the Activity timeline, not as records here. - Activity — a unified timeline (more on this below).
- Details — the case description and resolution in full, but not "all metadata fields": 16 of the object's 27, in the three sections listed under What a case record stores above. The eleven that are not on the tab are First Response Date, Created Date, Closed Date, Escalated Date, Is Closed, Customer Satisfaction, Customer Feedback, Internal Notes, Priority Rank, Display Title and Resolved by Article. Seven of them are on the case form instead; the remaining four are on no case screen at all — Escalated Date is written by the escalation flows and displayed nowhere, Priority Rank is the sort key behind My Open Cases, Display Title is the object's
- AI Reference Rail — there is no rail on this page, of any kind. The main region holds the tab strip and nothing else, and not one component on the page is AI-driven. The service skills are real — see How the AI assistant helps below — but you reach them by asking, not from a panel beside the case. The one reference rail this app does render sits on the opportunity detail page, and even there it lists related records (quotes, products, open tasks) rather than suggestions.
The Activity timeline
The Activity tab is the single source of truth for what has happened on a case. It combines four streams into one chronological feed:
| Stream | Where it comes from | What you'll see |
|---|---|---|
| Comments | A teammate or customer posts on the case | Threaded discussion, @mentions, internal/external visibility |
| Logged calls | "Log a Call" action — stored as sys_activity of type completed | Call subject, duration, outcome, who logged it |
| Outbound emails | "Send Email" action — queued through sys_email | Subject, recipient, delivery status (queued / sent / bounced) |
| Field changes | Every change to a tracked field on the case | What changed, who changed it, when, before → after |
You don't have to configure any of this — every CRM object ships with trackHistory: true and the platform feeds engine wires comments, activities and audit log into the same timeline automatically.
Tip — if you only want to see the discussion, use the filter dropdown at the top of the Activity tab; you can also subscribe to the case from there so any future post pings you in the bell.
How the AI assistant helps
The service skills auto-activate on any case. Ask:
- "Triage this case" — Case Triage skill recommends a priority based on subject and description.
- "Suggest a resolution" — no one skill does this end to end. Case Triage declares two tools,
describe_objectandget_record, and no query tool at all, so it searches nothing. Ask Customer 360° for the published knowledge articles matched on the case's category or tags, then Email Drafting for the customer reply — the draft is grounded in the contact and account records, not in those articles, so paste in whatever it should quote. - "Customer 360" — pulls the full account context (open opportunities, past cases, contract tier).
If the Copilot decides the priority is Critical, it will also recommend immediate escalation and draft an internal notification message.
Tips for service agents
- ✅ Triage incoming cases promptly — but know that nothing in the app is watching. There is no first-response target to breach: First Response Date is recorded on the case and never compared against anything,
case_metricscarries no first-response measure so no report or dashboard tile reports on it, and no flow alerts on it. Promptness here is the team's own commitment, not a clock the app runs. - ✅ Use "Waiting on Customer" when you're genuinely blocked — but it does not stop the SLA clock, in any configuration.
sla_due_dateis written once and never recomputed, and the hourlycase_sla_monitorsweep collects open cases whose status is neither Resolved nor Closed — Waiting on Customer is not on that exclusion list. A case parked on the customer runs its clock down and is flagged SLA Violated and escalated on schedule, at every priority. - ✅ Always write a resolution — it's mandatory, but also feeds the knowledge base for future cases.
- ✅ Link a task when the case requires future follow-up (e.g., "verify fix worked in 7 days").
- ❌ Don't close a case without confirming the fix worked.
Tips for service managers
- ✅ There is no Breached SLA view to watch, and no red badge either — the only colour rule on a case list is All Cases' row tint, and it is keyed on priority, not on a breach. Work Escalated Cases daily instead, and read the SLA Violations tile on Service Overview for the count.
- ✅ Use the kanban for daily standups — it is named Service Workflow and reaches the case list as the Workflow tab.
- ✅ Coach agents whose Resolution Time trends up — but you will be reading it off the cases themselves. There is no agent leaderboard:
case_metrics, the dataset behind every service report and dashboard widget, declares Status, Priority, Origin, Type and Created as its only dimensions, so nothing in analytics can group cases by agent.
Tips for admins
- The SLA targets per priority and account tier are stored in the case object's automation. To change them, edit
src/objects/_case-sla.tsand the matching table insidecase_sla_defaultsinsrc/objects/case.hook.ts— both, because a sandboxed hook body cannot import a constant — see SLA & Escalation and Administration › Automation. - Neither
escalation_team@example.comnorsupport_manager@example.comis configured anywhere — both addresses exist nowhere in this app. The one recipient list that does exist is thenotifynode of the Case Escalation flow,recipients: ['{caseRecord.owner_id}']— the case owner alone — and it is edited insrc/flows/case-escalation.flow.ts, not on a Setup screen. - Email-to-case ingestion (auto-create case from an inbound email) requires the email plugin — see Customization › Extending Objects.