---
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://coevera.webplanet.sk/blueprints/one-entity-five-request-types.html
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 request type 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, who need five
structurally different things:

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

Previously all five arrived as email and phone calls to shared mailboxes: no reference
numbers, no way to answer "where is my request?" without asking a person, no cycle-time
data.

Requirements: one queue and one reference-number series; one status lifecycle; distributor
self-service scoped to own records; a conversation thread per request with a strict
internal/external split; an audit trail; and multi-department approval for new-product
requests only.

The tension is in the combination. Uniformity is wanted for the *queue*; divergence is
required in the *content*. A certification request needs a lot and heat number and has no
delivery date; a quote request needs repeating part lines a general question never has.

## 02 · Why the obvious approach fails

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

Fails on a structural point: **a dropdown value cannot select a form.** Coevera binds one
editable form layout per `CustomEntityType`, and no custom field's value can swap the
layout. So one form serves all five types, carrying the union of every type's fields — 42
here, of which about a dozen apply to any given request. Distributors get asked for a lot
number on a purchase order.

Two further consequences:

- **Mutability.** `typeId` is set at creation and is immutable — platform-enforced, no
  guard automation needed. A dropdown is editable, so a purchase order can silently become
  a certification request mid-life, invalidating every report built on it.
- **Redundancy.** Each record carries two answers to the same question — its real
  `typeId`, which exists whether used or not, and your dropdown — with nothing keeping
  them in agreement.

### Instinct two: five separate custom entities

Clean forms, but loses what the business asked for:

- five reference-number sequences instead of one shared series
- no single queue — "everything open for this distributor" becomes five list views
- five field pools to keep in step (status, severity, submitter, SLA target and more are
  common to all types), which then drift
- no in-place conversion: a general question that turns out to be a new-product request
  must be retyped into a different entity, losing history and reference number
- every report becomes a five-way union

**The distinguishing question.** Do these share a lifecycle and a queue, differing mainly
in fields carried? Sub-types of one entity. Do they have independent lifecycles that never
appear in the same list? Separate entities. Case management is firmly the former.

## 03 · Data model

One custom entity `Case`, five sub-types, two supporting custom entities, three built-in
extensions.

| 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 pool; each form selects its subset. 13 fields are common to all five and are
defined and maintained exactly once. Per-type distinctions — form selection, automation
triggers, saved views, API filters — address the type by `typeId` or `type.name` directly.

> **Trap worth knowing before you start.** Creating a custom entity automatically creates a
> default sub-type beneath it, same name as the entity, empty form. Add five of your own
> and users see **six** entries in the "+" menu, one a dead end. Instead host one sub-type
> *on* the auto-default: rename it, and create only N−1 new sub-types. Here "General
> Enquiry" lives on the auto-default.

### Supporting entities

| Entity | Why it exists | Sub-types |
|---|---|---|
| `RequestedPart` | Repeating part lines on a quote request; parent lookup 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 | 2 — *Note* (distributor-visible), *Internal Note* |

`Note` applies the same pattern at smaller scale: rather than a custom "is internal"
boolean, visibility is carried by `typeId`. Two sub-types, no flag to get wrong, automation
filters on type directly. Author and timestamp come from native owner/created fields.

### 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), domain attribute dropdowns, stock status whose "special order required" value marks a new-product-request candidate |

## 04 · Field-level configuration

| 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 |

Naming follows the platform convention: custom fields prefixed `cf_`; dropdown and lookup
fields carry an `_id` suffix because they resolve to option or record UUIDs rather than
literals; lookup fields named for the relationship. Field names here are anonymised; shapes,
types and relationships are as built.

### Per-role field permissions

Three roles, enforced at field level — None / Read / 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.** Resolution originally allowed distributor read
> access; changed to none, with a separate customer-visible summary added. 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 — beats one field written for two audiences.

## 05 · Automation & logic

Nine automation processes, all space-scoped. 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 narrows it to the applicable types — 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 changes do not re-stamp |
| 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, logs an internal note |
| Manual button | "Announce status change" — posts a visible note to submitter plus notify-contacts in one click |
| Daily schedule | Closes cases 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 submitting distributor remains record owner for the case's
whole life; the internal analyst is a separate lookup. The instinct is to reassign ownership
on acceptance — but ownership is what drives the distributor's access to their own record.
Reassigning it removes 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** — no fields can be added, so it cannot carry 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.

Only new-product requests need formal sign-off, from six departments. The working pattern 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 standard approval UI and audit
   trail.
4. The Quote carries a lookup back to the case; the platform maintains the reverse link.
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?". Opportunity-as-deal is the wrong framing.
- **No pipeline pollution.** Opportunity proxies 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 — no
  custom price field on the case needed.
- **Built-in expiry.** Quote has a native expiration date, useful for time-boxing approvals.

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; the
platform refreshes them when the Quote changes.

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

The requirement is one thread where some entries are distributor-visible and some strictly
internal. Built-in notes have no visibility dimension, so the thread became a custom entity
with two sub-types. Explicit cost: the case has both built-in notes *and* a custom thread,
and the built-in one must be kept off the forms to avoid two competing places to write.

### Free-text fields cannot drive distribution lists

Notify-contacts began as a text area for typing email addresses; replaced with a multi-select
lookup to Contact, because automation cannot reliably parse addresses out of free text. If a
field's value must 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 and AI-populated fields absent from a
  record's form layout silently do nothing — they neither compute nor error. Form membership
  is functional, not cosmetic.
- **Sequences cannot reset annually.** The native counter increments on creation with no
  yearly reset. A year can be composed in as a prefix, but the counter runs continuously.
- **Space-scoped processes need session-authenticated credentials.** Personal-access API
  credentials are rejected when writing space processes, 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.
- The audit-note process fires on every case update rather than only 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, 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 label. Three fields came out with names
  differing from spec; one had silently not been created at all. Neither visible from the UI.
- **Sub-type count and identity.** Confirmed five sub-types exist 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 configuration — logged in and
  confirmed fields that should be invisible to a distributor are actually absent.
- **Each process confirmed enabled, and its node graph read back.** Two were enabled but
  stopped at placeholder nodes — reporting success and doing 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.
