The 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.
01The 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.
02Why 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.
03Configuration — 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.
04What 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.
05Making 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.
06Limits & 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.
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 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.
07Verification
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.