CQL in Approval Chains: Reference Guide
How Approval Chain CQL Is Evaluated
Before building conditional logic, it helps to understand how ChartHop evaluates these expressions:
- Conditional stage expressions are evaluated against each change in the scenario, not the scenario as a whole — unless you use scenarioChanges, which operates at the scenario level.
- Approver expressions (the "Custom CQL expression" approver option) are evaluated differently from inclusion conditions: a bare change / change.after is not in scope there. To route an approver based on what changed, you must reach the change through scenarioChanges (see Custom CQL Approver Expressions below).
- The best way to test a change.before / change.after expression is to use it as a filter on the scenario Changes tab. If it returns results there, it will fire in the approval chain.
- If a stage's approver resolves to nobody and no fallback approver is set, the chain will stall. Always configure a fallback approver for any stage that uses dynamic routing.
- Per-change filter (no wrapper needed): In a stage's "Only include stage if" field, change is an implicit per-change context variable. ChartHop evaluates each change individually, so you write the condition directly (e.g., change.type="create" && levelAuto=["IC0","IC1"]). Wrapping in scenarioChanges.filter{} is not valid in this context and will throw an "Unknown field scenarioChanges" error.
- When to use each pattern: If the condition is "does this change meet a condition?" write it directly using change.field=value. If the condition is "do all applicable changes together meet a condition?" use scenarioChanges.filter{}.aggregation{} > threshold (see scenarioChanges Expressions below).
Stage Approver Options
When setting up a stage, the Stage Approvers field supports several modes:
Option | What it does |
|---|---|
Manager | Routes to the direct manager of the job being changed |
Grand Manager | Routes to the manager's manager |
Specific Person | Routes to a named individual — always fires for that person |
Person Field | Routes to whoever is in a custom Person-type field (e.g., Hiring Manager, Talent Partner) |
Multiple stages, one per manager in the chain | Creates a dynamic stage for every manager in the hierarchy, all the way up — each approves in sequence |
Custom CQL expression | Define your own routing logic |
Conditional Stage Expressions
These go in the "Only include stage if" field. The stage only fires when the expression evaluates to true.
Trigger only if a specific field changed
Trigger only if manager changed
Trigger only if a specific person submits the scenario
scenarioChanges Expressions
scenarioChanges operates at the scenario level rather than per-change. Use it when the approval decision should be based on the combined contents of the scenario rather than any individual change. These are called Approval Group Inclusion Expressions in the platform.
Trigger based on total cost impact across all changes
Use when you want approval based on the total cost of all changes combined.
Example: Require VP approval when the combined salary impact of all changes exceeds $500,000. Replace the number with your org's threshold.
Trigger if any change in the scenario affects a specific condition
Use when you want approval if at least one change matches a condition.
Example: Include the Engineering Director as an approver whenever any change in the batch affects an Engineering employee — even if the rest of the scenario touches other departments.
Trigger only if every change in the scenario matches a condition
Use when you want approval only if every change matches a condition.
Example: Route to a specialized Engineering approval chain only when the entire batch contains Engineering changes. Skip it if the scenario is mixed across departments.
Combine scenario content with submitter identity
Use when you want to combine a condition about the changes with a condition about who is submitting the request.
Example: Only include this approval stage when all changes are in Engineering AND the person submitting the request is the CEO. This lets you create different approval flows based on both what's changing and who's requesting it.
Custom CQL Approver Expressions
The Custom CQL expression approver option lets you compute the approver(s) dynamically instead of picking a manager or a named person. The expression must resolve to a person (or a list of people) — for example a Person-type field on the job, or a value looked up from a table. ChartHop routes the request to that person's job.
Referencing the change being approved (important)
When the approver depends on what changed — for example, routing to the owner of the department, region, or cost center a job is moving into — you must reach the change through scenarioChanges. A bare change / change.after is empty when an approver expression is evaluated, so the expression resolves to nobody and the stage silently falls back to the fallback approver.
Wrap the lookup in scenarioChanges.filter.map:
This reads: for each change that sets a region, look up that region in the regionOwner table and return its owner (a Person). Each matching owner is added as an approver.
Route to an owner looked up from a table
- Store the mapping in a table — for example, regionOwner with a unique key column (regionName) and a Person owner column (owner).
- Add the key to the job as a field — for example, a single-select region field.
- Set the stage's Stage Approvers to a Custom CQL expression that filters scenarioChanges to the changes that set the key, then maps each to the table's owner column (see the expression above).
- Set a fallback approver — the stage falls back if a change's key has no matching row, or if the resolved owner has no active job.
Common Approval Chain Patterns
Route to a Talent Partner or custom role via a Person field
- Create a custom Person-type field (e.g., talentPartner) on the job
- In the approval stage, set Stage Approvers to that Person field
- ChartHop will route to whoever is in that field for each job — different jobs can route to different people automatically
- Set a fallback approver for jobs where the field is blank
Route to the owner of the value a job is moving into
Use a Custom CQL approver with a table lookup wrapped in scenarioChanges when the approver isn't stored on the job itself but in a lookup table keyed by a field the change sets (region, cost center, department owner, etc.):
Send approvals up the entire manager chain
In the Stage Approvers dropdown, select "Multiple stages, one per manager in the manager chain". ChartHop will dynamically create one approval stage per manager level, from the direct manager up to the top of the org. Each approves in sequence.
Route differently based on department
Create separate conditional stages, each with its own "Only include stage if" condition:
Require approval only above a cost threshold
Key Things to Know
Test before you configure. Use the expression as a filter on the scenario Changes tab first. If it returns the changes you expect, it will behave the same way in the approval chain.
Fallback approver matters. Any stage that uses dynamic routing — Person fields, custom CQL, manager chain — should have a fallback approver configured. If the expression resolves to nobody, the chain falls to the fallback. If there's no fallback, the chain stalls.
Change data in a custom approver needs scenarioChanges. In a Custom CQL approver expression, a bare change / change.after is empty and the stage will fall back. Wrap change references in scenarioChanges.filter.map. Inclusion conditions in "Only include stage if" don't need this — they already run per change.
One scenario, one approval chain. A single scenario routes through one approval chain. You can build a lot of conditional logic within that chain, but you can't branch a scenario into two separate chains simultaneously.
Approvers can edit during their stage. Once it's their turn in the chain, approvers can edit the scenario before approving or rejecting. Once they act, the scenario is locked for that stage.
The Changes tab is your best debugging tool. If a conditional stage isn't firing as expected, check whether your expression returns results there. If it doesn't, the expression syntax or field reference needs adjustment.
