> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/governance/certification/certification-model.md).

# The certification model

The certification object model mapped onto the tables that store it — five objects, the separate database, and the two things both called "decision source".

## What this is, and when you need it

Certification screens hide the object model behind friendly words — "campaign", "reviewer", "decision". When you need to report on a campaign, explain an outcome to an auditor, or work out why a user is missing from a review, you need the object underneath and the table it lives in. That is what this page gives you. Every other page in this section assumes it.

The SoD object model has moved to [Segregation of duties](/administration-guide/governance/sod.md), and risk scoring is owned by [Risk management](/administration-guide/risk.md).

## Certification: five objects, in order

**Certification** is the *definition* — the reusable configuration. One row in `CERTIFICATION`. It holds the type, the schedule, the scope selection, the reviewer steps, the expiration policy and the auto-certify policy. It never contains decisions.

**Campaign** is one *execution* of a certification. One row in `CAMPAIGN`, with `CERTIFICATION_ID` pointing back at the definition. A campaign copies the settings it must not lose if the definition is edited later — `AUTO_CERTIFY_ENABLED`, `AUTO_CERTIFY_MAX_SCORE`, `AUTO_CERTIFY_VISIBILITY`, `ALLOW_BULK_APPROVAL`, `BETA_FEATURES_ENABLED` and `MIN_MEMBERSHIP_RISK` are all per-run copies captured at launch. This is why editing a certification does not change a running campaign.

**Reviewer steps** exist twice, deliberately. On the definition they are `CERTIFICATION_REVIEWER` rows (one per step, with `REVIEWER_LEVEL`, the reminder cadence and the mail template) plus `CERTIFICATION_REVIEWER_ESCALATION` rows for the escalation chain. On the campaign they are `CAMPAIGN_STEP` rows (`STEP_LEVEL`, `TOTAL_COUNT`, `COMPLETED_COUNT`) whose *resolved* human reviewers are the `CAMPAIGN_REVIEWERS` rows keyed by `STEP_ID`. `CAMPAIGN_REVIEWERS` is the durable record — it is what still associates a reviewer with a campaign after the Activiti task is gone, so a completed campaign remains visible to the people who reviewed it.

**Review item** is one user × entitlement row to be decided. `ACCESS_REVIEW_ITEM`, one row per pair, with `ROLE_ID` or `GRP_ID` set (never both), the entitlement metadata, and a denormalized snapshot of the user taken at launch (`USER_FIRST_NAME`, `USER_LAST_NAME`, `USER_PRINCIPAL`, `USER_SUPERVISOR_ID`, `USER_STATUS`). The snapshot is why a historical campaign still reads correctly after the user is renamed or terminated.

**Decision** is recorded in two places. The item carries the current state — `ACCESS_REVOKED`, `ACCESS_REVOKED_BY`, `DECISION_DATE`, `REVIEW_OUTCOME`, `AUTO_CERTIFIED` — and `ACCESS_REVIEW_ITEM_AUDIT` carries one row per reviewer action, with `REVIEWER_ID`, `AUDIT_SPECIFICATION` (the step status the action left behind), `REVIEW_DATE`, `REVIEW_NOTE`, `CAMPAIGN_STEP_ID` and `DECISION_SOURCE`. `DECISION_SOURCE` is one of `INDIVIDUAL`, `BULK` or `AUTO`. Auditors ask which items were waved through in bulk; that column is the answer, and it is the only place the distinction is kept — no screen shows it.

{% hint style="warning" %}
**Two different things are called "decision source", and only one of them is a column.**

|                                    | Values                                  | Where it lives                                                              |
| ---------------------------------- | --------------------------------------- | --------------------------------------------------------------------------- |
| **How the decision was submitted** | `INDIVIDUAL`, `BULK`, `AUTO`            | Stored, in `ACCESS_REVIEW_ITEM_AUDIT.DECISION_SOURCE`                       |
| **What produced the decision**     | `REVIEWER`, `EXPIRATION_POLICY`, `NONE` | **Derived at read time** for the Campaign History view. Not stored anywhere |

They are not alternative values of one field, and the second set never appears in `ACCESS_REVIEW_ITEM_AUDIT.DECISION_SOURCE`. A query looking for `DECISION_SOURCE = 'EXPIRATION_POLICY'` returns nothing, on every release. To find policy-driven revocations in SQL, test the actor instead: `ACCESS_REVIEW_ITEM.ACCESS_REVOKED_BY` holding the system user id (`0001`) is what "the clock revoked it" looks like in the data.
{% endhint %}

Two supporting tables complete the picture:

| Table                              | What it holds                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CAMPAIGN_PREVIEW_EXCLUSION`       | Everything the launch *dropped* — one row per excluded user or access item, carrying the grain of the exclusion (`USER` or `ACCESS`), its category (`USER_LEVEL`, `ACCESS_DROP` or `REVIEWER`) and the reason the preview gave, from the list in [Campaign preview exclusion reasons](/administration-guide/reference/enums.md#campaign-preview-exclusion-reasons) |
| `ACCESS_REVIEW_ITEM_SOD_VIOLATION` | The SoD violations attached to a review item, so the reviewer sees the conflict at the moment of decision                                                                                                                                                                                                                                                          |

An item can be waiting on more than one reviewer, so the current reviewers are held as their own rows in `ACCESS_REVIEW_ITEM_CUR_REV` rather than as a column on the item.

### Reading a campaign end to end

A finished campaign's full story is the **union** of two frozen tables: the exclusion slice (`CAMPAIGN_PREVIEW_EXCLUSION`) and the reviewed slice (`ACCESS_REVIEW_ITEM` plus its audit rows). Every access slice in that union ends up with one of four outcomes:

| Outcome        | Meaning                                                                                                    |
| -------------- | ---------------------------------------------------------------------------------------------------------- |
| `EXCLUDED`     | Never entered the campaign — dropped by a scope filter, a dropped-access rule, or an unresolved reviewer   |
| `CERTIFIED`    | Reviewed and kept                                                                                          |
| `REVOKED`      | Reviewed (or expired) and removed                                                                          |
| `NOT_REVIEWED` | Campaign ended with the item un-actioned and the expiration policy's on-expiry action left at `DO_NOTHING` |

A `REVOKED` or `NOT_REVIEWED` outcome also records who decided it: `REVIEWER`, `EXPIRATION_POLICY` or `NONE`. OpenIAM derives that from the revoking actor, so you can read it yourself — `ACCESS_REVIEW_ITEM.ACCESS_REVOKED_BY` holding the system user id means the expiry policy did it, any other id means a human did.

**"Nobody reviewed this" and "nobody was asked to review this" are different outcomes, in different tables.** If you report only on `ACCESS_REVIEW_ITEM` you will silently omit every exclusion, which is the most common way a certification report overstates coverage.

## The certification database is separate

Certification does not live in the `openiam` schema. It has its own database, with its own schema creation for MySQL, PostgreSQL, MS SQL Server and Oracle, reached through its own datasource:

| Property                                                   | Shipped value                                                                                                                                                           |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jdbc.certification.url`                                   | `jdbc:mysql://${jdbc.host}:${jdbc.port}/certification?autoReconnect=true&useUnicode=true&characterEncoding=utf8&connectionCollation=utf8_general_ci&serverTimezone=UTC` |
| `certification.databaseSchema.name`                        | `certification`                                                                                                                                                         |
| `vault.secret.certification.jdbc.username` / `...password` | supplied from Vault                                                                                                                                                     |

These three are deployment properties, not `SYS_PROP` rows, so you will not find them under **Administration → System Configuration** and you cannot confirm their live values from the Webconsole. If you need to know which database the certification service is actually talking to, ask the deployment owner — the [Installation Guide](/installation/installation.md) owns where these are set, including the Helm value that names the database.

Two consequences you will meet as an administrator. A SQL report that joins a campaign to a role or a user is a cross-database join, because only one side is in `openiam`. And a database backup that covers `openiam` alone does not contain a single certification, campaign or decision — that is the most common way a governance history is lost, and it is worth checking against your own backup scope today.

### Migrating from the old schema

Before this split, the same data lived in the `openiam` schema as `ACCESS_CERTIFICATION*`, and certification reviewers were `APPROVER_ASSOC` rows with `ASSOCIATION_TYPE = 'CERTIFICATION'`. A one-shot migration copies it forward on the upgrade that introduces the certification service, and it is **off by default** — `org.openiam.certification.migrate-from-openiam` ships `false`, and it is a deployment property of the certification service rather than a `SYS_PROP` row, so turning it on is the deployment owner's job. See the [Installation Guide](/installation/installation.md).

What you need to know as the administrator who inherits the result:

* **The symptom of a skipped migration** is a certification list that is empty, or missing everything created before the upgrade, on a system whose old `ACCESS_CERTIFICATION` rows are still in `openiam`. Nothing warns you; the new service simply starts with no history. Check the list at **Access Certification** immediately after an upgrade, and compare its count against what you had.
* **Re-running it is safe.** The migration stops as soon as it finds rows in the target `CERTIFICATION` table, so a deployment that leaves the property on does not accumulate duplicates.
* **Identifiers survive.** Campaign and reviewer ids are preserved, which is what allows workflow processes that were in flight across the upgrade — they hold those ids as process variables — to keep working.
* **The user snapshot columns on `ACCESS_REVIEW_ITEM` cannot be recovered.** They did not exist in the legacy schema, so they stay `NULL` on migrated rows and a report over migrated campaigns will show blank user names. Fall back to the live `USERS` row for those campaigns, and accept that it reflects the user as they are now, not as they were when the campaign ran.

The rename map, for reconciling old exports against new ones, is in [Certification database tables](/administration-guide/reference/certification-tables.md#the-legacy-rename-map). `ACCESS_CERTIFICATION_RISK_TYPES` / `CERTIFICATION_RISK_TYPES` is a dead end there: the risk-event-type mechanism was retired and the table dropped. Its replacement, `RISK_SCORE_THRESHOLD`, was itself dropped in 2026.9.1.0; risk scoping is now `MIN_MEMBERSHIP_RISK`.

## How certification consumes risk

Risk scoring is owned by `risk-manager` and stored in the `openiam` schema — its own tables are documented in [Risk management](/administration-guide/risk.md). Certification consumes it in three ways. A campaign drops the grants whose membership score is below `CERTIFICATION.MIN_MEMBERSHIP_RISK`. Each review item records `RISK_SCORE`, `RISK_BAND` and `RISK_JUSTIFICATION` at build time, keeps those launch values, and refreshes the current score while the item is undecided. And the auto-certify policy uses that item score to record a `CERTIFIED` decision without a human.

Scores are recomputed by the `RISK_NIGHTLY_SWEEP` batch task, which ships **enabled** at `0 0 2 * * *` against the bean `riskNightlySweepInitializer.perform`. Confirm it on **Administration → Batch Tasks**: unlike the reminder and escalation tasks, this one is on when you install, so the usual surprise is scores that moved overnight, not scores that never move. Configuration is on [Risk policies](/administration-guide/risk/policies.md).

## Values you will meet on screen

Full lists are in [Enumerated values](/administration-guide/reference/enums.md), and every SoD value is in [SoD rule and policy reference](/administration-guide/reference/sod-rules-and-rule-sets.md#the-values-you-will-meet). The certification ones that decide behaviour:

| Where you meet it                                                 | Values                                                                                                  |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| The campaign type, fixed once the definition is saved             | `APPLICATION_CERTIFICATION`, `USER_CERTIFICATION` — risk is a filter, not a type                        |
| The status shown against a campaign                               | `STARTED`, `IN_PROGRESS`, `AWAITING_SIGN_OFF`, `COMPLETED`, `EXPIRED`, `PARTIALLY_EXPIRED`, `CANCELLED` |
| The on-expiry action on the expiration policy                     | `DO_NOTHING` (default), `REVOKE_ALL_ACCESS`, `REVOKE_ONLY_ALREADY_REVOKED_ACCESS`                       |
| Auto-certify visibility to the reviewer                           | `SHOWN_AS_PREAPPROVED` (default), `HIDDEN` — only the default is offered in the UI                      |
| A review item's outcome                                           | `EXCLUDED`, `CERTIFIED`, `REVOKED`, `NOT_REVIEWED`                                                      |
| How it was submitted (`ACCESS_REVIEW_ITEM_AUDIT.DECISION_SOURCE`) | `INDIVIDUAL`, `BULK`, `AUTO`                                                                            |
| What produced it (Campaign History, derived — not a column)       | `REVIEWER`, `EXPIRATION_POLICY`, `NONE`                                                                 |

## Verification

Governance actions are auditable by constant. The ones worth saving as searches:

| Audit action                                                                  | Emitted when                                                |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `CREATE_CERTIFICATION` / `MODIFY_CERTIFICATION`                               | A certification definition is saved                         |
| `EXECUTE_CERTIFICATION`                                                       | A preview is launched, or a campaign is executed directly   |
| `MAKE_ACCESS_REVIEW_ITEM_DECISION`                                            | One reviewer decides one item                               |
| `SUBMIT_BULK_ACCESS_REVIEWS`                                                  | A bulk decision batch is submitted                          |
| `COMPLETE_CERTIFICATION_REVIEW`                                               | A user's review step settles and provisioning is dispatched |
| `FORCE_EXPIRE_CERTIFICATION_CAMPAIGN` / `EXTEND_CERTIFICATION_CAMPAIGN`       | An administrator expires or extends a campaign              |
| `SOFT_SOD_POLICY_VIOLATION_DETECTION` / `HARD_SOD_POLICY_VIOLATION_DETECTION` | A violation is detected                                     |
| `RISK_NIGHTLY_SWEEP` / `RISK_SCORE_EVALUATION`                                | Risk is recomputed                                          |

Retention for all of them is governed by `org.openiam.audit.log.prune.days.*` — see [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md).

## When it goes wrong

| Symptom                                                                                 | Cause                                                                                                                                            | Fix                                                                                                                                                                       |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A report built from `ACCESS_REVIEW_ITEM` shows 100% coverage that the business disputes | Excluded users and dropped items live in `CAMPAIGN_PREVIEW_EXCLUSION`, not in the item table                                                     | Union both tables, or use the shipped scope report — see [Campaign history and evidence](/administration-guide/governance/certification/campaign-history-and-evidence.md) |
| A SQL join between a campaign and a role or user returns nothing                        | `CAMPAIGN`/`ACCESS_REVIEW_ITEM` are in the `certification` database; `ROLE`/`USERS` are in `openiam`                                             | Cross-database join, or read the denormalized snapshot columns already on `ACCESS_REVIEW_ITEM`                                                                            |
| A completed campaign is invisible to the people who reviewed it                         | Reviewer resolution is being read from live workflow tasks instead of `CAMPAIGN_REVIEWERS`; the workflow tasks are gone once the campaign closes | Query `CAMPAIGN_REVIEWERS` joined to `CAMPAIGN_STEP` — that is the durable record                                                                                         |
| Editing a certification did not change the running campaign                             | `AUTO_CERTIFY_*`, `ALLOW_BULK_APPROVAL` and `BETA_FEATURES_ENABLED` are per-run copies on `CAMPAIGN`                                             | Expected. Change the definition and launch a new campaign                                                                                                                 |
| A runbook references `CERTIFICATION_RISK_TYPES` or a risk-event-type picker             | Retired as of 2026.7.1.0; the table is dropped, so the query errors rather than returning nothing                                                | Scope on `RISK_SCORE_THRESHOLD` instead — see [Access certification and risk](/administration-guide/risk/certification.md)                                                |
| An SoD exemption cannot be saved                                                        | The policy has `EXCEPTIONS_ALLOWED = 'N'`                                                                                                        | Enable exceptions on the `SOD_POLICY` row, or use a mitigating control instead                                                                                            |

## Related

* [Plan and scope a campaign](/administration-guide/governance/certification/plan-and-scope-a-campaign.md) — the first task on this model
* [What your reviewers see](/administration-guide/governance/certification/the-reviewer-experience.md) — how these objects reach the people deciding
* [Risk management](/administration-guide/risk.md) — how risk is configured, and what certification reads from it
* [Operate certification services](/administration-guide/governance/certification/operate-certification-services.md) — running the separate service and database
* [Certification database tables](/administration-guide/reference/certification-tables.md) — every certification table and column, and the legacy rename map
* [Enumerated values](/administration-guide/reference/enums.md) — every value listed above, with the field it appears in

*Applies to OpenIAM 2026.9.1. Last reviewed 14 September 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/governance/certification/certification-model.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
