Approval & Review Process — Multi-Stage Approval Chains

Multi-stage approval workflows for deals (PipelineItem), quotes, contacts, and proposals. Admins build named ApprovalProcess templates with sequential or parallel approver steps; team members submit entities into the process; approvers approve/reject from the inbox or via email. Submitted entities are hard-locked (HTTP 423 on edit attempts) while pending so nobody slips a change through during review. Distinct from "AI Actions & Approvals" which gates the AI's own actions — this one gates HUMAN-driven business processes. Plan-gated: Pro (10 active processes) / Max (unlimited).

Open in CRMown →

Steps

  1. Build the process: go to Approvals (sidebar > Approvals) > Processes tab > "+ New process". Pick the entityType this process applies to (PipelineItem / Quote / Contact / Proposal / custom), set name + description, and toggle the global flags: requireSubmitNote (force submitter to add context), allowSelfApproval (rare — usually off), lockOnSubmit (default ON — entities lock on submit).
  2. Configure approver chain. Each ApprovalStep has an order number, a name, approverUserIds (explicit list) AND/OR approverRoles (role-name strings like ADMIN / EXECUTIVE), and a logic mode: "any" (any approver can advance the step) or "all" (every named approver must approve). approvalType on the parent process is "sequential" (each step waits for the prior) OR "parallel" (all steps fire simultaneously) OR "any_one" (first approval anywhere wins).
  3. Optionally add a triggerCriteria JSON to auto-route entities through this process. e.g. {"dealValue": {"gt": 10000}} on a PipelineItem process means deals over $10k auto-match. Multiple processes can match — the resolver either picks the best fit (1 match → auto-submit) or asks the user to choose (2+ → "pending_approval_choice" UI banner with a chooser).
  4. Set escalation: escalateAfterHours + escalationUserId. If a request sits in pending state past the threshold, the approval-escalate worker fires an email reminder to the escalation user and appends an "escalate" entry to the audit trail.
  5. Submit an entity for approval: from the entity detail page (deal, quote, etc.), click "Submit for approval". If a process matches automatically, it submits in one click. If multiple processes match, you see a chooser. The entity's status flips to pending_approval; lockedByApprovalRequestId is set on the row; PATCH/DELETE on it now returns 423.
  6. Approvers act: pending requests show in /dashboard/approvals > Inbox (sidebar badge counts pending-for-you). Click in, see the diff context (what entity, who submitted, submitter note), then click Approve, Request Info, or Reject. Reject + Request Info require a reason. Approval moves the request to the next step (or completes it). Final approval unlocks the entity and sets it to "approved"; rejection unlocks it and sets it to "rejected".
  7. Email path: every step transition sends an email to the next approver (approvalRequestEmail), the submitter on terminal events (approvalApprovedEmail / approvalRejectedEmail / approvalInfoRequestedEmail), and to escalation users on hour-based timeouts (approvalEscalationEmail). Approvers can act from the inbox AND from a one-click email link.
  8. Withdraw your own request: submitter can withdraw any non-terminal ApprovalRequest from the request detail page. Withdrawing unlocks the entity and lets you edit + resubmit.

Good to know

  • Lockable entity types are PipelineItem (deals) and Quote in v1. Contact and Proposal are submittable but don't hard-lock — you can still edit them while approval is pending. (We chose narrow lock semantics intentionally to avoid breaking existing CRM-edit-during-review workflows for those types.)
  • Approvals integrates with CPQ: a CpqPriceRule with requiresApproval=true on the matching quote auto-fires submitForApprovalAuto on quote send. The result is one of three outcomes: pending_approval (1 process matched), pending_approval_choice (2+ matched — UI shows chooser banner), or draft + warning ("0 processes match — fix your approval setup"). All three are surfaced in the quote detail page.
  • Approver authorization is checked on every action — even if you have a stale email link, the backend re-resolves "is this user authorized for this step right now?" before accepting. Roles are dereferenced via the User.role enum (single-string, not a join table).
  • Notify lists (notifyOnApprove / notifyOnReject) are user IDs that get email notifications on terminal outcomes IN ADDITION to the submitter. Useful for cc'ing a manager or finance lead.
  • requireSubmitNote=true forces a non-empty submitter note on the request. Use this for high-stakes processes where you want context attached to every request (e.g. CFO discount approvals).
  • allowSelfApproval=false (default) means the submitter is excluded from the approver pool even if their userId or role matches a step. Turn this on only when you genuinely want self-approval (rare).
  • Sandboxable: approval processes (and their steps) are part of the sandbox clone + diff + promote workflow. Build a new process in a sandbox, test it against cloned deals, then promote selected approval-process changes back to production.
  • Quota: Pro plan caps at 10 ACTIVE approval processes (isActive=true). Inactive processes don't count. Hit the cap and you'll see a "checkApprovalProcessQuota" 402 response with the upsell.
Example: A SaaS sales team wants tighter discount governance. The RevOps lead builds a 2-step ApprovalProcess for PipelineItem entityType: step 1 — Sales Director (any), step 2 — CFO (all). triggerCriteria is {"dealValue": {"gt": 25000}, "discountPercent": {"gte": 15}}. escalateAfterHours: 4, escalationUserId: VP Sales. A rep submits a $48k deal at 18% off — auto-matched to this process, deal locks, Sales Director gets the email, approves in 30 minutes. CFO's turn: she's in a board meeting and the 4-hour escalation timer fires — VP Sales gets notified, taps the email link, approves on behalf. Deal unlocks, customer emailed, rep moves on. Total cycle: 5h 12m vs the prior 2-day spreadsheet ping-pong. Audit trail shows every actor + timestamp + comment.

Troubleshooting

"423 Locked" when editing a deal or quote
The entity has an active ApprovalRequest pending against it. Lock semantics are deliberate — prevents changes during review.
Fix: Either wait for the approval to resolve (you'll see status in /dashboard/approvals), OR withdraw the request if you submitted it and changed your mind (request detail page > Withdraw). Lock auto-releases on terminal outcomes.
Submit button shows "no_process" or grayed out
No active ApprovalProcess matches this entity's entityType, OR the matching processes' triggerCriteria don't match this entity's data.
Fix: Settings > Approvals > Processes — verify a process exists for the entityType (PipelineItem / Quote / etc.) and that isActive=true. If you're using triggerCriteria, double-check the JSON matches your entity's actual fields. Ad-hoc submission (without auto-match) requires explicit processId — currently UI requires a matching process.
Multiple processes match — "pending_approval_choice"
Two or more ApprovalProcesses' triggerCriteria matched this entity. The auto-resolver refused to guess.
Fix: On the quote / deal detail, the chooser banner lists the matching processes. Pick one and Submit. To prevent this in the future, narrow your triggerCriteria so processes are mutually exclusive (e.g. one process per dealValue range).
Approver can't approve — "not authorized for this step"
Server-side re-check ran (every action re-resolves authorization) and the user no longer matches the step's approverUserIds OR approverRoles. Could be a role change or step config change since the request was created.
Fix: Check the step config — approverRoles uses User.role enum strings exactly (ADMIN / EXECUTIVE / DIRECTOR / TEAM / etc.). If user's role was changed, request needs a different approver or step needs to be reconfigured.
Quota exceeded — "checkApprovalProcessQuota"
Pro plan caps at 10 active ApprovalProcesses. You've hit it.
Fix: Either deactivate (isActive=false) processes you're no longer using, or upgrade to Max (unlimited).

Error messages

  • entity_locked — The pipeline item / quote you're editing has an active approval pending — it's hard-locked. HTTP 423.
  • no_active_process — Submit-for-approval was called but no active ApprovalProcess matches this entity type. Configure one first.
  • multiple_processes_match — More than one process matched the entity's triggerCriteria. The frontend chooser banner asks the user to pick one.
  • not_authorized_for_step — The approver isn't in the current step's approverUserIds or approverRoles. Re-check happens on every action so role changes mid-flight are caught.
  • approval_quota_exceeded — Pro plan limit of 10 active approval processes hit. Deactivate one or upgrade to Max.
  • self_approval_forbidden — allowSelfApproval is false on the process and the actor is the same person who submitted the request. Set allowSelfApproval=true on the process if you want this behavior.

Plans

Pro plan: 10 active approval processes. Max plan: unlimited. Free / Starter: not available. Permissions: approvals:read (everyone except VIEWER limited subset), approvals:write (TEAM+ — submit + take action), approvals:admin (DIRECTOR+ — process CRUD).

Related

  • CPQ — quotes with discount rules (requiresApproval=true) auto-submit through this approval system; pending_approval_choice UI lets users pick when multiple processes match
  • Pipeline / Deals — PipelineItem is a top-tier lockable entityType; useful for deal-level approval (e.g. discount thresholds, contract terms)
  • AI Actions & Approvals — different feature; that one gates AI-driven actions (sending emails, etc.) by risk level. This one gates human-submitted business processes
  • Sandbox Environments — approval processes are sandboxable; test new chains against cloned data before promoting to production
  • Notifications — approval-related events (requested, approved, rejected, escalated) fan out to user bells AND email; configurable per-user under Settings > Account > Notifications

View as Markdown