Blueprint 001 · Data model

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

Orders, quote requests, new-product requests, certification requests and general questions. One queue, one reference number, one status lifecycle — but five completely different sets of questions to ask.

Revised 2026-09-02 Verified against a live Coevera space Markdown twin ↓

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

01The 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.

02Why 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.

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

ElementValue
Custom entityCase
Name fieldCase Number
SequenceHD{Year}{Number6}HD2026000001
FunctionalitiesActivities, Documents, Notes, Global Search
Sub-typesPurchase Order · Quote Request · New Product Request · Certification Request · General Enquiry
Discriminatornative 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

EntityWhy it existsSub-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

EntityAdded
Account (the distributor)Tier, region, territory, parent-distributor lookup
Contact (the submitter)Distributor role, help-desk group
ProductManufacturer 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

04Field-level configuration

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

GroupFieldsExamples
Common to all five types13Status, severity, submitter contact, distributor account, assigned analyst, notify contacts, description, resolution, customer-visible summary, SLA target, hours worked
Purchase Order5PO number, expected delivery date
Quote Request4End customer, market area, currency, need-by date
New Product Request12Technical specification, feasibility notes, approval-proxy lookup, approval-status rollups
Certification Request5Lot number, heat number, certification type
General Enquiry3Enquiry 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.

FieldDistributorAnalystManager
Customer-visible summaryReadFullFull
Resolution (internal narrative)NoneFullFull
Hours workedNoneFullFull
Assigned analystNoneReadRead

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.

05Automation & 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.

TriggerWhat it does
Case createdCreates a distributor-visible note, "Case created by <user>", opening the thread
Case updatedWrites an internal note "<actor> changed status to <status>" as audit trail
Case updated → status enters an active state and assigned-analyst is emptyStamps 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 createdResolves recipients through the case's lookups, then chains to a second process that sends the email
Case created, filtered to new-product-requestSpawns the approval proxy Quote — see §6
Quote updated, filtered to approval decisionWrites 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 scheduleCloses 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.

06Limits & 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.

07Verification

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 pattern, no client data Blueprint 001 · rev 2026-09-02