---
title: "How do you 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://blueprints.coevera.com/blueprints/quote-approval-thresholds/
publisher: Coevera
customer_data: none
---

# How do you 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 exactly what you want them to do, and
asking permission for every small concession would slow the whole team down for no benefit.

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

What the business asked for:

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

## 02 · Why the obvious approach fails

Four things get tried before anyone reaches for a real approval process, and each fails in the
same fundamental 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 is the person who ticks the box,
and nothing about the record changes when they do. It produces a field that is always `true` and
means nothing.

### An automation that emails the manager

This is a notification, not a gate. The quote is already saved, already printable and already
sendable at the moment the email goes out. It tells you what happened; it does not stop it
happening.

### A pipeline step called "Approval"

A convention, not a control. Steps are moved by users, and a user who needs to get a quote out
will move past it. It also silently misrepresents the data: the step says "approved" because
someone dragged a card.

### Naming the approvers individually

This one actually works — until the first reorganisation. Named approvers break when someone
changes team, goes on leave, or leaves the company, and the failure mode is a queue of approvals
waiting on a person who no longer exists. It also has to be maintained forever by whoever
remembers it 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 at the
> moment 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.**
> These are three separate operations, and stopping after the first two leaves a process that
> looks present and does nothing.

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

### The schema, by part

The configuration payload has four top-level members.

#### trigger

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

#### filter — where the threshold lives

This is 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 worth drawing out. **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 that 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 of them is sufficient. |
| `canDelegate` | Whether an approver may hand the decision to someone else. |
| `recordLock` | **The enforcement mechanism.** Controls who may edit the record while the decision is pending. |
| `expiration` / `expirationDays` / `expirationResult` | Whether a pending approval times out, after how long, and what verdict it takes on timeout. |
| `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 it needs no maintenance as the company changes shape. 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 when approval is granted.

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

For the requirement in §1, the shape is:

| Setting | Value | Why |
|---|---|---|
| Trigger entity / event | Quote · create-or-update | Catches both 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 approver; set deliberately if you add a second. |
| `recordLock` | Managers may edit | Blocks the raiser, leaves an escalation path. See the caveat in §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 the 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 they are trying to work out why a quote is locked.

## 05 · Making the decision visible

The approval history records who decided what and when, and that satisfies audit. It does not
satisfy the sales team, who need to see 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 a 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 trying to do 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](https://blueprints.coevera.com/blueprints/one-entity-five-request-types/).

## 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 that nobody answers is *granted automatically* when the period elapses.
>
> This is worse than having no approval process. It produces an audit trail that shows an approval
> which no human ever 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 the 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 it means 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
any 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](https://blueprints.coevera.com/blueprints/one-entity-five-request-types/).

### The rejection path is usually missing

In every build we have reviewed, the approve branch was complete and the reject branch was empty
or partial. The failure is quiet: a rejected record simply 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**, which means 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 that is believed to work and 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 is what 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.
