Skip to content

Open Pipeline Is Ready for Coaching

Detailed page. Use “On this page” to jump directly to the section you need.

Build an Apex Check that flags only an open Opportunity carrying all three coaching risks, stale activity, no Next Step, and a Close Date outside the current quarter.

Setup reference

Use the Apex reference for the complete setup fields and behavior.

The supporting Apex class lives under integration-tests/ and does not install with the package. Create and deploy the subscriber-owned class in Step 2 before configuring the Check. The code on this page uses the public rhc.* Apex types provided by the installed package.

A sales manager opens an Account before pipeline coaching.

  • The manager needs to find Opportunities that have several warning signs on the same deal.
  • A stale deal with no Next Step and a Close Date outside the current quarter needs focused coaching.
  • The same warnings spread across different Opportunities do not identify one deal with that combined risk.

Why use Record Health Check

Record Health Check identifies deals where all three warning signs occur together, so the manager can coach the Opportunity that needs attention instead of reconciling separate warnings.

  • Install Record Health Check.
  • Assign Record Health Check Admin to the administrator who creates the Check Set and Check.
  • Have a Salesforce developer review, test, and deploy the Apex class. Record Health Check does not install this example class.
  • Confirm that intended users can read Account, Opportunity, and the Opportunity fields listed under Security and access.
SkillHow this example teaches it
Apply several conditions to one related recordApex evaluates multiple warning signs on each open Opportunity.
Keep complex logic readableNamed Apex conditions replace a hard-to-read metadata expression.
Summarize a failure for usersThe result identifies why pipeline needs attention.
Card valueHealthyUnhealthyNo open Opportunities
StatusPASSFAILSKIPPED
Found0 unhealthy<N> unhealthyNot applicable
Expected0 unhealthy0 unhealthyNot applicable
MessageNo failure messageConfigured Critical messageApplicability explains the skip
Evaluation TypeWhy it fits
Verify with ApexBest fit. The class confirms that all three warning signs belong to the same open Opportunity.
Verify with a query in three separate ChecksWould show three separate results, and each warning could come from a different Opportunity.
Verify with a query in one CheckCould place every condition in one query, but the current-quarter date logic and user guidance would be harder to maintain.

Shared scope inputs are documented once in the Apex examples README. This Check receives Account Ids and binds them in one Opportunity query.

List<Id> accountIds = scope.recordIds;

The complete class below returns one outcome for every requested Account.

Confirm the business rule and developer ownership

Section titled “Confirm the business rule and developer ownership”

This class is not installed in a subscriber org. A developer must review, test, and deploy it before an administrator enters its class name. The Check uses calendar-quarter logic; do not use it unchanged for custom fiscal periods. LastActivityDate is derived from activity and is not normally edited directly, so create dated Tasks or Events for tests.

The applicability count query skips Accounts with no open Opportunities before the class runs. The class’s own zero-open result is defensive and would pass if reached. This example fails only when one Opportunity carries all configured risk conditions; use separate Formula or Query Checks when any single warning should fail. The default stale window is 30 days when the parameter is missing or invalid, so approve that fallback explicitly.

The card reports counts, not Opportunity names. Add a related-list or report action when users need the records. Add the card to the Account Lightning page, activate the intended assignment, and test as a user with Record Health Check Card User. Execute Anonymous is developer verification.

Use Check parameters to change the stale-activity window without editing the Apex class:

{
"staleDays": 30
}

Record Health Check parses the JSON and supplies it as scope.parameters. The class accepts staleDays from 1 through 3650. A missing, nonnumeric, or out-of-range value silently uses 30. Enter and test an explicit valid whole number rather than relying on that fallback. See Parameter parsing patterns for validation and type-conversion guidance.

Create an Apex class named AccountOpenOpportunityHealthCheck from the code below. It reads open Opportunities visible to the running user, calculates the current calendar-quarter boundaries, and counts an Opportunity only when all three conditions are true.

/**
* @author Gautam Kolan (https://github.com/gkolan)
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Example RecordHealthCheck that flags open Opportunities that are simultaneously
* stale, missing Next Step, and not closing this quarter. Tunable via
* {"staleDays": 30}
*/
global with sharing class AccountOpenOpportunityHealthCheck implements rhc.RecordHealthCheckPlugin {
private static final Integer DEFAULT_STALE_DAYS = 30;
private static final Integer MIN_STALE_DAYS = 1;
private static final Integer MAX_STALE_DAYS = 3650;
global Map<Id, rhc.RecordHealthCheckOutcome> evaluate(
rhc.RecordHealthCheckScope scope
) {
Integer staleDays = resolveStaleDays(scope.parameters);
Date staleCutoff = Date.today().addDays(-staleDays);
Date quarterStart = getQuarterStart(Date.today());
Date quarterEnd = quarterStart.addMonths(3).addDays(-1);
List<Id> recordIds = scope.recordIds;
// Seed every Account first. An Account with no open Opportunities returns
// no rows at all, and "nothing unhealthy" is a real pass rather than a
// record the check forgot to answer for.
Map<Id, Integer> unhealthyByAccount = new Map<Id, Integer>();
Map<Id, Integer> scannedByAccount = new Map<Id, Integer>();
for (Id recordId : recordIds) {
unhealthyByAccount.put(recordId, 0);
scannedByAccount.put(recordId, 0);
}
// One query for the whole scope. AccountId is selected so each row can be
// attributed back to the record it belongs to.
for (Opportunity opp : [
SELECT AccountId, LastActivityDate, NextStep, CloseDate
FROM Opportunity
WHERE AccountId IN :recordIds AND IsClosed = FALSE
WITH USER_MODE
]) {
if (!scannedByAccount.containsKey(opp.AccountId)) {
continue;
}
scannedByAccount.put(
opp.AccountId,
scannedByAccount.get(opp.AccountId) + 1
);
if (isUnhealthy(opp, staleCutoff, quarterStart, quarterEnd)) {
unhealthyByAccount.put(
opp.AccountId,
unhealthyByAccount.get(opp.AccountId) + 1
);
}
}
rhc.RecordHealthCheckValue expected = rhc.RecordHealthCheckValue.ofCount(0);
Map<Id, rhc.RecordHealthCheckOutcome> results = new Map<Id, rhc.RecordHealthCheckOutcome>();
for (Id recordId : recordIds) {
Integer unhealthyCount = unhealthyByAccount.get(recordId);
rhc.RecordHealthCheckOutcome outcome = unhealthyCount == 0
? rhc.RecordHealthCheckOutcome.pass('APEX_PASS')
: rhc.RecordHealthCheckOutcome.fail('APEX_FAIL');
results.put(
recordId,
outcome
.withFound(rhc.RecordHealthCheckValue.ofCount(unhealthyCount))
.withComparison('EQUALS', expected)
);
}
return results;
}
@TestVisible
private static Boolean isUnhealthy(
Opportunity opp,
Date staleCutoff,
Date quarterStart,
Date quarterEnd
) {
Boolean stale =
opp.LastActivityDate == null ||
opp.LastActivityDate < staleCutoff;
Boolean missingNextStep = String.isBlank(opp.NextStep);
Boolean closeNotThisQuarter =
opp.CloseDate == null ||
opp.CloseDate < quarterStart ||
opp.CloseDate > quarterEnd;
return stale && missingNextStep && closeNotThisQuarter;
}
@TestVisible
private static Date getQuarterStart(Date reference) {
Integer month = reference.month();
Integer quarterMonth = ((Integer) Math.floor((month - 1) / 3.0) * 3) + 1;
return Date.newInstance(reference.year(), quarterMonth, 1);
}
@TestVisible
private Integer resolveStaleDays(Map<String, Object> parameters) {
if (parameters == null) {
return DEFAULT_STALE_DAYS;
}
Object raw = parameters.get('staleDays');
if (raw == null) {
return DEFAULT_STALE_DAYS;
}
try {
Integer parsed = Integer.valueOf(String.valueOf(raw));
return parsed >= MIN_STALE_DAYS &&
parsed <= MAX_STALE_DAYS
? parsed
: DEFAULT_STALE_DAYS;
} catch (Exception ex) {
return DEFAULT_STALE_DAYS;
}
}
}

Create an Apex test class that proves these cases before deployment:

  1. one healthy open Opportunity returns PASS;
  2. one Opportunity with all three warning signs returns FAIL;
  3. warning signs split across different Opportunities do not produce a false failure;
  4. 200 Account IDs receive 200 outcomes; and
  5. the number of SOQL queries does not increase as more Accounts are supplied.

The repository’s AccountOpenOpportunityHealthCheckTest shows the package implementation tests. A subscriber test must use the public rhc.* types.

Record Health Check calls the plugin once for a scope:

Map<Id, rhc.RecordHealthCheckOutcome> evaluate(rhc.RecordHealthCheckScope scope)

The context contains:

Scope fieldTypeWhat it contains
recordIdsList<Id>Detached IDs to evaluate, with duplicates removed; use the collection in bulk SOQL
objectApiNameStringAPI name shared by every ID in the scope, such as Account
parametersMap<String, Object>Parsed Apex Parameters (JSON); an empty map when JSON is blank
checkQualifiedApiNameStringQualified Check identity
checkSetQualifiedApiNameStringQualified Check Set identity
checkDeveloperNameStringUnqualified Check DeveloperName
checkSetDeveloperNameStringUnqualified parent Check Set DeveloperName
runIdStringCorrelation identifier for the evaluation run

The returned map must contain exactly one entry for every requested ID. Build each outcome with a status factory and typed values:

Outcome fieldWhat the class must return
statusAn outcome created by pass, fail, unableToEvaluate, or skipped
reasonCodeA stable, nonblank code that explains the programmatic reason
foundA typed RecordHealthCheckValue describing what the class observed
comparisonOperatorThe operator behind the decision, such as EQUALS
expectedA typed RecordHealthCheckValue describing the passing requirement

For applicability, configure Applies To on the Check so Record Health Check skips before Apex runs. The framework supplies identity, label, severity, messages, display values, and diagnostics. Missing or extra map keys, a null outcome, an invalid status, forbidden writes, or an unhandled exception produces APEX_EVALUATOR_ERROR, not a pass. See Returning an outcome.

In Setup → Custom Metadata Types → Record Health Check Set → Manage Records, select New and create this Check Set:

Setup fieldValue
LabelAccount Apex Readiness
Record Health Check Set NameAccount_Apex_Readiness
ObjectAccount
Card TitleAccount Readiness
Card SubtitleConfirm open Opportunities are ready for coaching.
When Checks RunWhen the user clicks Run
Summary DisplayShow below checks
Reveal ModeOne by one
Passed ChecksShow each passed check
Skipped ChecksShow each skipped check
Found/Expected DisplayShow on demand
Stop after a system errorUnchecked
Show DiagnosticsUnchecked; enable temporarily only for authorized troubleshooting
Publish User Run EventUnchecked
ActiveChecked

In Setup → Custom Metadata Types → Record Health Check → Manage Records, create the Check:

Setup fieldAPI nameValue
Developer NameDeveloperNameOpen_Opportunities_Are_Healthy
LabelMasterLabelOpen Opportunities Are Healthy
Check SetRecord_Health_Check_Set__cAccount_Apex_Readiness
Check TitleCheckTitle__cOpen Opportunities Are Healthy
Evaluation TypeEvaluationType__cVerify with Apex
Apex ClassApexClass__cAccountOpenOpportunityHealthCheck
Apex Parameters (JSON)ApexParametersJson__c{"staleDays": 30}
Applies ToApplicabilityMode__cWhen a count query matches
Applies When (Count Query)ApplicabilityCountQuery__cSELECT COUNT() FROM Opportunity WHERE AccountId = {!record.Id} AND IsClosed = false
Count Must BeApplicabilityCountOperator__cGreater than
Count ValueApplicabilityCountThreshold__c0
Setup fieldAPI nameValue
Check DescriptionCheckDescription__cChecks whether any open Opportunity is stale, missing Next Step, and outside the current quarter at the same time.
Failure SeverityFailureSeverity__cCritical
Message When FailedFailureMessage__c{!record.Name fallback="this record"} has open opportunities that are simultaneously stale, missing a Next Step, and have a Close Date outside the current quarter. Update Next Step, activity, or Close Date on the unhealthy Opportunities.
Message When Unable To EvaluateUnableToEvaluateMessage__cUnable to check open Opportunity health. Confirm the running user can read the Opportunities and fields used by this Check.
Prerequisite CheckPrerequisiteCheck__cLeave blank
Fix MessageFixMessage__cReview the open Opportunities. For each unhealthy Opportunity, update Next Step, log current activity, or correct Close Date.
Action LabelActionLabel__cReview open opportunities
Action URLActionUrl__c/lightning/r/Account/{!record.Id}/related/Opportunities/view
Evaluation OrderEvaluationOrder__c20
ActiveIsActive__cChecked
Publish User Result EventPublishUserResultEvent__cUnchecked

staleDays sets how old LastActivityDate must be before an Opportunity counts as stale.

The applicability fields in Configure the Check are required for the documented SKIPPED result when the Account has no open Opportunities.

Count-query applicability and the Apex result produce these health results and card values:

Health result or card valueWhat the user sees
PASSZero unhealthy open Opportunities passes.
FAILOne or more Opportunities has all three warning signs, so the card shows Needs attention with Critical severity.
SKIPPEDAn Account with no open Opportunities is skipped by the applicability count query before the Apex class runs.
FoundFound shows the unhealthy Opportunity count, such as 0 unhealthy.
ExpectedExpected shows that the unhealthy Opportunity count must be 0.

LastActivityDate = null counts as stale, blank NextStep counts as missing, and null CloseDate counts as outside the quarter. An Opportunity remains healthy when it has only one or two warning signs because the class combines all three conditions with AND logic. Calling the class directly with no open Opportunities returns PASS; the Check’s applicability settings create SKIPPED.

The class uses sharing and a user-mode Opportunity query so its result follows the running user’s Salesforce access.

  • Opportunity plus AccountId, IsClosed, LastActivityDate, NextStep, and CloseDate.

  • A hidden unhealthy Opportunity does not contribute to the result and can change Needs attention to Pass or Skip.

  • If the user-mode query throws because the running user cannot access Opportunity or a queried field, Record Health Check returns ERROR with reason code APEX_EVALUATOR_ERROR.

  • The evaluator performs no DML or callouts.

  • Prove the access-limited case with the actual Permission Sets and Opportunity sharing assigned to card users.

  1. Add an open Opportunity with blank Next Step, LastActivityDate older than the stale window, and CloseDate outside the current quarter. Confirm Critical.
  2. Fix or remove that Opportunity, rerun, and confirm a pass.
  3. Remove all open Opportunities and confirm skip.
  4. Repeat the failing case as a user who cannot see the unhealthy Opportunity. Confirm the result follows that user’s visibility and does not expose hidden Opportunity data.

Execute Anonymous alternative:

rhc.RecordHealthCheckResponse response = rhc.RecordHealthCheck.evaluate(
rhc.RecordHealthCheckRequest.forCheck(
// This is the Check Qualified API Name created in Step 4.
'Open_Opportunities_Are_Healthy',
'001XXXXXXXXXXXXXXX'
).withResultMode(rhc.RecordHealthCheckResultMode.EVALUATION_WITH_DISPLAY)
);
System.debug(LoggingLevel.INFO, JSON.serializePretty(response));
  1. Add Record Health Check to the Account record page in Lightning App Builder.
  2. Select Account_Apex_Readiness, save, and activate the page.
  3. Open the same Account, click Run or Rerun, and compare Status, Found, and Expected with the Execute Anonymous result.
SymptomWhat to verify
Check skips unexpectedlyConfirm an open Opportunity is visible to the running user and the applicability query still uses the Account merge token.
Expected unhealthy row passesConfirm all three conditions are true on the same Opportunity and that staleDays is valid.
APEX_EVALUATOR_ERRORVerify Opportunity object/field access and inspect authorized diagnostics.
APEX_CLASS_NOT_FOUNDDeploy the class and match Apex Class exactly.

Change staleDays in JSON without redeploying. If you change the definition of unhealthy, update the loop conditions, tests, failure message, and required field permissions together. Remove or change the applicability configuration if an Account with no open Opportunities should pass rather than skip.