IIQ object for certification decision history

Hello, I am working with SailPoint IdentityIQ 8.4 and Certifications. In the Certification UI (Certifications → Access Review → Certification Items), for each certification item there is a three‑dashed menu button on the right with options: “Comment”, “History”, and “Account Details” (see attached screenshot).When select “History”, the “Decision History” window is opened (see attached screenshot).In that window (in this particular case), the last row has:Action = Commentand contains a comment text (for example: “Please keep ID and do not remove any privilege.”). My question is: Where exactly is this “Comment” stored technically?Specifically:- In which database table is this comment stored?- In which object type does it belong?- How can I retrieve this comment if I already have the CertificationItem object?- What is the correct / supported way to include this comment in a report? I would appreciate clarification on where this data is persisted and how it is related to the CertificationItem and its workflow.

Thank you in advance.


Screenshot 2026-04-27 152721.png

Screenshot 2026-04-27 150355.png

@avtan This is not available in audit or at identity object level. IIQ must be reading it from the Certification Item and Certification Entity objects of the past certifications.

Note: Found a fix? Mark it as a solution! Want to say thanks? Give Kudos to anyone who helped you understand or solve the problem. Thanks..!!!

@avtan

-->   It is stored in CertificationAction class and spt_certification_action table where you can filter using the certification id. If you already have certificationItem then get the action and get comments from the action. Attaching the sample query here:

select * from spt_certification_action where decision_certification_id=<certification id>;

--> For report we can use OOTB  -  Access review decision report which will give whole information along with the comments. Else once the certification is completed then you can download the report from the certification UI as well.

thank you all for reply.
We use custom reports, as the OOTB reports do not provide the required set of column.

@evanirohit0501  I think we have already checked this objects. Below there is what we already have:

<DataSource dataSourceClass=“sailpoint.reporting.datasource.CertificationLiveReportDataSource” objectType=“CertificationItem” type=“Java”>
…

<ReportColumnConfig field=“comments” header=“rept_cert_col_decision_maker_comments” property=“action” width=“110”>                <RenderScript>                  <Source>                    import sailpoint.object.CertificationItem;                    import sailpoint.object.CertificationEntity;                    import sailpoint.object.CertificationAction;  result = “certItemAction is null”;                                        CertificationAction a = (CertificationAction) value;                    if (a != null) {                                        result = “certItemAction comment is null”;                                          String c = a.getComments();                      if (c != null && c.trim().length() > 0) return c;                    }                      CertificationItem item = null;                     if (object instanceof CertificationItem) {                      item = (CertificationItem) object;                    } else if (row instanceof CertificationItem) {                        item = (CertificationItem) row;                    }                     if (item != null) {                                          CertificationEntity e = item.getEntity();                      if (e != null) {                                        result = “cert entity action or comment is null”;                                           CertificationAction ea = e.getAction();                        if (ea != null) return ea.getComments();                      }                    }                     return result;                  </Source>                </RenderScript>              </ReportColumnConfig>
…

and we have checked spt_certification_action comments for that particular comment but could not find (it is not there).

This comment is not a comment of approval or decline, this is jsut general comment. approval/decline comments you can turn on/off and if on it is required, but this is just general comment which is not  required, but approver can fill it.

@avtan This is coming from spt_identity_history_item: IdentityHistoryItem object. It creates a separate object for each operation like Comment, Approve or Reject.

Note: Found a fix? Mark it as a solution! Want to say thanks? Give Kudos to anyone who helped you understand or solve the problem. Thanks..!!!

@neel193  first of all respect and thank you very much. i found that comment.

I have some additional questions:

I found CertificationLink and CertifiableDescriptor objects inside IdentityHistoryItem.
Using them, I can identify the exact certification object and obtain some information about the certifiable object, but I do not see how this can help in my report.
My main report object is CertificationItem, and I need to retrieve the “comment” from  IdentityHistoryItem.
Is there any straightforward or supported way to get the corresponding IdentityHistoryItem starting from a CertificationItem?

and another question: 
Could you please advise where it is possible to find more detailed documentation for SailPoint classes / the SailPoint object model?
I have the document “IdentityIQ Object Model and Usage (IdentityIQ Versions: 7.x through 8.x)”, but it does not contain sufficient information.
The available Javadocs list the classes; however, they provide very limited descriptions and do not clearly present the overall object structure.
Is there any more comprehensive documentation available that describes the SailPoint object model in greater detail?

again thank you very much in advance

@avtan Could you please share the report columns which you want to have along with how are you going to get the value? Accordingly we can discuss more on the columns, whose logic is yet to be determined.

I have already created some code for retrieving that comments. bellow you can find the datasource and some columns from report.

The “comments” column contains comments taken from CertificationAction and for omments stored in IdentityHistoryItem, I created a separate column called “historyComments”. It is working, but I am not fully sure whether this is the correct or best approach to retrieve this information.

<DataSource dataSourceClass="sailpoint.reporting.datasource.CertificationLiveReportDataSource" objectType="CertificationItem" type="Java">
              <QueryParameters>
                <Parameter argument="phase" defaultValue="Staged" operation="NE" property="parent.certification.phase" valueClass="sailpoint.object.Certification$Phase"/>
                <Parameter argument="type">
                  <QueryScript>
                    <Source>
                            import sailpoint.object.*;
                            queryOptions.addFilter(Filter.ne("parent.certification.type", Certification.Type.BusinessRoleComposition));
                            queryOptions.addFilter(Filter.ne("parent.certification.type", Certification.Type.AccountGroupPermissions));
                            return queryOptions;
                          </Source>
                  </QueryScript>
                </Parameter>
                <Parameter argument="tags" property="parent.certification.tags.id">
                  <QueryScript>
                    <Source>

                            import sailpoint.object.*;
                            import java.util.*;

                            if (value != null && !value.isEmpty()){
                              queryOptions.addFilter(Filter.containsAll("parent.certification.tags.id", value));
                            }

                            return queryOptions;

                          </Source>
                  </QueryScript>
                </Parameter>
                <Parameter argument="certificationGroups" property="parent.certification.certificationGroups.id"/>
                <Parameter argument="applicationNames" property="applicationNames">
                  <QueryScript>
                    <Source>
                            import sailpoint.object.*;
                            import sailpoint.api.ObjectUtil;
                            List appNames = new ArrayList();
                            if (value != null){
                              appNames = ObjectUtil.convertIdsToNames(context, Application.class, value);
                            }

                            if (appNames.size() != 0){
                              queryOptions.addFilter(Filter.containsAll("applicationNames", appNames));
                            }
                            return queryOptions;
                          </Source>
                  </QueryScript>
                </Parameter>
                <Parameter argument="creationDate" property="parent.certification.created" valueClass="daterange"/>
                <Parameter argument="signedDate" property="parent.certification.signed" valueClass="daterange"/>
                <Parameter argument="expirationDate" property="parent.certification.expiration" valueClass="daterange"/>
              </QueryParameters>
            </DataSource>
            <Columns>
              <ReportColumnConfig field="nativeIdentity" header="rept_cert_col_account_id" property="exceptionEntitlements.nativeIdentity" sortable="true" width="110"/>
              <ReportColumnConfig field="identityName" header="Account owner name" property="parent.identity" scriptArguments="parent.certification.type,targetName" width="110">
                <RenderScript>
                  <Source>

                                import sailpoint.object.*;
                                if (value == null || Certification.Type.DataOwner.equals(scriptArgs.get("parent.certification.type"))){
                                  return scriptArgs.get("targetName");
                                }

                                Identity myIdentity = context.getObjectByName(Identity.class, value);
                                result = myIdentity.getDisplayableName();
                                return result;

                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="identity" header="rept_cert_col_identity" property="parent.identity" scriptArguments="parent.certification.type,targetName" width="110">
                <RenderScript>
                  <Source>

                                import sailpoint.object.*;
                                if (value == null || Certification.Type.DataOwner.equals(scriptArgs.get("parent.certification.type"))){
                                  return scriptArgs.get("targetName");
                                }

                                return value;

                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="manager" header="rept_cert_col_manager_name" property="parent" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.*;
                          return value != null ? ReportingLibrary.getCertificationItemManagerName(context, value) : null;
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="signerName" header="Signer Name" property="parent.certification.signOffHistory.signerName" sortable="true" width="110">
                <RenderScript>
                  <Source>

                                import sailpoint.object.*;
                    						result = "";
                                if (value != null ){

                                  Identity myIdentity = context.getObjectByName(Identity.class, value);
                                  result = myIdentity.getDisplayableName();

                    						}
                                return result;
                            // keep original header for field="signer" header="rept_cert_signoff_signer" (next field below)
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="signer" header="Signer WorkerID" property="parent.certification.signOffHistory.signerName" sortable="true" width="110"/>
              <ReportColumnConfig field="signed" header="rept_cert_signoff_signoff_date" property="parent.certification.signOffHistory.date" sortable="true" width="110"/>
              <ReportColumnConfig field="esigned" header="rept_cert_signoff_esig" property="parent.certification.signOffHistory.electronicSign" sortable="true" width="110"/>
              <ReportColumnConfig field="certName" header="rept_cert_col_cert_access_review" property="parent.certification.name" sortable="true" width="110"/>
              <ReportColumnConfig field="certGroupName" header="rept_cert_col_cert_grp_name" property="parent.certification.certificationGroups.name" sortable="true" width="110"/>
              <ReportColumnConfig field="tags" header="rept_cert_col_tags" property="parent.certification.tags.name" subQueryKey="id" width="110"/>
              <ReportColumnConfig field="entitlementType" header="rept_cert_col_entitlement_type" property="type" sortable="true" width="110"/>
              <ReportColumnConfig field="entitlement" header="rept_cert_col_entitlement_desc" property="exceptionEntitlements" scriptArguments="bundle,violationSummary,targetName" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.*;
                          return ReportingLibrary.describeCertificationEntitlement(value, scriptArgs.get("bundle"), scriptArgs.get("violationSummary"), scriptArgs.get("targetName"));
                  </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="status" header="rept_cert_col_status" property="summaryStatus" sortable="true" width="110"/>
              <ReportColumnConfig field="decision" header="rept_cert_col_decision" property="action.status" sortable="true" width="110"/>
              <ReportColumnConfig field="newValue" header="rept_cert_col_newValue" property="action" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.datasource.*;
                          return DataSourceUtil.getRemediationModifiableNewValue(value);
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="remediationCompleted" header="rept_cert_col_revoke_completed" property="action" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.ReportingLibrary;
                          import sailpoint.tools.Util;
                          import java.lang.String;

                          String status = ReportingLibrary.getRemediationStatus(context, value);

                          if (status != null && "yes".equalsIgnoreCase(status.trim())) {
                              return "Revoke Completed";
                          } else {
                              return Util.isNullOrEmpty(status) ? "" : status;
                          }

                          //return Util.isNullOrEmpty(status) ? "" : status;
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="decisionMakerName" header="rept_cert_col_decision_maker" property="action.actorDisplayName" sortable="true" width="110"/>
              <ReportColumnConfig field="decisionMaker" header="Decision Maker WorkerID" property="action.actorName" sortable="true" width="110"/>
              <ReportColumnConfig field="comments" header="rept_cert_col_decision_maker_comments" property="action" width="110">
                <RenderScript>
                  <Source>
                    import sailpoint.object.CertificationItem;
                    import sailpoint.object.CertificationEntity;
                    import sailpoint.object.CertificationAction;

			result = null; //"certItemAction is null";

                    CertificationAction a = (CertificationAction) value;
                    if (a != null) {

                    	result = null; //"certItemAction comment is null";	

                      String c = a.getComments();
                      if (c != null && c.trim().length() > 0) return c;
                    }

                    CertificationItem item = null;

                    if (object instanceof CertificationItem) {
                      item = (CertificationItem) object;
                    } else if (row instanceof CertificationItem) {  
                      item = (CertificationItem) row;
                    }

                    if (item != null) {

                      CertificationEntity e = item.getEntity();
                      if (e != null) {

                    		result = null; //"cert entity action or comment is null";

                        CertificationAction ea = e.getAction();
                        if (ea != null) return ea.getComments();
                      }
                    }

                    return result;
                  </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="historyComments" header="Other Comments" property="id" width="110">
                <RenderScript>
                  <Source>
      import sailpoint.object.CertificationItem;
      import sailpoint.object.CertificationLink;
      import sailpoint.object.EntitlementSnapshot;
      import sailpoint.object.Filter;
      import sailpoint.object.IdentityHistoryItem;
      import sailpoint.object.QueryOptions;
      import sailpoint.tools.Util;

      String comments = null;
      Iterator iterator = null;

      try {

        if (value == null) {
          return null;
        }

        CertificationItem certificationItem =
          context.getObjectById(CertificationItem.class, (String) value);

        if (certificationItem == null ||
            certificationItem.getCertification() == null ||
            certificationItem.getCertification().getId() == null) {
          return null;
        }

        String certificationId = certificationItem.getCertification().getId();
        String identityName = certificationItem.getIdentity();

        if (identityName == null) {
          return null;
        }

        String applicationName = null;
        String nativeIdentity = null;

        EntitlementSnapshot entitlementSnapshot =
          certificationItem.getExceptionEntitlements();

        if (entitlementSnapshot != null) {
          applicationName = entitlementSnapshot.getApplicationName();
          nativeIdentity = entitlementSnapshot.getNativeIdentity();
        }

        QueryOptions qo = new QueryOptions();
        qo.addFilter(Filter.eq("type", "Comment"));
        qo.addFilter(Filter.eq("identity.name", identityName));

        iterator = context.search(IdentityHistoryItem.class, qo, "id");

        StringBuffer buffer = new StringBuffer();

        while (iterator != null && iterator.hasNext()) {

          Object[] row = (Object[]) iterator.next();

          if (row == null || row.length == 0 || row[0] == null) {
            continue;
          }

          IdentityHistoryItem historyItem =
            context.getObjectById(IdentityHistoryItem.class, (String) row[0]);

          if (historyItem == null) {
            continue;
          }

          CertificationLink certificationLink = historyItem.getCertificationLink();

          if (certificationLink == null ||
              !Util.nullSafeEq(certificationLink.getId(), certificationId)) {
            continue;
          }

          if (applicationName != null &&
              historyItem.getApplication() != null &&
              !Util.nullSafeEq(historyItem.getApplication(), applicationName)) {
            continue;
          }

          if (nativeIdentity != null &&
              historyItem.getNativeIdentity() != null &&
              !Util.nullSafeEq(historyItem.getNativeIdentity(), nativeIdentity)) {
            continue;
          }

          String historyComment = historyItem.getHistoryComments();

          if (historyComment != null) {
            if (buffer.length() > 0) {
              buffer.append(" | ");
            }
            buffer.append(historyComment);
          }
        }

        if (buffer.length() > 0) {
          comments = buffer.toString();
        }

      } finally {
        if (iterator != null) {
          Util.flushIterator(iterator);
        }
      }

      return comments;
    							</Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="application" header="rept_certification_form_application" property="exceptionEntitlements.application" sortable="true" width="110"/>
              <ReportColumnConfig field="exclusionExplanation" header="rept_cert_col_exclusion_explanation" property="parent.certification" width="110">
                <RenderScript>
                  <Source>

                          if (value != null){
                            List archivedEntities = value.fetchArchivedEntities(context);
                            if (archivedEntities != null
                                && archivedEntities.size() > 0
                                && archivedEntities.get(0) != null) {
                              return archivedEntities.get(0).getExplanation();
                            }
                          }

                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="recommendation" header="rept_cert_col_recommendation" property="recommendValue" scriptArguments="id" sortable="true" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.ReportingLibrary;
                          return ReportingLibrary.getRecommendedDecision(context, value, scriptArgs.get("id"), locale, timezone);
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="recommendationReasons" header="rept_cert_col_recommendation_reasons" property="id" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.ReportingLibrary;
                          return ReportingLibrary.getRecommendationReasons(context, value, locale, timezone);
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
              <ReportColumnConfig field="recommendationTimestamp" header="rept_cert_col_recommendation_timestamp" property="id" width="110">
                <RenderScript>
                  <Source>
                          import sailpoint.reporting.ReportingLibrary;
                          return ReportingLibrary.getRecommendationTimestamp(context, value, locale, timezone);
                        </Source>
                </RenderScript>
              </ReportColumnConfig>
</Columns>

@avtan AFAIK you can only rely on IdentityHistoryItem and it should be fine, But please test it out with the certification you already have in your system.

Note: Found a fix? Mark it as a solution! Want to say thanks? Give Kudos to anyone who helped you understand or solve the problem. Thanks..!!!

thanks neel193

Hello @avtan,

Thanks for sharing this, it’s really helpful! I like how you handled both CertificationAction and IdentityHistoryItem comments and explained it clearly. This will be really beneficial to the community!

Keep up the great work.