---
title: "How do we stop quotes and discounts going out before someone signs off?"
blueprint: 004
slug: quote-approval-thresholds
category: Process control
revised: 2026-09-02
platform: Coevera CRM (formerly Pipeliner CRM)
canonical: https://coevera.webplanet.sk/blueprints/quote-approval-thresholds.html
publisher: Coevera
customer_data: none
---

# How do we stop quotes and discounts going out before someone signs off?

**Short answer.** An **approval process on the Quote entity**, triggered on create-or-update and
narrowed by an ordinary **filter node** — which is where the threshold lives. A quote total with
the `More` operator and a value of `10000` is all "only above 10K" means. The same mechanism works
on a discount percentage or a margin field.

The enforcement is **`recordLock`**, not a notification. Route to **`SalesUnitManager`** rather than
named people so it survives staff changes. And check **`expirationResult`** before you ship — it can
be set to *Approve*, meaning an approval nobody answers is granted automatically.

## 01 · The business problem

Salespeople discount to close. Most of the time that is what you want, and asking permission for
every small concession slows the team down for no benefit.

The problem is the tail. A handful of quotes each quarter carry a discount or a total nobody senior
would have agreed to, discovered after the fact — at booking, at invoicing, or when someone runs a
margin report. By then the customer has the number in writing.

Requirements:

- quotes above a value threshold cannot proceed until someone signs off
- everything below the threshold is completely unaffected — no extra step, no notification
- the approver is whoever currently manages the person who raised it, without maintaining a list
- an audit trail of who approved what and when
- and the record genuinely held, not just flagged

## 02 · Why the obvious approach fails

Four things get tried first, and each fails the same way: it *records an intention* rather than
*preventing an action*.

**A required "approved by manager" field.** Self-attested and unenforced. The person who wants the
discount ticks the box, and nothing about the record changes. It becomes a field that is always
true and means nothing.

**An automation that emails the manager.** A notification, not a gate. The quote is already saved,
printable and sendable when the email goes out.

**A pipeline step called "Approval".** A convention, not a control. Steps are moved by users, and a
user who needs a quote out will move past it — while the data now claims approval because someone
dragged a card.

**Naming the approvers individually.** This works until the first reorganisation. Named approvers
break when someone changes team, goes on leave or leaves, and the failure mode is a queue waiting on
a person who no longer exists.

> **The real requirement is two things at once:** a *lock* on the record while the decision is
> outstanding, and *dynamic routing* that resolves the approver from the org structure when the
> approval is raised. Coevera's approval process provides both; nothing else in the platform
> provides either.

## 03 · Configuration — three calls, not one

> **Creating an approval process does not configure it, and configuring it does not enable it.**
> Three separate operations; stopping after the first two leaves a process that looks present and
> does nothing.

| Step | Carries |
|---|---|
| **1 · Create** | Only `name`, `description`, `ownerId`. No trigger, no filter, no approvers. |
| **2 · Configure** | The entire schema, as a JSON string: trigger, filter, settings, decision nodes. |
| **3 · Enable** | A separate call. Until it runs, nothing fires. |

### The schema, by part

**trigger.** Names the entity and event, and — usefully — *who* can trigger it. Beyond entity type
and an event such as create-or-update, it carries an actor setting with lists of units and users.
Left as any user it applies to everyone; narrowed, it gates one team's quotes without touching
another's.

**filter — where the threshold lives.** An ordinary filter node, the same structure used throughout
the platform's automation. The captured example is named for what it does — *sum over 10K* — and
contains a single rule: the quote total field, operator `More`, value `10000`.

Two consequences. **Below the threshold no approval is created at all** — this is not an approval
that auto-approves small deals, it simply does not fire. And because it is a general filter node,
**the threshold can be anything you can filter on**: a discount percentage, a margin field, a
product category, or several conditions combined.

Note a numeric filter value on a money field is stored as a tuple carrying the amount alongside a
currency reference — a bare number is not the whole story on a multi-currency field.

**settings — who decides, and what is frozen.**

| Setting | What it does |
|---|---|
| `approvers` | A list of entries, each either a named user or a **role type** such as the sales unit manager. |
| `allApprove` | Whether every approver must agree, or any one suffices. |
| `canDelegate` | Whether an approver may hand the decision on. |
| `recordLock` | **The enforcement mechanism.** Who may edit the record while pending. |
| `expiration` / `expirationDays` / `expirationResult` | Whether a pending approval times out, after how long, and what verdict it takes. |
| `salesProcessDependency` | Ties the approval to the record's sales-process position. |

> **Use a role type for the approver, not a user id.** A sales-unit-manager approver resolves from
> the record owner's unit when the approval is raised. It keeps working through reorganisations,
> leavers and holidays, and needs no maintenance. Named approvers are the single most common reason
> an approval process quietly stops working a year after it was built.

**approveNodes and rejectNodes.** Two arrays of automation nodes run on the respective decision. They
are the same node types used in ordinary automation, so anything automation can do, a decision can do
— the captured example uses an update-record node to move the quote's pipeline step on approval.

One shape detail that catches people: on an update-record node the payload lives in the entity's
`input` as a JSON string, while the `operations` array only annotates which fields inside it are
being written. Supplying operations against an empty input produces a generic, unhelpful error.

## 04 · What to actually set

| Setting | Value | Why |
|---|---|---|
| Trigger entity / event | Quote · create-or-update | Catches a quote raised above the threshold and one edited up to it later. |
| Trigger actor | Any user | Governance that exempts some people is not governance. |
| Filter | Total · `More` · threshold | Below it, nothing fires at all. |
| Approvers | Sales unit manager (role type) | Survives staff and org changes with no maintenance. |
| `allApprove` | false, for a single approver | Only meaningful with more than one; set deliberately if you add a second. |
| `recordLock` | Managers may edit | Blocks the raiser, leaves an escalation path. See §6. |
| `expirationResult` | **Reject**, not approve | See §6 — the alternative silently grants approval nobody gave. |
| Approve node | Move the pipeline step | Makes the decision visible on the record, not only in approval history. |
| Reject node | Move the step, and notify | The rejection path is the one most often left empty. See §6. |

Naming the filter for its business meaning rather than its mechanics pays off later — the name is
what an administrator sees when working out why a quote is locked.

## 05 · Making the decision visible

The approval history records who decided what and when, which satisfies audit. It does not satisfy
the sales team, who need the state on the record itself.

That is what the decision nodes are for. On approval, move the quote to a step that reads as
approved; on rejection, move it to one that reads as rejected and tell the owner. If downstream
automation needs to run — publishing a value, creating a task, writing back to a parent record — the
decision node can trigger a further process rather than doing everything itself.

If the quote is a proxy standing in for something that cannot host an approval itself, the write-back
is the whole point of the design — that pattern is Blueprint 001.

## 06 · Limits & trade-offs

### The setting that turns governance into theatre

> **`expirationResult` can be set to approve.** With expiry switched on and that verdict, an
> approval nobody answers is *granted automatically* when the period elapses.
>
> This is worse than having no approval process. It produces an audit trail showing an approval no
> human gave — and it will be discovered by whoever is auditing, not by you. If expiry is enabled at
> all, the result should be reject, with escalation handled by the reject nodes. Check this value
> explicitly on every approval process you inherit.

### The lock is not a freeze

A record-lock mode that permits managers to keep editing means exactly that. It is a sensible
default — it leaves an escalation path when something urgent is stuck — but the record is not
immutable while pending. Do not describe it to an auditor as though it were.

### What an approval cannot be attached to

Approvals can only target **Account, Contact, Lead, Opportunity or Quote**. Custom entities are not
supported as approval targets, and the approval record itself accepts no custom fields — so approval
metadata you need to report on has to live on the target record. Both constraints, and the proxy
pattern that works around them, are in Blueprint 001.

### The rejection path is usually missing

In every build we have reviewed, including our own, the approve branch was complete and the reject
branch was empty or partial. The failure is quiet: a rejected record stays where it was, with no
signal to its owner, and looks identical to one still waiting. Build the reject nodes at the same
time as the approve nodes, or you will not build them at all.

### Other things to know

- **Created is not enabled.** A process can exist, be fully configured, report healthy, and never
  fire.
- **The configuration is a JSON string inside the request**, so it is not schema-validated the way
  ordinary arguments are. Type markers are required at the root and on nested nodes, and a payload
  missing them is rejected with a message that does not say so.
- **Money filter values carry a currency reference** alongside the amount. A threshold on a
  multi-currency field is not just a number.
- **Only one captured configuration is verified here.** Multi-approver behaviour — ordering, partial
  approval, what a delegation does to the audit trail — should be tested in your own space before
  you rely on it.

## 07 · Verification

An approval process is a control. A control believed to work that does not is the worst possible
state, so the test plan matters more here than in most builds.

- **Confirm all three steps ran** — created, configured, and *enabled*. Read the enabled state back
  rather than assuming the call succeeded.
- **Test below the threshold.** The correct behaviour is that nothing happens at all — no approval
  record, no notification, no lock. An approval that fires and auto-approves is a different, wrong
  design.
- **Test above the threshold**, and separately test *editing a quote up across* the threshold. The
  create-or-update event catches the second case, and it is the one people forget to try.
- **Verify the lock as the raiser, not as an administrator.** Log in as a sales user and attempt the
  edit. Administrators frequently cannot reproduce the lock at all, which is why it gets signed off
  as working when it is not.
- **Exercise the expiry path deliberately.** Set a short expiry in a test space, let it lapse, and
  confirm the verdict it takes is the one you intended.
- **Exercise rejection, not just approval.** Confirm the record moves and the owner finds out.
- **Confirm approver resolution after an org change.** Move a test user to a different unit and check
  the approval routes to the new manager.

**What would signal a regression:** approvals appearing on quotes below the threshold; a pending
approval whose approver list is empty; records above the threshold reaching a closed state with no
approval in their history; or an approval history showing decisions timestamped at exactly the expiry
interval, which means the timeout is deciding instead of a person.

---

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