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.
typeIdis 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
| 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 |
04Field-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.
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.
| 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.
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:
- A new-product-request case is created.
- A process spawns a linked Quote under a dedicated quote type reserved for approvals.
- The approval process runs on that Quote, natively, with the standard approval UI and audit trail.
- The Quote carries a lookup back to the case; the platform maintains the reverse link automatically.
- On decision, a second process writes the outcome back to the case status.
- 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
typeIdon 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.