---
title: "How do you run five different kinds of customer request through one help desk?"
blueprint: 001
slug: one-entity-five-request-types
category: Data model
revised: 2026-09-02
platform: Coevera CRM (formerly Pipeliner CRM)
canonical: https://blueprints.coevera.com/blueprints/one-entity-five-request-types/
publisher: Coevera
customer_data: none
---

# How do you run five different kinds of customer request through one help desk?

**Short answer.** Model **one custom entity** for the case, and each request type as a
**CustomEntityType** sub-type beneath it. Coevera's native `typeId` field — a foreign key on every
record pointing at its entity-type — is the discriminator, and it natively drives form selection,
process filtering and saved views. Define every field once on the parent entity; each sub-type's
form picks the subset it needs.

The one thing that will reshape your design: **approval processes cannot target a custom entity**,
and the Approval record takes no custom fields. If any of your request types needs multi-party
sign-off, that runs on a proxy Quote — see §6.

## 01 · The business problem

A manufacturer sells through a network of independent distributors. Those distributors need things
from the manufacturer constantly, and the requests are not all the same shape:

- A **purchase order** to be processed against agreed pricing
- A **quote request** for a list of parts, with quantities and a need-by date
- A **new-product request** — something not in the catalogue, which needs engineering feasibility
  and sign-off from six internal departments before a price can be given
- A **material certification request** for a specific production lot, to satisfy an end customer's
  compliance file
- A **general question or complaint** that fits none of the above

Before the project, all five arrived as email and phone calls to shared mailboxes. Nothing had a
reference number, nobody could answer "where is my request?" without asking a person, and there
was no way to see how long anything had taken.

What the business asked for was one help desk, with:

- a single queue and a single reference-number series, so any request can be quoted in an email;
- one status lifecycle everyone understands, from new through to solved;
- distributor self-service — submitters see their own requests and nobody else's;
- a conversation thread per request, with a strict split between what the distributor sees and
  what stays internal;
- an audit trail of who changed what, when;
- and, for new-product requests only, a formal multi-department approval.

The tension is entirely in the combination. Uniformity is wanted for the *queue*. Divergence is
required in the *content*: a certification request needs a lot number and a heat number, and has
no use for a delivery date; a quote request needs a repeating set of part lines, which a general
question never has.

## 02 · Why the obvious approach fails

There are two first instincts, and both are wrong in a way that costs a rebuild.

### Instinct one: one entity plus a "Request Type" dropdown

This is the reflex, because a dropdown is the cheapest thing to create. It fails on a specific,
structural point: **a dropdown value cannot select a form.**

Coevera binds one editable form layout to each `CustomEntityType`. There is no mechanism for a
custom field's value to swap the layout. So with a dropdown you get exactly one form for all five
types, carrying the union of every type's fields — in this build that is 42 fields, of which
roughly a dozen apply to any given request. Distributors are asked for a lot number on a purchase
order. The form cannot be made to make sense.

Two further consequences, less obvious but just as damaging:

- **Mutability.** `typeId` is set at creation and is immutable — the platform enforces it, no
  guard automation required. A dropdown is editable by anyone with field access, so a purchase
  order can silently become a certification request halfway through its life, invalidating every
  report built on it.
- **Redundancy.** Every record would then carry two answers to the same question — its real
  `typeId` (which exists whether you use it or not) and your dropdown — with nothing keeping them
  in agreement.

### Instinct two: five separate custom entities

The opposite reflex: if the five types are genuinely different, model them separately. This
produces clean forms, and loses everything the business actually asked for:

- **Five reference-number sequences** instead of one shared series
- **No single queue.** "Show me everything open for this distributor" becomes five list views a
  human has to mentally merge
- **Five field pools to keep in step.** Status, severity, submitter, SLA target and a dozen more
  are common to all types; now they exist five times and drift
- **No in-place conversion.** A general question that turns out to be a new-product request cannot
  become one — it has to be retyped into a different entity, losing its history and its reference
  number
- **Every report is a five-way union,** and every new type makes it a six-way union

> **The distinguishing question.** Do these things share a lifecycle and a queue, and differ
> mainly in which fields they carry? Then they are sub-types of one entity. Do they have genuinely
> independent lifecycles that never appear in the same list? Then they are separate entities. Case
> management is firmly the former.

## 03 · Data model

One custom entity, `Case`, with five `CustomEntityType` records beneath it. Two supporting custom
entities, and three extensions to built-in entities.

### The case entity and its sub-types

| Element | Value |
|---|---|
| Custom entity | `Case` |
| Name field | Case Number |
| Sequence | `HD{Year}{Number6}` → `HD2026000001` |
| Functionalities | Activities, Documents, Notes, Global Search |
| Sub-types | Purchase Order · Quote Request · New Product Request · Certification Request · General Enquiry |
| Discriminator | native `typeId` — FK to the entity-type record. No custom field. |

All 42 custom fields are created **once**, against the parent entity. The five sub-types share
that single field pool; each one's form definition selects the subset relevant to it. This is the
property that makes the whole pattern pay off — 13 fields are common to all five types and are
defined and maintained exactly once.

Everywhere a per-type distinction is needed — form selection, automation triggers, saved views,
API filters — the type is addressed by `typeId` or `type.name` directly. No wrapper, no custom
field, no synchronisation logic.

> **Trap worth knowing before you start.** Creating a new custom entity automatically creates a
> default sub-type beneath it, with the same name as the entity and an empty form. If you then add
> five sub-types of your own, users see **six** entries in the "+" menu, one of which is a dead
> end.
>
> Instead, host one of your sub-types *on* the auto-created default: rename it to that sub-type's
> name, and create only N−1 new sub-types. Here, "General Enquiry" lives on the auto-default and
> the other four are created explicitly.

### Supporting entities

| Entity | Why it exists | Sub-types |
|---|---|---|
| `RequestedPart` | Repeating part lines on a quote request. A parent lookup back to `Case`, rendered as an inline grid on the quote-request form only. | 1 |
| `Note` | The case conversation thread, with two visibility tiers. See §6 — the built-in notes could not express this. | 2 — *Note* (distributor-visible) and *Internal Note* |

The `Note` entity is the pattern applied a second time, at smaller scale: rather than a custom "is
internal" boolean, visibility is carried by `typeId`. Two sub-types, no flag to get wrong, and
automation filters on the type directly. Author and timestamp come from the native owner and
created fields — no custom fields needed for either.

### Built-in entities, extended

| Entity | Added |
|---|---|
| Account *(the distributor)* | Tier, region, territory, parent-distributor lookup |
| Contact *(the submitter)* | Distributor role, help-desk group |
| Product | Manufacturer part number (unique, global search), plus domain attribute dropdowns and a stock-status field whose "special order required" value marks a part as a new-product-request candidate |

## 04 · Field-level configuration

42 custom fields on `Case`, distributed as 13 common plus a per-type tail:

| Group | Fields | Examples |
|---|---|---|
| Common to all five types | 13 | Status, severity, submitter contact, distributor account, assigned analyst, notify contacts, description, resolution, customer-visible summary, SLA target, hours worked |
| Purchase Order | 5 | PO number, expected delivery date |
| Quote Request | 4 | End customer, market area, currency, need-by date |
| New Product Request | 12 | Technical specification, feasibility notes, approval-proxy lookup, approval-status rollups |
| Certification Request | 5 | Lot number, heat number, certification type |
| General Enquiry | 3 | Enquiry category, referral source |

Field naming follows the platform's own convention — custom fields are prefixed `cf_`, dropdown
and lookup fields carry an `_id` suffix because they resolve to option or record UUIDs rather than
literals, and lookup fields are named for the relationship they express. Field names in this
blueprint are anonymised; the shapes, types and relationships are as built.

### Per-role field permissions

Three roles, and the visibility split is enforced at field level rather than by hiding things in
the UI: **None**, **Read**, or **Full** per field per role.

| Field | Distributor | Analyst | Manager |
|---|---|---|---|
| Customer-visible summary | Read | Full | Full |
| Resolution *(internal narrative)* | None | Full | Full |
| Hours worked | None | Full | Full |
| Assigned analyst | None | Read | Read |

> **Design note that emerged in use.** The resolution field originally allowed distributor read
> access. It was changed to no access, with a separate customer-visible summary added alongside
> it. The reason: when analysts know the customer can read a field, they self-censor, and the
> internal record loses the technical detail that makes it useful later. Two fields — one
> unfiltered, one written deliberately for the customer — works better than one field written for
> two audiences.

## 05 · Automation & logic

Nine automation processes, all space-scoped. The organising rule: **one process per concern,
filtered by `typeId`.** Because all five sub-types share one entity, each process is defined once
and a filter node early in the graph narrows it to the types it applies to — rather than five
near-identical copies.

| Trigger | What it does |
|---|---|
| Case created | Creates a distributor-visible note, "Case created by <user>", opening the thread |
| Case updated | Writes an internal note "<actor> changed status to <status>" as audit trail |
| Case updated → status enters an active state *and* assigned-analyst is empty | Stamps the acting user as assigned analyst. The empty-field test is a one-shot guard, so later status changes do not re-stamp it |
| Note created | Resolves recipients through the case's lookups, then chains to a second process that sends the email |
| Case created, filtered to new-product-request | Spawns the approval proxy Quote — see §6 |
| Quote updated, filtered to approval decision | Writes the decision back to the case and logs an internal note |
| Manual button | "Announce status change" — posts a visible note to submitter plus everyone on notify-contacts in one click |
| Daily schedule | Closes cases that have been solved and untouched for 14 days |

### Two patterns worth lifting

**The one-shot guard.** To capture "whoever first accepted this" without re-stamping on every
later update, filter on the target field being empty and set it to the acting user. The condition
makes the write idempotent — no separate "has this run?" flag.

**Ownership stays put.** The distributor who submitted the case remains the record owner for its
whole life; the internal analyst is captured in a separate lookup field. The instinct is to
reassign ownership to the analyst on acceptance — but ownership is what drives the distributor's
access to their own record. Reassigning it would remove the submitter's visibility of their own
request.

## 06 · Limits & trade-offs

### Approval processes cannot target a custom entity

> **Platform limit.** `ApprovalProcess` can only be triggered by or linked to **Account, Contact,
> Lead, Opportunity or Quote**. And the `Approval` record itself is **not customisable** — you
> cannot add a field to it, so you cannot give it a link back to your custom entity.
>
> Schema introspection does not reveal this. The field-creation mutation accepts an entity name as
> a plain string, so the call looks valid and is rejected at runtime. Worth knowing before you
> design around it.

Only one of the five types — new-product requests — needs formal sign-off, from six departments.
The pattern that works is a **proxy on Quote**:

1. A new-product-request case is created.
2. A process spawns a linked Quote under a dedicated quote type reserved for approvals.
3. The approval process runs on that Quote, natively, with the standard approval UI and audit
   trail.
4. The Quote carries a lookup back to the case; the platform maintains the reverse link
   automatically.
5. On decision, a second process writes the outcome back to the case status.
6. The Quote can be archived afterwards.

**Why Quote and not Opportunity**, given both can host an approval:

- **Semantic fit.** A new-product request *is* a pricing decision — "can we make this, and at what
  price?". Quote models that. Opportunity-as-deal is the wrong framing.
- **No pipeline pollution.** Opportunity proxies would appear in the deal pipeline and contaminate
  forecasting and revenue reports. A Quote sits in its own quote type, out of the way.
- **The price is captured natively.** The Quote's line items *are* the proposed price — so no
  custom price field on the case is needed at all.
- **Built-in expiry.** Quote has a native expiration date, useful for time-boxing an approval
  request.

Two rollup fields on the case surface the proxy's state — approval status and rejection reason —
derived from the linked Quote rather than stored. No write-back logic needed for those; the
platform refreshes them when the Quote changes.

### Built-in notes could not express two-tier visibility

The requirement is a single thread where some entries are visible to the distributor and some are
strictly internal. The built-in note functionality has no visibility dimension, so the thread
became a custom entity with two sub-types. A cost worth being explicit about: the case therefore
has both built-in notes *and* a custom note thread, and the built-in one has to be kept off the
forms to avoid two competing places to write.

### Free-text fields cannot drive distribution lists

The notify-contacts field began as a text area for typing email addresses. It was replaced with a
multi-select lookup to Contact, because automation cannot reliably parse addresses out of free
text to build a recipient list. If a field's value needs to be *acted on* rather than only read,
it has to be a typed reference.

### Other constraints hit in this build

- **A field must be on the form to work.** Calculated fields and AI-populated fields that are
  absent from a record's form layout silently do nothing — they do not compute and they do not
  error. Form membership is functional, not cosmetic.
- **Sequences cannot reset annually.** The native sequence counter increments on creation and has
  no yearly reset. A year can be composed into the number as a prefix, but the counter itself runs
  continuously.
- **Space-scoped processes need session-authenticated credentials.** Personal-access API
  credentials are rejected when writing space processes, even though they can read them.

### Known gaps in this build

Reported rather than omitted:

- The approval write-back handles the approved branch only. A rejected proxy leaves the case in
  its working state with no automatic propagation — a parallel filter is needed for the rejected
  outcome.
- The audit-note process fires on every case update rather than only on status changes; it needs a
  changed-fields condition to stop being noisy.
- SLA target dates are set manually. Deriving them from severity was designed and deliberately
  deferred.

## 07 · Verification

Sub-type designs fail quietly — a form that renders, a process that reports success, and a
discriminator wired to the wrong thing. What was actually checked:

- **Field inventory read back per entity.** Every field re-read from the API after creation and
  reconciled against the spec by API name, not by label. Three fields had come out with names
  differing from the spec, and one had silently not been created at all — neither visible from the
  UI.
- **Sub-type count and identity.** Confirmed five sub-types exist under the case entity and no
  orphan sixth — the auto-default trap from §3 shows up exactly here.
- **Discriminator immutability.** Attempted to change `typeId` on an existing record and confirmed
  the platform refuses.
- **Per-role field access, tested as each role.** Not read from the configuration — logged in and
  confirmed the fields that should be invisible to a distributor are actually absent.
- **Each process confirmed enabled, and its node graph read back.** Two processes were enabled but
  stopped at placeholder nodes — they reported success and did nothing. This is the check that
  found the approval write-back gap.
- **One end-to-end pass per sub-type** in the UI: create, advance through the lifecycle, confirm
  notes and emails land, confirm the distributor view shows only what it should.

> **The general rule.** A mutation returning success means the write was accepted, not that the
> behaviour is correct. Read the state back, and check the result as a user of each role, before
> calling any phase done.

**What would signal a regression:** cases appearing with an empty or wrong sub-type; the
assigned-analyst field changing after first acceptance; notes reaching a distributor that were
written as internal; new-product requests with no linked approval proxy.

---

Published by Coevera. Abstracted to the reusable pattern — no customer names, no client
data, no personal data.
