Skip to content

Policy Acknowledgements

Enterprise feature — requires the enterprise image and a license with the Policy acknowledgements entitlement. See Enterprise edition.

Some documents aren’t just good to read. People are required to read them: security policies, code-of-conduct updates, safety procedures. CompassDocs can ask every reader to confirm, tie each confirmation to the exact version they read, tell people once when the text changes, and keep the record for as long as an auditor might ask for it.

This page describes read confirmation as it works from 1.14.0. If you are upgrading, read Upgrading to 1.14.0 and, from 1.12.x or earlier, Upgrading to 1.13.0, Upgrading to 1.12.0, Upgrading to 1.11.0, Upgrading to 1.10.1 and Upgrading to 1.10.0 first: your existing confirmations stay current, but the rules for what asks people again changed in 1.10.0, from 1.10.1 people are told when their confirmation lapses, from 1.11.0 every integration that edits the text of a policy must say what readers should see, from 1.12.0 every confirmation keeps the words the person agreed to, a policy can ask everyone again on an interval or be retired, and /compliance is everyone’s own page, from 1.13.0 every person a policy asks has a due date of their own, is reminded on a cadence without anyone clicking, and can be exempted, extended, snoozed or asked again one at a time, and from 1.14.0 the portal says what to do next, every person has one compliance record, policies are grouped into sets, the programme is snapshotted every month with a hash an auditor can verify, group leads see where their team stands, and a chat channel can hear the counts.

CompassDocs uses one word for each thing, on every screen and in every email:

  • The feature is read confirmation. A reader confirms a document, and re-confirms it after a material change.
  • The sentence a reader agrees to is the confirmation statement (“I confirm that I have read and understood this document.” unless the workspace changed it under Program settings, or the policy carries its own on its Settings tab).
  • A manager asks everyone to re-confirm; the reset is what happens, not what the manager clicks. The control is Ask everyone to re-confirm… wherever it appears.
  • The evidence row is the attestation record; the page that lists them is the record.
  • Status chips read Confirmed, Pending, Overdue or Exempt (the last two from 1.13.0), nothing else.
  • Versions are numbered as in Version history: version 12 is shown as v12.
  • A policy is a document that requires read confirmation. The word appears only on the compliance pages; on the document page it is always “this document”.
  • Counts are always of people: “42 of 50 people have confirmed”.
  • A group of policies is a set (1.14.0): never a pack or a bundle, except inside an example name an administrator types (“Annual pack”).
  • Every person has one compliance record (1.14.0): everything they must confirm, have confirmed and were told, on one page and in one CSV.
  • Needs attention (1.14.0) is the Overview’s queue of people and policies with a reason to act, one reason each; a snooze hides a row from it and pauses reminders, and changes nothing else.

The older word, acknowledgement, survives only where renaming it would break something: API paths (/api/documents/{id}/ack, …/acknowledgements), audit keys, permission keys and the licence entitlement name.

On any document (the switch is disabled until it is published), approvers and admins (anyone holding document.ack_require in the space) see a shield icon in the document’s action row, Read confirmation settings. It is tinted while the requirement is on. Clicking it opens a small popover headed Read confirmation with exactly four things and, from 1.13.0, one link; nothing else is ever added to it (per-policy settings live on the Settings tab of the record page: who must confirm, the deadline, the reminders, the confirmation statement and signature, the re-confirmation interval and effective date and the lifecycle):

  1. Require readers to confirm, a switch. It is disabled with the help “Publish first” until the document is published. On a retired policy the switch gives way to Retired 3 Mar 2026 · un-retire it on the Settings tab, with the tab linked: Un-retire on the Lifecycle card is the only way back.
  2. A status line while the requirement is on: 42 of 50 confirmed · v12 · asked 3 Mar 2026 by Matt, with a View record link to the record. After a quiet switch-on it reads Required 3 Oct by Matt (no notice sent); once the popover is reopened it reads 0 of 42 confirmed · v12 · no notice sent yet until someone is asked.
  3. When the text changes, the document’s edit default (below).
  4. Ask everyone to re-confirm…, the deliberate re-ask (see Asking everyone to re-confirm).

From 1.13.0 the popover also carries one link, All settings, to the document’s Read confirmation Settings tab (shown to holders of document.ack_require on the space or compliance.program_manage; on a retired policy the Retired line already links the tab). The audience, the deadline and the reminders are set there, never in the popover.

The switch never flips by itself. Turning it on first asks one question:

Require read confirmation? Everyone who can see this space (42 people) will be asked to read and confirm this document.

with three answers:

  • Require and notify: the requirement is switched on and everyone who can see the document is told, within a minute, by a dashboard notice and an email with a link straight to the confirmation card (see How notices are sent). The toast says how many: “Read confirmation required: 42 people will be notified.”
  • Require quietly: the requirement is switched on and nobody is told. The right choice while you are still assembling a set of policies, or when a reminder will follow anyway. The audit entry records notify: false and the audience count.
  • Cancel (or Escape): nothing changes and the switch stays off.

The count is the audience at that moment. Switching the requirement on asks the policy’s audience: by default everyone who can see its space (active accounts; in a private space, the granted groups), or, from 1.13.0, the groups, people and roles named on the policy’s Settings tab (see Who must confirm). Each person gets an assignment row the moment the requirement is switched on; the request notice goes to the people on those rows. People who join later are enrolled as they arrive (see New joiners and directory groups). It is the same question, in the same words, that the compliance portal asks (see The central compliance portal), so the two paths cannot drift.

Once the requirement is on:

  • Every person in the audience is asked to confirm it, on the document itself (see Reading and confirming).
  • Everyone with pending confirmations sees a row on their dashboard: “3 policies need your read confirmation” (from 1.13.0 ending “· 1 overdue” when any is), naming up to three policies with their deadlines, the overdue ones first (“Expense policy (due 29 Oct 2026)”, after the space’s icon). It opens the policy’s confirmation card when one policy is pending and My policies otherwise. The count is the whole list (until 1.12.0 it stopped at twenty).
  • Each confirmation is recorded with the person, the time, their IP address, the statement they agreed to, the version they confirmed and a SHA-256 of that version’s text, and also lands in the audit log as document.acknowledge.
  • Approvers see live progress in the popover’s status line and, in full, on the record.

When the text changes holds one of three values. A policy receives the workspace’s choice (marked “(workspace default)” in the list; set under Program settings) when it is switched on while still at the built-in value, Ask the editor each time, and can choose differently for itself at any time.

Value What happens when someone saves a text or title change
Ask the editor each time (recommended) Every text save opens the editor’s question (editorial fix or material change, see Editing a confirmed document). The editor decides and the record keeps the choice. The help under the editor’s Change note says “Saving will ask whether readers must re-confirm.”
Ask only when a change looks material Good for procedures that change often. A save whose change looks editorial with confidence (under 2% of the words, in at most two paragraphs, no heading, number, list item or title change) is saved without the question: confirmations are kept, the History tab records a Kept row with source Default and the editor’s toast reads “Saved as an editorial fix (document setting). Confirmations kept.” Anything that looks material or borderline (a title, heading or number change, 5% or more of the words, three or more paragraphs, or cumulative drift since the confirmed version) still opens the question. Nothing passes silently: every kept edit is a row on the History tab.
Always ask everyone to re-confirm Good for signed-off policies. No question: every text save asks everyone to re-confirm, the change note is required, and the toast reads “Saved. 42 people will be asked to re-confirm.”

Writers that cannot ask the question in the editor take the decision another way. The v1 API and the Claude connector must say keep or reset with a reason on every change to the text or title of a published policy (and on publishing such a draft whose text drifted), or they are refused with the standing and the measured change so a person can be asked (see the API reference and the connector). An import leaves such a policy (a published one, or a paused one the zip would publish) unchanged unless Also overwrite documents that require read confirmation is ticked, and then follows the edit default. A branch merge, a restore and an approved change request ask the person doing them (1.11.0). A scheduled publish follows the edit default, since nobody is there when it fires; the editor warns under the schedule field beforehand.

The compliance portal shows the value on each policy row as a chip, Asks editor, Asks when material or Always re-asks, with the full sentence in its tooltip. Changing the value is written to the audit log as ack.edit_default_set with the old and new values.

Turning the requirement off is reversible, so it tells rather than asks: the toast “Read confirmation no longer required for this document” carries Undo, which switches it back on without notifying anyone. Every record is kept. The audit entry document.ack_removed records the standing at that moment (how many had confirmed, of how many).

Turning it back on later asks people again only if the text changed in between (see What asks everyone again). The compliance portal can switch a policy off as well: Stop requiring on the policy’s row does the same thing, with the same toast and the same Undo.

Unpublishing a document that requires read confirmation does not switch the requirement off and does not reset anyone. The editor’s Unpublish this document? question says so: “Read confirmation is paused while the document is unpublished. Confirmations are kept.” While the document is a draft:

  • it is listed in the compliance portal under a collapsed Paused (unpublished) group with an Unpublished chip and its standing (“3 of 5 people confirmed”), and offers no action there: Remind, Ask everyone to re-confirm…, Stop requiring and the record download all need a published document;
  • it is out of the portal’s tiles, out of the People table and off the dashboard’s “documents need your read confirmation” row, because nobody owes a policy that is down;
  • its record is untouched: every confirmation stays current against the confirmation marker, and the Read confirmation page and its CSV still show them.

Publishing it again with the text people confirmed puts it straight back in the live list with the same counts and asks nobody. Publishing it with changed text is a publish of a drifted draft (see what asks everyone again): the editor is asked, or the edit default decides for a caller that cannot ask. To stop requiring confirmation while a policy is unpublished, use the shield on the document (PATCH /api/documents/{id}/ack { "required": false }); the portal’s Stop requiring needs a published document. An import whose zip would publish a paused policy is held off by the same checkbox as a published one (see Import & export).

A scheduled unpublish pauses the policy the same way and writes document.scheduled_unpublish to the audit log with ack_paused: true.

Retiring is the end of a policy’s life as a policy, with the evidence kept. Switching the requirement off drops the policy from the portal and leaves only audit rows; retiring keeps it on the record as Retired, says why, and names the policy that replaced it. Auditors ask exactly that: the text in force on a date, who had confirmed it when it ended, and what came next.

Retire policy… sits in the Lifecycle card of the document’s Read confirmation Settings tab, and on the policy’s row in the compliance portal. The card also holds Replaced by: a picker over the published documents you can see, saved with the page’s Save settings row like any other setting (it can be set before or after retiring; un-retiring clears it, since a policy that is live again is not replaced by anything). Retire policy… asks first: Retire “Expense policy”?, with the standing (“42 of 50 people have confirmed version 12.”), what retiring does and does not do, and a reason of at least ten characters, shown on the compliance record and in exports. Retiring a policy:

  • switches the requirement off and records who retired it, when and why, in one step; a policy that is already retired answers “This policy is already retired.”;
  • leaves the confirmation marker where it is, so the final roster is reproducible from the ledger;
  • cancels the policy’s queued notices (a reminder or re-ask that had not gone out yet);
  • writes a retired row to the decision ledger with the standing frozen at that moment: how many had confirmed, of how many, and which version;
  • writes ack.retired to the audit log with the reason, the replacement, the counts and the version;
  • from 1.14.0, stores a retirement snapshot of the policy’s record as it stood at that moment (kind Retirement on Compliance › Evidence; a snapshot that fails never fails the retire and is audited as ack.snapshot_failed; see Evidence: snapshots and the audit package) and tells any chat channel subscribed to Policy retired (see Chat channels).

It does not change the document: it stays published (or unpublished, if it was paused) and readable. Unpublish it separately if people should no longer read it. A paused policy can be retired too.

What people see: the confirmation card and the sticky control disappear, My policies no longer lists the policy, and the notices strip at the top of the document shows one neutral row for everyone: “Retired 3 Mar 2026 · replaced by Expense policy 2026”, with the replacement as a link when the reader may open it, or “Retired 3 Mar 2026” alone.

On the Settings tab the Lifecycle card then reads Retired 3 Mar 2026 by Mia Manager with the reason, and offers Un-retire. After Retire policy… or Un-retire, keyboard focus lands on the button that took the other’s place.

In the portal (Compliance › Policies), retired policies sit in a collapsed Retired · 3 group at the foot of Policy progress, below Paused, each with a Retired 3 Mar 2026 chip (who retired it is the chip’s tooltip), its final standing (“42 of 50 people confirmed v12”: the counts and the version frozen on the ledger row), the reason, Replaced by with the replacement as a link when one was named, View record (the policy’s Read confirmation page, which stays open with its History tab) and the per-policy CSV for exporters. There is no Remind, no re-ask and no Stop requiring. A retired policy is out of the Overview’s tiles and never offered by Request read confirmation; switching it back on through the shield answers “This policy is retired. Un-retire it first.”

Retire policy… on a live row of the same list opens the same question as the Settings tab’s Lifecycle card and, once answered, moves the row to the Retired group with the toast “Policy retired: 42 of 50 confirmations on record”. Un-retire on a retired row asks once (“Resume read confirmation for …?”, Resume) and toasts “Policy resumed: 42 of 50 confirmations still current”.

Un-retire (the portal group and the Settings tab) asks first, Resume read confirmation for “Expense policy”? (“Confirmations made before it was retired stay on record. If the text changed while it was retired, people whose confirmation is older than the current version are asked again.”), then clears the retirement and switches the requirement back on. Confirmations made before it was retired stay on record and count again; if the text changed while it was retired, people whose confirmation is older than the current version are asked again (the marker followed the text while the requirement was off, as it does for any document that asks nobody). A re-confirmation cycle that fell due while the policy was retired is moved a whole interval out, so nobody is asked again the day it comes back. Stop requiring followed by a new Request read confirmation works the same way: the policy keeps its own interval and its next date, and a date that passed while the requirement was off is moved a whole interval out rather than firing at the next hourly check on top of the request. An unpublished document resumes as Paused. The audit log gets ack.unretired. The toast reports the standing, Policy resumed: 42 of 50 confirmations still current, and, while anyone’s confirmation is still current, carries Ask everyone to re-confirm… as its action, on the Settings tab and in the portal alike: the page header’s question and reset, from the toast, for the case where that is the wrong number.

API: POST /api/documents/{id}/ack/retire { "reason": "…" } answers { ok, acked, required, rev }; POST /api/documents/{id}/ack/unretire answers { ok, current, eligible }; both need document.ack_require on the space or compliance.program_manage, and are 403 to a program_read-only role. PATCH /api/documents/{id}/ack-settings { "superseded_by": id | null } sets the replacement: the target must be a published, live document in your scope and not the policy itself (400 “A policy cannot be replaced by itself.” / “The replacement must be a published document you can see.”); GET answers superseded_by and superseded_by_title.

Trashing a retired policy is allowed. A trashed document with confirmations on record is never auto-purged (see Retention and leavers).

From 1.13.0 every policy has an audience: the people it asks, kept as one row per person (its assignment rows). There are two modes, chosen in the Who must confirm card of the policy’s Read confirmation Settings tab and saved with the page’s one Save settings row.

The default, and what every policy did before 1.13.0: every active account that can see the policy’s space. In a public or internal space that is everyone with an account; in a private space it is the people the space is granted to (directly or through a group) and anyone who can see every space. New accounts, people added to a granted group and people given access to the space are enrolled as they arrive. Saving this mode clears any rules the policy held (exclusions included), so a later switch to targeted mode starts from nothing; Exempt the document’s author and approvers applies in both modes.

Targeted mode names who is asked: groups (Entra, Google or manual), people by name, and roles (Viewer, Editor, Approver, Admin). Exclusions take people out again (they apply in targeted mode only), and Exempt the document’s author and approvers leaves out whoever wrote the document (the byline, matched through the directory’s linked names rather than the bare spelling, and anyone who published a version of it) and the space’s approvers. A person who matches an include rule and no exclusion is asked; nobody else is.

A rule written by a policy set (1.14.0) is tagged with the set: the person’s row carries source set on their compliance record and in the exports, and deleting the set leaves the rule in place as if a manager had written it.

An audience can only narrow who is asked, never widen it. Every rule is intersected with who can see the policy’s space: a person a rule names who cannot see the space is not enrolled (the card’s preview counts them as “without access”) and is not required until access is granted in Space settings (see Spaces & permissions), at which point they are enrolled without anyone saving the policy again. Private spaces still count only people with access.

From 1.18.0 the ceiling also takes in everyone who reads the policy through a published handbook: a handbook’s audience reads the policy there even when they cannot open its space, so they are within the ceiling too. A private-space policy that asks everyone who can see its space, placed in a handbook for everyone, therefore asks the whole workspace; see Handbooks and who is asked.

The Who must confirm card on the policy’s Read confirmation Settings tab holds a stacked choice, saved with the page’s one Save settings row:

  • Everyone who can see this space: every active account with access to the space, and anyone who gains access later (what every policy did before 1.13.0).
  • Specific groups, people and roles: the pickers Groups (each group marked Entra, Google or Manual), People, the Roles checkboxes (Viewer, Editor, Approver, Admin) and Exclude, one box for groups and people left out even when a rule names them. A targeted audience that names nobody is refused before anything is saved (“Name at least one group, person or role.”). Under People the card states the ceiling: People must be able to see this document’s space. Someone who cannot is shown as “No access” in the preview and is not required until access is granted in Space settings.

Two checkboxes apply in both modes: Exempt the document’s author and approvers (off by default) and Enrol new joiners automatically (on by default; New accounts and people added to the audience get 14 days from the day they join, the number being the resolved new-joiner days, see Deadlines and defaults; the audience a switch-on enrols, and the rows the upgrade wrote, get the policy’s days to confirm instead).

The line under them is live: Applies to 412 people · 3 without access counts the rules as the card holds them (the second clause only when someone named cannot see the space) and writes nothing. Saving reconciles the rows at once and reports it: Audience saved: 14 people added, 3 people removed from the audience (re-opened rows count as added) or Audience saved. Nobody was added or removed. The save also tells the people it added or re-opened: one request notice each, worded with their own deadline, through the shared outbox (see How notices are sent); nobody else hears, and people the directory hooks or the hourly sweep add are first told by the reminder sweep.

A person who leaves a policy’s audience, by leaving a group, losing access to the space, being excluded by a rule or being disabled, drops out of every count at once. Their row is kept, closed with the reason (left the group, access removed, audience narrowed, disabled), and their past confirmations stay on the record: the All confirmations view and the ledger export still show them. If they come back, the same row re-opens with a fresh deadline (the new-joiner days from the day they return) and cleared reminder and escalation stamps, and they are asked again; a confirmation made before they left does not count for the new cycle. Nothing about an audience ever deletes a confirmation.

Only a person the policy asks sees the confirmation card and the sticky control. Someone outside the audience, or recorded as exempt, reads the document like any other; a confirmation posted for them anyway is refused with “This document doesn’t ask you for a read confirmation.”

Someone who reads a policy only through a published handbook confirms it in the handbook (see Confirming from a handbook).

The audience is on the API with the rest of the policy’s settings (audience_mode, rules, exclude_stewards, assign_new_members and the preview on GET / PATCH /api/documents/{id}/ack-settings; see What is stored). A saved audience is written to the audit log as ack.audience_set with the mode, the rules, the two switches and the counts.

A new account, whether created by an admin, provisioned by SCIM or made at a first SSO sign-in, is enrolled in every policy whose audience admits it and has Enrol new joiners automatically on (the default), with a due date the new-joiner days from the day they join (14 built in; a space or the workspace can set another number, see Deadlines and defaults). A joiner with more than one policy gets one message, Welcome: 3 policies to read, listing the policies with their due dates and linking My policies, not one request per policy; a single policy arrives as the usual request. The message is the editable template Welcome: policies to read (see email templates); the enrolment is audited as ack.auto_enrolled. From 1.14.0 a policy set that auto-assigns admits every new account to its policies the same day, whether or not each policy’s own Enrol new joiners automatically switch is on: the set is the explicit instruction, and the welcome notice lists them with the rest.

Audiences follow the directory. Adding someone to a group, a sync that replaces a group’s members, granting or removing a group’s membership of a private space, a change of a space’s visibility and disabling or re-enabling an account all bring the policies concerned in line at once. Changes made in Roles & permissions take effect within the hour: the hourly compliance sweep checks every live policy against its audience first, and each policy it moved is audited as ack.audience_reconciled with the counts. A policy with Enrol new joiners automatically off never gains people through these automatic paths; it still loses people who leave, and a manager’s save of its audience enrols everyone the rules name. Everyone these paths add gets the new-joiner days from the day they are added. The automatic paths tell nobody (the reminder sweep is their first word); a manager’s save sends the people it added a request notice at once.

Leaving a group closes the requirement but never removes a past confirmation. The scheduled directory sync’s report gains the line Policy audiences updated: +12 / −3 when a run moved anyone.

Thirty policies run one at a time is thirty requests, and a new joiner needs eight of them on day one. From 1.14.0 a policy set groups policies into one unit: a set has a name, a description, its policies in order (the order they are read in, and the order My policies lists them), and a switch, Auto-assign to new joiners, on by default. Typical sets: New starter onboarding (the eight policies every new account reads in week one), Annual pack (the twelve re-confirmed each year), Engineering (the three only that group signs). Compliance › Sets lists them (compliance.program_read to look, compliance.program_manage to act).

Creating a set. New set opens a form above the list: the name (Set name, with New starter onboarding as the example in the field), an optional description, the policies in order and the Auto-assign to new joiners switch. The search box offers every published document that is not retired, whether or not it requires confirmation yet; each policy you add joins the list under it, where move up and move down set the order and the row names its space and says “not yet required” while it is not asking anyone. A name must be unique among active sets (the server answers A set with that name exists.).

What a set shows. Each set’s card carries its members as numbered chips (“1. Acceptable Use”), its standing over the members’ open rows (“87% · 6 overdue”, or “Nobody asked yet” while no member asks anyone), an Auto-assign chip while new joiners get it and an Inactive chip once it is deactivated.

Assigning a set to people, to groups, or to everyone does to every policy in it what saving the policy’s audience would. Assign names people and groups (“Add people…”, “Add groups…”) and sends with Assign set, or Assign to everyone after the question Assign “New starter onboarding” (8 policies) to every active member?. Before anything is sent the dialog says what will change:

  • To people and groups: each policy gains the named people and groups in its audience (targeted mode), the people it starts asking get one request notice per policy through the shared outbox, and their rows read source set on their record and in the exports.
  • To everyone: each policy asks everyone who can see its space (space mode; any rules it had are cleared, as saving space mode does).
  • A policy in the set that did not yet require confirmation is switched on with the workspace defaults; the dialog lists them (These 2 documents will start requiring confirmation: …).
  • A policy that already asks everyone who can see its space is left as it is: a set never narrows an audience (the dialog warns: These 3 policies currently apply to everyone who can see their space; assigning the set will not narrow them). Narrow it on the policy’s Settings tab first if that is what you mean.
  • The rule that a policy reaches only people who can see its space still holds: a set cannot give anyone access.

The toast carries what happened: Set assigned: 8 policies, 412 people · 2 switched on. The Policies page shows a set chip on each member policy and filters by set; My policies groups a person’s rows under the set’s label; the matrix and roster CSVs and the compliance snapshot carry a set column per policy.

New joiners. While the switch is on and the set is active, every new account (created locally, by SCIM or at first SSO sign-in) is added to each of its policies on day one with the new-joiner deadline, whether or not the policy’s own Enrol new joiners automatically switch is on: the set is the explicit instruction. The welcome notice lists them with the rest.

Editing, deactivating, deleting. Edit changes the name, the description, the order and the switch; a policy taken out of a set keeps its people as plain rules its Settings tab can then edit (the set’s tag comes off its rules in the same save; put back in the set, it is re-linked only when you assign the set again). Deactivate is the way to retire a set: auto-assign and assignment stop, every rule and every confirmation stays, the card shows Inactive, the toast offers Undo, and its name is free while it is deactivated; Activate brings it back. Delete removes the set only, after Policies and confirmations stay; only the set is removed. This cannot be undone.; a set that auto-assigns asks for its name to be typed first. Every policy keeps its audience, its rows and its confirmations; the rules the set wrote stay in place as a manager’s would.

Every change is written to the audit log: ack.set_created (“Created policy set”), ack.set_updated, ack.set_deactivated, ack.set_deleted and ack.set_assigned (“Assigned policy set”, with the set, the people, the policies and the members switched on).

API. GET /api/admin/compliance/sets answers { rows, candidates }: every set (active first) with its ordered policies and progress, and the published documents a set may hold. POST /api/admin/compliance/sets takes { name, description?, doc_ids, assign_new_members? } and answers 201 { set }, or 409 A set with that name exists. PATCH /api/admin/compliance/sets/{id} takes any of { name, description, doc_ids, assign_new_members, active } and answers { set }; DELETE answers { ok }. POST /api/admin/compliance/sets/{id} takes { user_ids?, group_ids?, everyone? } (up to 200 ids each) and answers { ok, policies, people, added, switched_on, not_narrowed }, or 409 This set is deactivated. Reactivate it to assign it. Writes need compliance.program_manage, the list compliance.program_read; 400 names the first problem.

The confirmation sits after the text, so the reading comes first.

  • A one-line pointer at the top. While a confirmation is pending, the notice strip above the document says “Please read this document carefully. You’ll be asked to confirm at the end.” with a Jump to confirmation button. It is a pointer, not the control: nothing is recorded from the top of the page.
  • The card at the end. After the last paragraph, a card titled Confirm you’ve read this document shows the confirmation statement as its only sentence (the policy’s own, or the workspace default; see What the reader confirms), a meta line naming what is being confirmed (“Version 12 · published 3 Mar 2026 by Dana Whitfield”, with “· approved by Priya Nair” when the version came through the review queue), and one button: I’ve read and understood this. Clicking it records the confirmation and shows the toast “Confirmation recorded.” On a policy that requires a typed signature, a Sign to confirm block sits between the statement and the button; the button waits until the name is typed.
  • Nothing is locked. The button is live from the moment the card appears; nothing waits for a scroll position or a timer. The card’s placement after the text is the only gate.
  • The sticky bar knows where you stand. Once the title scrolls away, the sticky document bar carries a Confirmation pending button that jumps to the card. It reads Confirm now once the end of the document has been on screen, and becomes a Confirmed 3 Mar 2026 chip after you confirm. On a phone the bar shows this control and the title only.
  • A deadline is information, never a wall (1.13.0). When the policy you are asked to confirm has a due date, the card at the end shows a chip Due 29 Oct 2026 (amber once three days or fewer remain) or Overdue since 29 Oct 2026. An overdue policy is confirmed the same way, with the same button; the card only adds “This was due 29 Oct 2026. Confirming now still records today’s date.” The notice at the top keeps its one sentence and its one action and gains the date: “Please read this document carefully. You’ll be asked to confirm at the end by 29 Oct 2026.” The sticky bar shows Due in 2 days, Due tomorrow, Due today or Overdue beside the jump link (on a phone only Overdue is shown). All of this leaves once you confirm. The date is yours: it comes from your own assignment, so two people on the same policy can see different dates (a new joiner’s deadline counts from the day they joined); see Deadlines and defaults.
  • The confirmed state. The card changes to Confirmation recorded with “You confirmed version 12 on 3 Mar 2026, 14:02.”; the notice strip says the same in one line. On a signed confirmation the sentence ends “, signed as Dana Whitfield.” From 1.14.0 the card then links to View your record, the page with everything you have confirmed and been asked to confirm (see A person’s record); your standing across every policy is on My policies, which links to the same page as Your record. If the document is edited later and the editor keeps confirmations (an editorial fix), your confirmation stays current and the card offers Re-confirm the current text (v13) for anyone who wants their record to cite the newest words.
  • Asked again. A re-confirmation never looks like a first request. When your confirmation has lapsed, the notice strip says “This document changed since you confirmed it on 3 Mar 2026. Please read the changes and confirm again.” with the same Jump to confirmation button, and the sticky bar’s control reads Re-confirm needed. The card at the end says why, in one line above the statement: after a text change, “Updated 3 Mar 2026 by Dana Whitfield: “Section 4 now covers contractors”“ (the change note the editor wrote, whichever way the change arrived); after a manager’s Ask everyone to re-confirm…, “Dana Whitfield asked everyone to confirm this document again on 3 Mar 2026: “Section 4 now applies to contractors too.”“; and, when the policy’s re-confirmation cycle came due, “Annual re-confirmation. It has been 12 months since you confirmed this document. Please read it again and confirm.” When the text changed, a disclosure What changed since you last confirmed (2 versions) lists the versions since the one you confirmed with their notes, who wrote them and when, then shows the difference against the text you confirmed, with the lines added and removed and an Open in version history link that opens the same comparison (your version against the current one) on the Version history page. A manual or annual re-ask that changed no text shows the reason and no comparison, and the strip says “Everyone was asked to confirm this document again on 3 Mar 2026. Please read it and confirm again.” rather than claiming the document changed. A confirmation recorded before 1.10.0 is compared against the newest version that existed when you confirmed it, and the disclosure says “version inferred”.
  • A stale read is caught. The page knows which version it rendered. If the text changes while you are reading, your click records nothing: the toast says “This document changed while you were reading it. The page has been refreshed; please read the changes.” and the page reloads with the current text.
  • Printed copies carry your standing. A printed document ends with one line in place of the card: “Read confirmation recorded for Dana Whitfield on 3 Mar 2026, 14:02 · version 12” or “Read confirmation required. Not yet confirmed by Dana Whitfield”.
  • Every notice lands on the card. A request, a reminder or a re-confirmation notice, in the bell inbox or by email, opens the document at the confirmation card, so the person arrives where the action is.

Recording a confirmation needs the document.ack_submit permission, which the Viewer preset holds. A custom role without it sees “Read confirmation is required for this document, but your role can’t record one. Ask your space approver.” in the notice strip and no card.

Two things are on every confirmation from 1.12.0: the words the person agreed to, and, when the policy asks for it, their signature.

The card at the end of a policy shows one sentence, the confirmation statement. By default it is the workspace’s, set under Program settings (“I confirm that I have read and understood this document.” until an administrator changes it). A policy can carry its own: on its Read confirmation page, the Settings tab (visible to anyone holding document.ack_require in the space, and to program managers) has a Confirmation statement field, up to 500 characters, shown above the confirm button and stored with every confirmation. Legal teams often add a clause such as “and that I will comply with it”. Leave the field blank to use the workspace default, which the field shows as its placeholder. The statement is rendered as Markdown, so a clause may carry emphasis or a link, but it stays one sentence beside one button: pressing the button is the confirmation itself, and nothing opens a dialog to ask the same question twice.

Every confirmation stores the statement as the server showed it at that moment (statement_text on the record). Changing the workspace default or a policy’s statement later changes what new confirmations record, never what earlier ones say, so an auditor reads the words each person agreed to.

For a policy where a click is not strong enough evidence, switch on Require typed signature on the same Settings tab. The card then shows a Sign to confirm block: the reader types their full name exactly as it appears on their account (case and spacing do not matter; the help line names it), and an account with a local password re-enters it. The confirm button waits, with “Sign above to confirm” under it, until the name matches; the password is checked on the server, and a wrong one is refused with “Password check failed. Re-enter your account password to sign.” The password check is throttled like a sign-in: five wrong passwords lock that account and address for fifteen minutes, for signing and for signing in with the username (only that username-and-address pair is counted, so colleagues signing from the same office address are not affected; signing in with the email, possible since 1.16.0, is counted separately, as on the sign-in form). Each wrong password is written to the audit log as auth.login_failed against the policy, and the lock as auth.lockout, and the password is never stored: the server compares it with the account’s hash and discards it, and it reaches no row, no audit entry and no log line.

SSO accounts have no password to re-enter: they sign by typed name alone, as training e-signatures do, and are identified by the sign-in session that reached the server. The record says which it was.

A policy whose text changed since you confirmed it asks for the signature again with the re-confirmation; so does Re-confirm the current text on a kept edit.

Beside the version, its hash and the reading evidence, each row of the record keeps:

  • statement_text: the confirmation statement as served when the person confirmed (empty on confirmations from before 1.12.0).
  • signed_name: the name as typed, on a signed confirmation; empty otherwise.
  • identity_proof: how the signer was identified. password for a local account that re-entered its password; sso_session for an SSO account, identified by its sign-in session; typed_name_only for a local account that somehow carries no password hash. Empty on an unsigned confirmation.
  • user_agent: the first 300 characters of the browser’s User-Agent header (filled on the record and in the CSV only for holders of compliance.export, as the IP address is).

The Record tab shows the typed name in a Signed column; the per-policy CSV appends statement, signed_name and identity_proof (see CSV columns). The card’s done state and the printed standing line read “signed as Dana Whitfield”.

The settings themselves are on the API: GET /api/documents/{id}/ack-settings answers { statement, statement_default, signature, recert_months, next_recert_at, effective_at, marker, recert_default, superseded_by, superseded_by_title, audience_mode, rules, exclude_stewards, assign_new_members, due_days, due_at, remind_before_days, remind_every_days, escalate_after_days, nudge_every_days, owner_user_id, owner_name, requested_by, defaults, inherited } (the keys from audience_mode on are 1.13.0’s, below; statement is null while the policy uses the default), and PATCH takes any subset of { statement, signature }: the statement is trimmed and refused over 500 characters, null or blank returns the policy to the default, signature is a boolean; an unknown key is refused (400) rather than ignored. From the same release the body may also carry recert_months (a whole number of months from 1 to 60, or null for never), next_recert_at (a date, YYYY-MM-DD or a timestamp, not in the past; null schedules the next cycle from the last ask), effective_at (a date or null) and superseded_by (a document id or null), any subset in one call; the first problem is answered with 400 and nothing is written (the replacement is judged before any field is saved, and next_recert_at on a policy with no interval, and none in the same call, is 400 “Set an interval before a next re-confirmation date.”). marker is when everyone was last asked (the first cycle is dated from it) and recert_default the workspace’s interval. The route needs document.ack_require in the document’s own space or compliance.program_manage (the shipped Compliance manager preset qualifies); a document outside your scope, or a draft you may not read, is 404, and superseded_by_title is null when the replacement is outside your scope (the id is always answered). Each PATCH that changed something is written to the audit log as one ack.settings_changed row naming the fields it moved (for the statement, whether it changed, and the signature setting after the change). What the interval and dates do is under Annual re-confirmation and effective dates, the replacement under Retiring a policy.

From 1.13.0 the body may also carry audience_mode (space or targeted), rules (a list of { kind: "group" | "user" | "role", ref, effect?: "include" | "exclude" }; the list is saved whole, junk and duplicates dropped, 200 rules at most), exclude_stewards and assign_new_members (booleans; a targeted audience with no include rule is 400 “Name at least one group, person or role.”), due_days (1 to 365, or null to inherit), due_at (YYYY-MM-DD, today or later, or null), move_existing (true recomputes every open deadline from the rule as it now stands; a new or changed fixed date does so by itself), the four reminder numbers (each in its range, or null to inherit) and owner_user_id (a user id, or null; “Owner not found.” otherwise), any subset in one call. The answer adds moved (deadlines moved by this call) and, when the audience was saved, added, closed and reopened. defaults is the cascade resolved with the policy’s own layer (values, origin per key, space); inherited the same cascade without it, which is what a blank field falls back to. GET ?preview=1&audience_mode=targeted&exclude_stewards=1&rules=<URL-encoded JSON> answers { count, no_access, sample } (the people the rules would hold open, the people named who cannot see the space, the first five names) for the rules as sent and writes nothing. The audit rows are ack.audience_set (mode, rules, the two switches, added, closed, reopened), ack.due_set (the rule, from_rule, moved_existing, moved) beside the move’s own ack.due_moved, and ack.defaults_set with scope: "policy" per reminder number or owner that changed; the 1.12.0 fields keep ack.settings_changed. What the audience does is under Who must confirm, the deadline under Deadlines and defaults and the reminders under Reminders and escalation.

From 1.18.0 policies can be brought together in policy handbooks: named, ordered bundles of published documents from any spaces that people read as one, print, and confirm one policy at a time or all at once. A handbook changes three things about read confirmation: who a policy may ask, where a confirmation can be made, and what is kept when a whole handbook is confirmed.

Anyone who can read a published handbook reads every published document in it, even in a space they cannot open (see Who reads a handbook). So putting a policy in a published handbook adds the handbook’s audience to the people the policy may ask: the visibility ceiling becomes everyone who can see the policy’s space plus everyone who reads it through a published handbook.

  • A policy set to Everyone who can see this space also asks everyone who reads it through the handbook.
  • A targeted audience may now name them: a group, person or role rule enrols someone who reads the policy only through a handbook, and the card’s preview no longer counts them as “without access”.
  • Publishing, unpublishing or deleting the handbook, changing its audience or its contents, a change to the membership of one of its audience groups, and a change to who can see a space all bring the policies’ audiences up to date at once, without anyone saving the policy again.
  • Unpublishing a handbook and publishing it again does not cancel confirmations that are still current. Someone who confirmed the current version before the handbook was unpublished still counts when it comes back.

Each policy in the handbook reader shows where you stand: To confirm, Confirmed or Updated since you confirmed, and a policy that does not ask you says For information. The contents tick what you have confirmed and Your progress counts the policies that ask you.

Under each policy’s text is a confirmation bar: the policy’s statement as a checkbox, the version it confirms, and Confirm (Re-confirm after a kept edit). It writes exactly the record the card at the end of the policy’s own page writes, against the same version, and counts in the same place. On a policy you confirmed in an earlier version, a What changed in version N box above the text lists the version notes since, with Compare versions when you can open the document’s space.

Someone who reads a policy only through a handbook confirms it there: the policy’s own page stays closed to them. Recording a confirmation needs document.ack_submit on the policy’s space, or, for someone who reads the policy only through a handbook, held anywhere (the Viewer preset holds it).

Each confirmation made in a handbook is audited as document.acknowledge, with via: "handbook" and the handbook’s id.

A handbook’s Require a typed signature switch covers confirmations made in the handbook: the bar under each policy in the reader and the whole-handbook page. It asks for the typed name (and the password, for a local account) exactly as a policy’s own typed signature does, with the same checks and the same throttle.

Confirming from the policy’s own page in its space follows the policy’s own Require typed signature setting, not the handbook’s. Someone who reads a policy only through handbooks confirms it in a handbook, and must sign if any published handbook they can read that holds it has the switch on.

A handbook with Ask readers to confirm the whole handbook on offers Confirm handbook, which opens /handbooks/{id}/confirm:

  • Every policy is listed with where you stand on it, with a count line such as “3 confirmed · 4 to confirm · 1 updated · 2 for information”.
  • One statement to tick: “I have read and understood all 8 policies in Employee Handbook 2026, and I agree to follow them.” A waiting policy whose own statement differs is listed under it (“These policies ask you to confirm in their own words:”); the one tick covers those too, and each of those records keeps the policy’s own words. Every other record stores the handbook’s sentence.
  • Your name and password are asked once for the whole batch when any waiting policy requires a typed signature, or the handbook does; the line under the fields says which.
  • Confirm 5 policies confirms every policy still waiting for you, each against its current version and exactly as its own card would: the same record, the same reading evidence, one document.acknowledge entry each, marked via: "handbook". Like a single confirmation, it sends no notice.

Every check runs before the first record is written: the license, the ticked statement, the versions the page showed and the signature. If a policy changed while you were reading, nothing is written: “A policy in this handbook changed while you were reading. The page has been refreshed; please read the changes.” A wrong password is refused and logged as on a single policy, against the handbook.

From 1.18.2 the page also sends the statement and the contents it showed, and the server compares both with its own before writing anything, so the statement stored is always the one the person ticked. Contents changed since the page was drawn (a document added for information, a type changed) are refused as a changed policy is, and the page refreshes. For handbooks that hold SOPs or other documents, the statement names them (see The statement for SOPs and mixed handbooks).

Once nothing in the handbook is left waiting for you, a whole-handbook record is kept for reporting: the handbook (and its title at the time), the person, when, how many policies the batch confirmed, the typed name on a signed batch, the IP address and browser, and a fingerprint (a SHA-256 of the ordered policies and the versions confirmed). It is audited as handbook.confirmed. The per-policy records are the evidence of each policy, as always; the whole-handbook record says the person confirmed the handbook as it stood.

The record is kept when the handbook is deleted and when the person leaves. Anonymising a person’s compliance record also anonymises their whole-handbook records. The Handbooks page shows the share of the audience holding a record of the current contents to the handbook’s editors and to holders of Read compliance program (compliance.program_read); see Progress on the Handbooks page.

People asked only through a handbook (1.18.3)

Section titled “People asked only through a handbook (1.18.3)”

Someone in a handbook’s audience who can’t open a policy’s space is still asked to confirm that policy, and from 1.18.3 they are reached, reminded and counted like everyone else:

  • My policies, the dashboard and the sidebar badge list the policy. The row opens it inside the handbook, at its confirm bar (/handbooks/{id}/{doc}#confirm-reading), and names the handbook instead of the space (“In Employee Handbook”). Unpublishing the handbook takes the row away.
  • Requests, reminders, the welcome and bundled mails and the bell link into the handbook and add a line naming it: “It’s in Employee Handbook.” When a policy is in several handbooks the person reads, the oldest published one is used. A link never points at a draft handbook or one the person can’t read.
  • Remind, the hourly sweep, escalations, Team compliance, the team view and the compliance portal count them, and a group lead can remind them.
  • The Record tab, the portal and the team view mark them Through a handbook. The tag names the handbook only to people in its audience or who manage handbooks; anyone else sees “a handbook you can’t open”.
  • The CSVs carry the handbook in a new last column, asked_through (see CSV columns).

Nothing changes about who can read a document: a handbook still opens its published policies only to its own audience.

Compliance in the main navigation opens on My policies: every policy you are asked to read and confirm, and where you stand on each. The row appears for anyone with a policy in a space they can see, whether or not they run the programme, and never without the licence; it carries a badge with the number of policies you have not confirmed yet, like Training’s open assignments. From 1.14.0 the header links to Your record (see A person’s record), and a group lead the team view opens for sees a banner at the top, You lead a team. See your team’s policies (see Team leads).

The page opens with a standing line, 3 of 5 policies confirmed, and a progress bar, then two groups:

  • To confirm: the policies you owe a confirmation. From 1.13.0 the group is ordered overdue first, then by the soonest deadline, then by the most recently changed policy (the order of the “last saved” date each row shows). Each row shows the policy’s space and title, when it was last saved, and one chip: To confirm · due 29 Oct 2026 (you have never confirmed the current ask; the date is your own, see Deadlines and defaults), Overdue · due 29 Oct 2026 (that date and the policy’s grace have passed) or Changed since you confirmed 3 Mar 2026 (an edit, a re-ask or the annual cycle lapsed an earlier confirmation). The title links straight to the confirmation card at the end of the document. From 1.14.0 rows that belong to a set sit under the set’s label, New starter onboarding · 3 of 8 done (your rows in that set over every state), in the set’s order; a policy in two sets appears under the first; policies in no set follow under no label.
  • Confirmed: the policies you are up to date on, each with Confirmed 3 Mar 2026 · v12: the date and the version you confirmed. Once nothing is left to confirm the group ends with You’re up to date. 5 policies confirmed.

Over ten rows a filter box narrows the list. Paused (unpublished) and retired policies are not listed and not counted. With nothing to confirm the page reads Nothing to confirm and “Policies you’re asked to read and confirm will appear here with their due dates.” The page holds no history, print or CSV: your full record, with its history, the notices and one personal CSV, is Your record (/compliance/person/me, 1.14.0; see A person’s record). Nothing nags: no dialog on sign-in, no interstitial; the dashboard row, the badge and the notices are the reminders.

API. GET /api/compliance/mine answers the same rows for the signed-in person: { rows: [{ id, title, space_icon, space_name, state, acknowledged_at, prior_acknowledged_at, rev, updated_at, due_at }], confirmed, total }, with state one of pending, reconfirm, overdue (from 1.13.0) or confirmed, due_at the person’s own deadline or null (1.13.0), acknowledged_at and rev the current confirmation (null unless confirmed), and prior_acknowledged_at the newest confirmation from before the current ask (the one that lapsed, or the previous cycle’s); rows arrive in the page’s order. From 1.14.0 each row also carries set_names and set_position (the sets the policy belongs to and its place in the first), and lead_team: boolean sits beside confirmed and total (whether the team view would open for the caller). Any role; 402 without the licence.

A confirmation covers the version of the text that was on screen when the person confirmed, and the record keeps that version number and a hash of its text. Each policy also carries a confirmation marker: the moment everyone was last asked to confirm, and the version they were asked to confirm. A confirmation is current while it is newer than the marker. Moving the marker is what “asking everyone to re-confirm” means. From 1.10.0 only a change to the document’s text or title can move it, and from 1.10.1 a manager can move it deliberately, with a reason, without editing anything (see Asking everyone to re-confirm). The document’s “last updated” time has nothing to do with it. Because every confirmation names its version, a reader who is asked again sees exactly what changed since the version they confirmed, not since the last save.

What asks everyone again, and what does not

Section titled “What asks everyone again, and what does not”
Asks everyone to re-confirm Does not
A text or title edit saved as a material change (or any text save under Always ask everyone to re-confirm, where the change note is required, or one that looks material or borderline under Ask only when a change looks material; see the edit default) A text or title edit saved as an editorial fix (confirmations kept, the reason on the History tab; under Ask only when a change looks material a change that looks editorial with confidence is kept and recorded from source Default)
Ask everyone to re-confirm… clicked by a manager, with the manager’s reason, without any edit (refused inside an hour of the last ask or reminder) Summary, tags, category, type or author, even after an editorial fix: only a save that changes the text or title asks
Restoring an earlier version, when the restored text differs from the confirmed version and the restorer chooses to re-ask (the restore asks the question; see below) Moving the document to another space, or nesting it under another page
Merging a branch, when the merged text differs from the confirmed version and the merger chooses to re-ask (the merge asks the question; see below) Scheduling a publish or an unpublish, or clearing a schedule
Approving a change request that changes the text, when the approver (or the submitter’s recommendation, or the edit default) says so Unpublishing, and republishing the text people confirmed, by hand, in bulk or on schedule (the policy is paused in between)
Publishing a draft whose text differs from the confirmed version, when the editor, the edit default or the API caller says so (a bulk publish skips it with “requires a re-confirmation decision; publish it individually”; a scheduled publish follows the edit default) Moving to the Trash and restoring from it
The policy’s re-confirmation cycle coming due (every 6, 12 or 24 months from the last ask; see Annual re-confirmation and effective dates) An image placed by the Claude connector or dropped through an upload link: confirmations are kept and a Kept row from source Image or Ticket is recorded, counted by the portal’s drift chip (the alt text is kept to one line with no markdown in it, so the change is exactly one image line)
Turning the requirement off, editing the text, turning it on (the marker advances on every text change whether or not confirmation is required, so nothing unread is ever shown as confirmed) A bulk tag, type, space or status change (bulk never writes text)
Saving with nothing changed (no version, no “edited” audit entry, no change to “last updated”)
Ask to re-confirm… on one person’s roster row (1.13.0): only that person’s clock restarts; the marker and everyone else’s confirmation stay where they are (see Asking one person again)

When everyone is asked again, everyone becomes Pending, the record shows the new ask date and version, everyone whose confirmation lapsed is told, and on the document they see the change note and a comparison against the version they confirmed (see Reading and confirming). Older confirmations are never deleted: they stay in the record, tied to the version they covered, and the newest row is the one that counts.

Where the editor is not there to be asked, each route that changes the text of a confirmed, published policy takes the decision in its own way:

Route Who decides, and how
The app’s own PUT /api/documents/{id} Takes ack_policy (keep or reset; anything else is refused with 400, before the review queue as well as before a direct save) with an ack_reason (a reset needs a reason of at least 10 characters; a keep follows Program settings’ “Require a reason for editorial fixes too”; longer notes are cut at 500). Without publish rights the two travel with the change request as the submitter’s recommendation.
Approving a change request that changes the text The approver decides, with the submitter’s recommendation in front of them. A request made from the editor, a merge, a restore, the v1 API or MCP carries the submitter’s choice (editorial fix or material change) and reason; approving applies the approver’s own choice when they make one, otherwise the recommendation (its reason is checked against the same rule at approval, and one that fails it counts as none), and a request that carries none is not approved until the approver decides (the API answers 428; see Change request decisions (API)). The History row (source Approval) names both: who recommended what, who approved, and the reason (the approver’s, or the submitter’s).
A branch merge or a restore Ask the same question as a save. Merging a branch into, or restoring an older version over, a confirmed document whose text differs from the confirmed version is refused (428) until the call says ack_policy (keep or reset) with an ack_reason (a reset needs a reason of at least 10 characters; a keep follows Program settings’ “Require a reason for editorial fixes too”). Restoring the confirmed version itself, or merging a branch whose text matches it, asks nothing (a bad ack_policy value is still refused). In the app the note typed in the question is the version note and the recorded reason. The History row reads source Merge or Restore.
The v1 API or the Claude connector The caller decides: ack_policy keep or reset with an ack_reason (a reset needs a reason of at least 10 characters; a keep follows Program settings’ “Require a reason for editorial fixes too”), else the call is refused (428 / decision_required) and nothing is written. Metadata-only changes and drafts that stay drafts are never refused for want of a decision (ack_policy is validated whenever it is sent, so a wrong value is 400 even there); publishing a drifted draft asks like an edit. A token can never confirm a policy, only edit one.
An import Left unchanged (a published policy, or a paused one the zip would publish) unless Also overwrite documents that require read confirmation is ticked; then the edit default decides (source Import, the importer named, each overwrite echoed to the audit log with the importer’s address, and the run written as document.import) and the summary says how many policies will ask people to re-confirm.
A scheduled publish of a draft whose text drifted from the confirmed version Follows the edit default; nobody can be asked when the schedule fires. Under the recommended default everyone is asked again; the History row reads source Schedule with the reason “Published on schedule”. The editor warns under the schedule field beforehand: “Publishing on schedule will ask 42 people to re-confirm: the text changed since version 12.”

Under the recommended edit default, saving a text or title change to a published document that people have confirmed opens a question in the editor, whatever the route (Save changes, Ctrl+S, Publish on a draft whose text drifted, or Submit for review):

How should readers see this change? 42 of 50 people have confirmed version 12 of “Expense policy”.

Two choices, each a card:

  • Editorial fix. Keep confirmations. Typos, formatting, broken links, layout. No change in meaning or obligations. Nobody is asked again; the version is still recorded in history.
  • Material change. Ask everyone to re-confirm. The meaning, scope or an obligation changed. The 42 confirmations are reset, everyone is asked to read the new text, and everyone whose confirmation lapsed is told (see What happens after a reset).

Below them, one sentence says which choice CompassDocs guessed and why (see the guess), with a Show changes disclosure that renders the difference against the version readers last confirmed. The Change note field is required, 10 characters or more:

  • Under Material change it is always required: “Tell readers what changed. This note is kept in version history and on the compliance record.” It is also what the re-confirmation notice shows people.
  • Under Editorial fix it is required while the program setting Require a reason for editorial fixes too is on (the default): “Say why readers need not re-confirm. This is recorded on the compliance record.” The keep is the decision an auditor questions, so by default it is never silent.

The primary button names the outcome: Save. Keep confirmations or Save and ask 42 people to re-confirm. Cancel (or Escape) leaves the editor with your changes unsaved and the Save button idle; the keyboard works the way a form does (arrow keys move the choice, Enter submits, Escape cancels). After saving, the editor confirms what happened: “Saved as an editorial fix. Confirmations kept.” or “Saved. 42 people will be asked to re-confirm.”

Under the other two edit defaults the editor behaves as the table above describes: Ask only when a change looks material opens the question only for a change that looks material or borderline and otherwise saves with “Saved as an editorial fix (document setting). Confirmations kept.”; Always ask everyone to re-confirm never opens it, requires the change note, and saves with “Saved. 42 people will be asked to re-confirm.” The help under the Change note names the document’s setting so the editor knows before saving.

The question measures the drift since the version readers last confirmed, not only this session’s edit, so after an editorial fix the next text save still asks (and the hint says “since v12”). Only a save that changes the text or the title asks: a save that touches tags, the summary, the category or the type after an editorial fix neither asks nor resets anyone.

While editing a confirmed document, a chip beside the status badge reads Read confirmation · 42 of 50. A metadata-only save never opens the question.

If your changes go to the review queue instead of publishing directly, the save becomes a change request as before. From 1.11.0 the question is asked at submission as a recommendation: your choice (editorial fix or material change) and change note travel with the request, the approver sees them beside the proposed change, and approving applies your recommendation unless the approver decides otherwise (see the review queue and the table above). The History row records both: version history reads “Editorial (kept) recommended by Alex, approved by Priya”, or “recommended material change by Alex, approved by Priya” when the approver overrode the recommendation. The same question, with the same cards and note, is asked by the review queue’s Approve, by Restore in version history and by Merge in the branch banner when the incoming text differs from the version people confirmed; only the title and the button’s verb change (“Restore. Keep confirmations”, “Merge and ask 42 people to re-confirm”), and in each the note is the version note and the recorded reason. See Versions & branches and the approval workflow.

After an editorial fix, a reader whose confirmation still counts but cites an older version than the text on screen sees Re-confirm the current text (v13) on the end-of-document card. Clicking it records a second row for the newer version; the record keeps both.

Three change notes that help an auditor, against one that does not:

  • “Expense cap raised to £75; receipts required over £20.”
  • “Section 4 now covers contractors as well as employees.”
  • “Fixed the broken link to the travel form; no change in meaning.”
  • “Edited.”

How CompassDocs guesses editorial vs material

Section titled “How CompassDocs guesses editorial vs material”

When the question opens, CompassDocs compares the new text with the version readers last confirmed and shows one sentence beside the choices, such as Looks editorial: 14 words in one paragraph changed (1.1% since v12). or Looks material: a number changed in “Section 4”. The guess is a measure of the change, not a judgement about meaning:

  • Material when the title, a heading or a number (including amounts and percentages) changed, when 5% or more of the words changed, or when three or more paragraphs changed.
  • Editorial with confidence only when under 2% of the words changed, in at most two paragraphs, with no heading, number, list item or title change.
  • Anything in between is borderline, shown as “Borderline: 3.4% changed across 2 paragraphs. Choose Material change if an obligation changed.”

A change too large to measure is treated as material. Line endings and whitespace-only differences count as no change. The comparison runs both against the text you loaded and against the confirmed version, and the larger of the two is what you see, so five small “editorial” saves in a row cannot hide a large drift from v12.

Under the recommended default the guess only pre-selects the answer: the editor still chooses, and the record keeps the choice, the guess, the percentage and whether they differed. Under Ask only when a change looks material the same measure decides whether the question opens at all, and only an editorial with confidence result passes without it.

Everyone who can see the document becomes Pending again. On their next visit the strip says the document changed since they confirmed it, with the day, the sticky bar reads Re-confirm needed, and the card at the end shows the change note and a comparison against the version they confirmed (see Reading and confirming); the dashboard reminder counts the document again, and the Record tab shows the new ask date and version.

From 1.10.1 a reset also tells people. There are three moments a person hears about a policy, and one cycle:

  • Requested. A manager clicks Request read confirmation in the compliance portal, or switches the requirement on from the document and chooses Require and notify. Everyone who can see the document gets a dashboard notice and an email: Action required: confirm “Expense policy” with “Please read and confirm “Expense policy”.“
  • Updated. The text changed and the editor asked everyone to re-confirm (or a merge, a restore, an approval, a scheduled publish or the API did). Everyone whose confirmation lapsed hears Changed. Please re-confirm “Expense policy” with who changed it and the change note: “Dana changed “Expense policy”: Section 4 now covers contractors. Please read the changes and confirm again.“ When a manager asks everyone to re-confirm by hand, the notice is titled Please re-confirm “Expense policy” and its body is the manager’s reason.
  • Reminded. Remind in the portal tells only the people who have not confirmed yet: Reminder: confirm “Expense policy” with “You still need to read and confirm “Expense policy”.“ From 1.13.0 the hourly sweep reminds the same people by itself, on a cadence and in words set by their due date (Due in 3 days: confirm “Expense policy”, Overdue: …); see Reminders and escalation.
  • Annual re-confirmation. The policy’s cycle came due. Everyone whose confirmation lapsed hears Please re-confirm “Expense policy” with “Annual re-confirmation: it has been 12 months since you confirmed “Expense policy”. Please read it again and confirm.“ The email carries the label Annual re-confirmation. Never coalesced, sent at once.

Every notice links straight to the confirmation card at the end of the document. See How notices are sent for the timing and the record, and note that space subscribers still get their separate “document updated” email.

Editing is not the only way to ask again. Once an author can mark a change editorial there must be a way to reverse that call (“that wording change did matter”), to re-ask after something that happened outside the document (a training session, an incident, a regulator’s letter), and to restart the annual cycle early, without faking an edit. That is Ask everyone to re-confirm…, offered in three places to anyone holding document.ack_require in the space or compliance.program_manage:

Clicking it opens one dialog, titled with the document’s name on all three surfaces:

Ask everyone to re-confirm “Expense policy”? 42 of 50 people have confirmed version 12. This resets those confirmations, notifies everyone who can see the document, and shows them your reason.

Why are readers being asked again? (e.g. Section 4 now applies to contractors too)

The reason is required, 10 characters or more; the button reads Ask 42 people to re-confirm. On confirmation the marker moves to the current version, every existing confirmation stops counting (the rows stay in the record, tied to the version they covered), a History row with source Manual records your reason and how many confirmations it touched, and everyone who can see the document is told at once: Please re-confirm “Expense policy” with your reason as the body (the email carries it as {{reason}} under the label Action required). The toast reads “42 people will be asked to re-confirm.” No new version is written, because the text did not change.

When to use it versus Remind. Remind nudges the people who have not yet confirmed the current version and resets nobody. Ask everyone to re-confirm is for when the people who have confirmed need to read again. When nobody has confirmed the current version there is nothing to reset, so the button is disabled with the tooltip “Nobody has confirmed this version yet. Use Remind.” and Remind is the live control. The disabled control still takes keyboard focus and shows the same sentence, so the reason is never hover-only.

The one-hour guard. A policy can be re-asked at most once an hour, and the hour is shared with Remind and Request: inside it the request is refused with the toast “Readers were asked again 12 minutes ago.” and nothing changes. The guard is part of the single database statement that moves the marker, so two managers clicking at the same moment, or two app instances, produce exactly one reset and one notice. A manual ask is never debounced or coalesced (see How notices are sent): the hour is its only brake.

What readers see. The dashboard row counts the document again, the bell carries Please re-confirm “Expense policy” with the reason, and the email says the same. On the document the strip reads “Everyone was asked to confirm this document again on 3 Mar 2026. Please read it and confirm again.”, the sticky bar reads Re-confirm needed, and the card shows your reason above the statement, “Dana Whitfield asked everyone to confirm this document again on 3 Mar 2026: “Section 4 now applies to contractors too.”“, with no comparison, because the text did not change. Their earlier confirmation stays visible in the record as a row for the older version.

The action is written to the audit log as ack.reconfirmation_requested with the reason, the number of confirmations reset and the version. For scripts: POST /api/documents/{id}/ack/reconfirm with { "reason": "…" } answers { ok, reset, queued } (add user_ids to ask one person instead); 400 when the document is not published or does not require confirmation, or when the reason is shorter than 10 characters (“A reason of at least 10 characters is required.”); 429 inside the hour; 402 when read confirmation is not licensed.

Sometimes one person should read a policy again while everyone else’s confirmation stands: a confirmation that took three seconds, a colleague back from six months’ leave, a manager who wants a named report to re-read a procedure after an incident. From 1.13.0, Ask to re-confirm… on a person’s roster row does that, on the policy’s record and in the portal’s expanded roster, shown on rows whose confirmation is current; tick several rows and the bar over the roster offers the same for all of them at once (Ask 3 people to re-confirm). It is open to the same people as the whole-audience ask: document.ack_require in the space or compliance.program_manage.

The dialog names the person and the policy:

Ask Ana Ruiz to re-confirm “Expense policy”? Their confirmation of version 12 stays in the record. They are asked to read the document and confirm again.

Why is this person being asked again?

The reason is required, 10 characters or more, and the button reads Ask to re-confirm. On confirmation the document’s marker does not move. Instead the person’s own clock restarts: their confirmation stops counting for this policy, they get a fresh deadline from the policy’s rule (the fixed date while it is still ahead, otherwise today plus the days to confirm; see Deadlines and defaults), and their reminder and escalation stamps are cleared so the automatic reminders start over for the new deadline. Nobody else is affected: other people’s confirmations, the Remind cooldown and the next annual date all stay as they were. The person’s earlier confirmation stays in the record, tied to the version it covered, exactly as after a whole-audience ask. The toast reads “Ana Ruiz will be asked to re-confirm.” (“3 people will be asked to re-confirm.”), with “1 person was skipped (exempt or no longer asked).” added when someone in the list could not be asked; there is no Undo, because the notice has already gone.

What the person sees. A notice at once, Please re-confirm “Expense policy” with your reason as the body (the email carries it as {{reason}} under Action required), and on the document the strip reads “Dana Whitfield asked you to confirm this document again on 3 Mar 2026. Please read it and confirm again by 17 Mar 2026.” (their fresh deadline), the sticky bar reads Re-confirm needed, and the card shows your reason above the statement: “Dana Whitfield asked you to confirm this document again on 3 Mar 2026: “Your confirmation took three seconds; please read section 4.”“ The roster row reads Asked again 3 Mar 2026 until they confirm (the chip marks a real re-ask only: someone whose row was re-opened because they returned to the audience is simply Pending); My policies lists the policy under To confirm with Changed since you confirmed …. Everyone else sees nothing of it: someone whose confirmation lapsed with an earlier Ask everyone to re-confirm… keeps reading that one (“Everyone was asked to confirm this document again on …”), never another person’s reason.

What the record shows. The History tab gains a row: Asked again, source Manual, your reason, and “1 affected” (or however many rows the ask re-opened). Its scope is the named people, not the audience, so the popover’s status line, the Record tab’s subtitle and the portal’s “Last asked” line, which speak for everyone, do not change. Version history’s chip on the version says Asked 3 people to re-confirm for a per-person ask and Asked everyone to re-confirm for the whole audience; the audit log labels both “Asked to re-confirm”.

Who can be asked. Only people the policy currently asks: a person who is exempt, whose row was closed when they left the audience, or who was never in it, is skipped and named in the answer, and when nobody in the list has an open row nothing happens (“This person isn’t asked to confirm this document.”). A per-person ask has a brake of its own: the same person cannot be asked again within an hour of their last ask (the route answers 429 “Ana Ruiz was asked again 12 minutes ago.” when everyone named is inside it, and otherwise lists those ids in skipped). It starts no policy-wide cooldown: asking one person never blocks Remind or a whole-audience ask, and a whole-audience ask inside its own hour never blocks asking one person.

The action is written to the audit log as ack.reconfirmation_requested with scope: "users", the user_ids asked, the ids skipped, the reason and the version. For scripts: POST /api/documents/{id}/ack/reconfirm with { "reason": "…", "user_ids": [12, 34] } (up to 200 ids; duplicates collapse) answers { ok, reset, queued, user_ids, skipped }: user_ids the people whose cycle restarted and who are notified, skipped the rest of the list. 400 names the first problem (“user_ids must be a list of user ids.”, “Choose at least one person.”, “Ask up to 200 people at a time.”, the reason rule, or “None of these people are asked to confirm this document.” with skipped in the body); 429 “Ana Ruiz was asked again 12 minutes ago.” when every person named was asked inside the last hour (otherwise those ids are simply in skipped). The reask action of the assignments endpoints (see the assignments API) reaches the same statement by assignment id, so one person can be asked again across several policies in one call.

Annual re-confirmation and effective dates

Section titled “Annual re-confirmation and effective dates”

SOC 2, ISO 27001 and HIPAA expect a policy to be read and confirmed again at planned intervals, whether or not its text changed. From 1.12.0 a policy can carry a re-confirmation interval: every 6, 12 or 24 months (any whole number of months from 1 to 60 through the API) everyone who can see it is asked to read it again and confirm, with no edit and no fake version. A manager sets it on the policy’s Settings tab, in the Re-confirmation card (“Re-confirm every”), and Program settings has a default a policy receives when it is first set to require confirmation (see Program settings).

Each policy stores the date of its next cycle; nothing is computed on the fly. The date is set when the interval is set: the moment everyone was last asked plus the interval, or 14 days from now when that is already in the past, so a policy last asked two years ago schedules a first cycle people can see coming rather than firing at the next tick (the Settings tab says so and lets you move the date, or use Ask everyone to re-confirm… instead). Every reset restarts the interval from that moment: a material change, a merge, a restore, an approval, a scheduled publish, a manual Ask everyone to re-confirm… and the cycle itself all move the next date to now plus the interval. An editorial fix (confirmations kept) leaves it alone. Choosing Never clears the date.

The Re-confirmation card on the policy’s Read confirmation Settings tab holds three controls, saved with the page’s one Save settings row:

  • Re-confirm every: Never, 6 months, 12 months or 24 months; the workspace’s value is marked “(workspace default)”, and an interval set through the API outside those four shows as it is (“18 months”).
  • Next re-confirmation, shown once an interval is chosen. Changing the interval prefills it the way the server schedules it: the day everyone was last asked plus the interval, or 14 days from today when that has already passed. Move it to any day from today on; a past date is refused before anything is saved (“The next re-confirmation date cannot be in the past.”). Only a date you change is checked: a stored date that has already passed (a retired or paused policy’s, which the sweep leaves alone) is shown with This date has passed; the next cycle fires when the policy is live again. under the field, and the statement, the signature and the replacement still save. Leave it blank and the server schedules it from the last ask. When the first cycle was pushed out, the card says so under the date: People last re-confirmed 18 months ago. The first cycle is scheduled for 2 Nov 2026: change the date, or use Ask everyone to re-confirm now. (the button in the page header).
  • Effective date, see below.

Saving writes one ack.settings_changed audit row naming the fields that moved; a save that changes nothing writes none.

Every app instance checks once an hour. One statement moves the policy’s confirmation marker to the newest version, the next date to now plus the interval and the reminder time, for every published, required, unretired policy whose date has passed. The statement moves the very date its condition tests, so a second instance running at the same moment finds nothing to do: each cycle fires once however many instances run. A paused (unpublished) or trashed policy waits; it is asked once it is live again. A retired policy waits too, and un-retiring it moves a cycle that fell due meanwhile a whole interval out, so people are not asked again the day it comes back. A policy whose requirement was stopped waits in the same way, and requesting it again moves a date that passed meanwhile a whole interval out. The cycle writes a Reset row on the History tab from source Annual re-confirmation, decided by Scheduler, with the reason “Annual re-confirmation” and the counts (confirmations reset, people eligible) taken before the marker moved, and queues one notice.

Everyone whose confirmation lapsed hears Please re-confirm “Expense policy” with “Annual re-confirmation: it has been 12 months since you confirmed “Expense policy”. Please read it again and confirm.“ The notice goes out at once and is never held back by the 24-hour rule that coalesces notices from text changes. On the document the strip and the card say Annual re-confirmation. It has been 12 months since you confirmed this document. instead of showing a comparison, because the text did not change. Remind keeps its one-hour cooldown from the moment the cycle fired.

For a manager, the shield popover’s status line gains a second line, Re-confirm yearly · next 1 Jan 2027 (“every 6 months”, “every 2 years”; nothing without an interval). For a reader, the confirmation card’s version line gains Effective 1 Jan 2026 · re-confirm yearly, either half alone when the other is unset. Both dates are calendar days and read as the day that was chosen in every time zone, on the card, in the popover, on the portal row and on the Settings tab alike.

In the compliance portal a policy with an interval shows Next re-confirmation 1 Jan 2027 on its row (Compliance › Policies, after “Updated … · last reminded …”), and Compliance › Overview gains a fifth tile, Re-confirmation due in 30 days: the live policies (published, requiring confirmation, not retired) whose next date falls within the next 30 days. The policy’s Read confirmation page ends its subtitle with the cycle: “42 of 50 people have confirmed version 12 · asked 3 Mar 2026 by Matt · Cycle 4 · annual” (the first ask is cycle 1; every annual re-confirmation on the History tab starts the next; “every 6 months” for a six-month interval). Policies without an interval show neither.

A policy is dated the day its requirement is first switched on. The date stays through an off-and-on and can be changed in the Effective date field of the Re-confirmation card on the Settings tab; policies that required confirmation before 1.12.0 are undated until someone dates them.

Each cycle is recorded as ack.recert (“Asked for annual re-confirmation”) by the system, with the version everyone is asked to confirm, reset (the confirmations that lapsed) and eligible; when the notice has gone out, the delivery writes its own ack.recert with the delivery counts, as 1.10.1 did for a manual re-ask. From 1.13.0 the sweep’s entry also carries rearmed: the open assignment rows whose due date the cycle restarted (see When a cycle or a re-ask fires).

The review schedule on a document (Review every 90 days, the steward reminder in Settings → Reviews) is a separate reminder to the people who maintain the text; it never asks readers to confirm anything, and a re-confirmation cycle never reminds the steward. A policy can have both.

From 1.13.0 every person a policy asks has a due date of their own, set the moment their clock starts: when they are enrolled in the policy, when an annual re-confirmation cycle fires, and when a manager asks them again. The date is stored on their assignment row, never recomputed, and moves only when a manager extends it, exempts the person, or changes the policy’s rule and chooses to move existing deadlines.

A policy’s deadline rule is one of three things: a number of days after assignment, a fixed date (“by 30 June 2027”), or nothing, in which case it inherits. Inheritance runs policy, then space, then workspace, then the built-in: a policy with no rule of its own uses its space’s Policy defaults (Settings → Spaces; see Spaces & permissions); a space that sets none uses the workspace’s Program settings (Compliance › Settings › Deadlines and reminders; see Program settings); and a workspace that leaves a field blank uses the built-in. The policy’s Settings tab (its Deadline card) tells you which: “Inherit (14 days, from Security)”.

The built-ins are: 14 days to confirm; 14 days for new joiners; 0 grace days after the due date; the first reminder 3 days before the due date; reminders every 7 days while a confirmation is due or overdue; escalation 7 days after the due date and its grace; and no nudges for a policy with no due date. Each can be set at any of the three levels, and each level sets only the numbers it wants to: a space can change the days to confirm and leave the reminder cadence to the workspace.

A fixed date applies while it is still ahead: everyone enrolled before that day is due on it, whenever they were enrolled. Once the day has passed, people enrolled afterwards get the relative rule instead (the policy’s days, or the inherited number), so nobody starts overdue. A fixed date is a calendar day: the person is due until that day ends in the workspace’s time zone (Settings → Workspace), so the chip on the card, the roster and the email all name the day you chose.

Two settings sound alike and are not. Days for new joiners is the time a person gets when they join a policy’s audience after it was switched on: a new account from the day it is created, and anyone a rule, a group change, a space grant or a return to the audience admits later, from the day they are added; it replaces the days-to-confirm number for that enrolment. The audience a switch-on enrols, and the rows the upgrade wrote, get the days to confirm. Grace days after the due date is the time after a person’s due date before they count as overdue: with 0, the built-in, a person is overdue the moment their due date has passed; with 3, they are still “due” for three more days, and reminders and escalation wait with them. Neither changes the due date people see.

A person is overdue when their due date plus the grace has passed and no current confirmation stands. A person who confirmed is never overdue, however old the date on their row; a person with no due date is never overdue either. Overdue is a state on the roster, the portal and My policies, and a trigger for reminders and escalation; it is not a wall. A deadline never blocks the document: an overdue policy is read and confirmed the same way, the card only says “This was due 29 Oct 2026. Confirming now still records today’s date.”, and the record keeps the date it was actually confirmed. The grace is the policy’s resolved number (its space’s Policy defaults, else the workspace default, else the built-in 0 days): the reader’s chips, My policies, the dashboard, the roster and the portal all read the same one.

When an annual re-confirmation cycle fires, every open row of the policy gets a fresh due date from the cascade (the fixed date while ahead, else today plus the days) and its reminder and escalation stamps are cleared, so the notice that goes out already says the new date. The same happens to the people a manager asks again. Exempt people, and people removed from the audience, are not touched. A pause does not stop the clock: when a paused policy is published again (by hand, in bulk, on schedule or through an approved change request), or a retired one un-retired, open rows whose due date passed meanwhile get a fresh deadline from the cascade rather than coming back overdue.

Changing a policy’s relative rule applies to people enrolled from then on; the Settings tab’s Also move existing deadlines recomputes every open row from the later of the day that person’s current cycle started and the day they were assigned (so a row the upgrade wrote counts from the upgrade, and a re-asked row from its re-ask), so someone enrolled ten days ago with a new 30-day rule is due in twenty. Changing a fixed date moves everyone to it. Each move keeps the old and new dates on the person’s row (the last ten moves), so the record never loses an SLA change, and is audited as “Moved read confirmation deadlines” with the rule before and after. A move never changes who confirmed what, and it is not an extension: the roster’s Undo on an extension never undoes a move.

The Deadline card on the policy’s Settings tab is a stacked choice, saved with the page’s one Save settings row:

  • Inherit (14 days, from Security): the label names the number and where it comes from (the workspace default, built-in). The policy follows that rule and moves with it when it changes.
  • Days after assignment: each person’s own deadline, counted from the day they are asked. The field takes 1 to 365 days. A change applies to future assignments only.
  • Fixed date: one date for everyone, today or later (The deadline cannot be in the past.). Setting or changing it moves every open deadline, and the save says so: Moved 412 deadlines, with Undo. A stored date that has already passed shows This date has passed; new assignments use the inherited days until you set another date. under the field, and Also move existing deadlines then recomputes from the rule the server already applies (the inherited days) rather than re-sending the old date.

The helper line, Current audience: due 29 Oct 2026 · new joiners: 14 days after they join, reads the rule as saved, which is what the current audience has. Also move existing deadlines recomputes every open deadline from the rule as the card holds it (the fixed date while it is still ahead, else the later of each person’s cycle start and assignment plus the days) and toasts Moved 412 deadlines; the toast offers Undo when the rule changed (the previous rule is re-applied the same way, restoring both the rule and the dates) and none when it did not, because a move under the same rule has no inverse. Exempt and removed rows are never moved; each move is kept on the row’s due_history and written as ack.due_moved.

Nobody has to click Remind thirty times a week. From 1.13.0, every hour, CompassDocs reminds the people whose confirmation is due soon or overdue, and tells a policy’s owner and the overdue people’s group leads when someone is well past the date. Both run from the same cadence, which every policy inherits and may override on its Settings tab (the Reminders card).

Setting Built-in Where to change it
First reminder, days before the due date 3 Program settings → Deadlines and reminders; a space’s Policy defaults; the policy’s Settings tab
Days between reminders (while due or overdue) 7 same
Days overdue before escalation 7 same
Days between nudges, no due date 0 (never) same
Grace days after the due date 0 Program settings; a space’s Policy defaults (not per policy)

A person with a due date is first reminded when the date is within the “days before” window, then again every “days between” while they have not confirmed, before and after the date. A policy with no due date nudges people every “days between nudges” when that number is above 0; left at 0, nobody is nudged and the manual Remind is the only reminder. The grace days delay two things: a person counts as overdue (the chip, the KPI, the reminder’s wording) only once the due date plus the grace has passed, and the escalation clock starts from the same moment.

What a reminder says: in-app, Due in 3 days: confirm “Acceptable Use Policy”, Due today: … (the date passed but the grace has not), or Overdue: …, with Due 29 Oct. as its body, opening the confirmation card; by email, the editable template Read confirmation due reminder (ack_due). Someone claimed for more than three policies in the same hour gets one notice instead, You have 5 policies to confirm, listing the titles and due dates and opening My policies (template Read confirmation reminders, bundled, ack_due_bundle); their record still shows one row per policy.

Who is never reminded: people whose confirmation is current, exempt people, people removed from the audience, snoozed people (until the snooze ends), disabled accounts, and anyone on a paused (unpublished) or retired policy. A snooze only pauses reminders and escalation for that person; it changes no status, count or export (see Exemptions, extensions and snooze).

A policy overrides the cadence on its Settings tab’s Reminders card: Remind before due (days), Remind every (days while overdue), Escalate after (days overdue) and Nudge without a deadline every (days, 0 = never). A blank field inherits and says so, Inherited from workspace (3), Inherited from Security space (3) or Built-in (3); a number typed must be whole and in the field’s range. Each number that changes is written as ack.defaults_set with scope: "policy", as the space and workspace layers write theirs.

Once a person is past their due date by the grace days plus the “days overdue before escalation”, they are escalated once (extending their due date re-arms it). Per policy, one notice goes to:

  • the policy owner (the Settings tab’s Policy owner, else the space’s Policy owner from its Policy defaults); without one, whoever switched the requirement on; without either, the space’s approvers. An owner or requester must be an active account that can see the policy’s space, like a lead; one who is not is passed over for the next rung;
  • the leads of every group the overdue people belong to.

The owner is chosen on the same card (Policy owner, a people picker whose placeholder is whoever switched the requirement on): Hears when people are overdue. Leave blank for whoever switched the requirement on, else the space’s approvers. Clearing the owner falls back the same way. The owner must be an active account that can see the policy’s space; the save refuses anyone else with “The owner must be an active account that can see this space.” (the same rule holds for a space’s Policy owner).

The notice reads Overdue: 7 people on “Acceptable Use Policy”, body More than 7 days overdue: Ana Ruiz, Ben Lo, +5. Each recipient’s link opens a page they can open: a group lead’s opens the policy itself (their team view, 1.14.0, is one tab away); an owner’s, a requester’s or an approver’s opens the Policies page with that policy’s roster expanded when they hold the Compliance section, else the policy’s Record tab. The email is the template Overdue read confirmations, escalation (ack_escalation, whose portal_url is that link). The overdue people themselves are not told they were escalated.

The scope rule: a lead only hears about policies in spaces they can read. A lead of a company-wide group is not told about a private space’s policy unless they are a member of that space (or can read every space); owners and approvers can read the space by construction.

Every automatic reminder writes one row per person and policy in the notice record (kind due), every escalation one row per recipient (kind escalation), with the channels that actually went out, exactly like a manual request. The audit log gets ack.reminder_auto (how many people a policy’s reminder went to, how many were overdue) and ack.escalated (how many people were overdue, how many recipients) when the sweep claims the rows, and a second entry of each, with the delivery counts, when the notices have gone out. The per-person proof is the notice record.

A crash between the claim and the notice skips that cycle’s reminder: the rows are stamped as reminded, nothing was sent, and the next cadence step sends the next one. This is the same trade the training reminders make, so that no restart can ever send a reminder twice.

Under your name → Notifications, the rows Policy reminders and Compliance escalations switch each channel. Like read confirmation requests, these emails ignore the master email switch; the per-row email choice is honoured, email only, and the record shows “email opted out”. The in-app notice always arrives.

A manager who leads a group (Training → Team leads, or the Overview’s Team leads block; the same leads Training uses) can see where that group’s members stand on the policies that ask them, and nudge one person at a time (1.14.0). Leading a group opens the view only when Program settings → Group leads see policy standing is on: it is on for a new workspace and off for one upgraded from 1.13.0, because widening what every existing lead can see is a permissions change an administrator should make on purpose. The permission Read team policies (compliance.team_read, held by the Compliance manager role) opens the view for every group, setting or not, and the compliance record of anyone in a group (the policies in spaces the holder can read), so every link on the team page opens; it is how an administrator looks at a lead’s view to answer a question about it.

The view is Compliance › Team, a tab the section shows only to people it opens for. One card per group you lead: its name, {n} members · {m} confirmed · {k} open and an {x} overdue chip when anyone is; under it a table of Person (a link to the person’s compliance record), Policy, Status (Confirmed, Pending, Overdue or Exempt), Due and Actions, and when the person was last reminded. Show narrows every card to the open rows (pending and overdue) or the overdue ones. The view is scoped twice: you see only the members of the groups you lead, and only the policies in spaces you can read. A policy a member must confirm in a private space you cannot see is counted on the member’s row (“and 2 policies you can’t view”) and never named; a member with nothing you can see reads “2 policies you can’t view”, and one with nothing to confirm at all “No policies to confirm”. The group header counts members, confirmed and open rows, and overdue people (exempt rows count nowhere).

Remind on a pending or overdue row sends that person one reminder, as a reminder from the lead (delivered within the minute, logged in the person’s notice record, audited “Sent read confirmation reminder” with the lead as the actor); the toast reads Reminder queued for Ana Ruiz. It follows the row’s own cadence: inside the policy’s days between reminders (the policy’s number, else its space’s, else Program settings’, else 7) the server answers Ana Ruiz was reminded 3 days ago., that sentence is the toast, and nothing is sent. A lead can remind only members of groups they lead, and a programme manager anyone the view shows; for a Read team policies holder who is neither, the row’s Remind is disabled with the tooltip Only the group’s lead or a programme manager can remind (the API answers 403 all the same).

Finding the page. A lead sees a banner at the top of My policies, You lead a team. See your team’s policies, and the Team tab in the Compliance row; both appear only while the page would open for them. Someone who holds Read team policies sees every group there, and “You don’t lead any groups yet.” in a workspace with no groups.

The weekly digest. Every lead with an email address gets one email a week (the first hourly tick of each ISO week, UTC): a Training section when training is licensed, and a Policies section when read confirmations are licensed and leads see policy standing, one line per group (“Support: 38 confirmed · 6 open · 2 overdue”) followed by the three most overdue people within the lead’s scope (“Ana Ruiz · Code of conduct · due 3 Oct 2026”). The email is the template Team digest (leads) (training_team_digest, see email templates): an override saved before 1.14.0 keeps rendering as it did and must add {{policy_summary}} to show the new section. Nothing goes out without SMTP, and a week is claimed only once across every instance. A digest that carried the Policies section is one row of kind digest on each lead’s notice record.

API. GET /api/compliance/team answers { groups: [{ id, name, members, confirmed, open, overdue }], rows: [{ group_id, group_name, user_id, user_name, username, assignment_id, document_id, title, space_id, status, due_at, acknowledged_at, reminded_at, hidden_count, may_remind }] } (may_remind says whether this viewer may remind the row: true for a lead of a group the member is in and for a programme manager, false otherwise, as for a Read team policies holder outside the groups they lead); 402 without the licence; 403 “You don’t lead any groups.” or “Group leads don’t see policy standing in this workspace.”. POST /api/compliance/team { assignment_id } answers { ok, queued: 1, outbox_id, assignment_id, user_id, document_id }; 429 “{Name} was reminded {n} days ago.” inside the cadence; 403 “You can only remind members of groups you lead.”; 404 for a row the viewer cannot see; 400 “{Name} has already confirmed “{title}”.“ or “{Name} is exempt from “{title}”.“ PUT /api/admin/compliance/settings { "ack_defaults": { "leads_see_policies": true } } turns the lead view on (audited settings.compliance). The leads themselves are read and written through GET / PUT /api/compliance/leads (the Compliance section to read, compliance.program_manage to write, up to 50 leads per group; an account without a display name is listed by its username; audited training.leads_set, “Set team leads”, with via: "compliance", because the list is one for both programmes).

From 1.17.0 a manager also sees a Team compliance tile on their dashboard. It goes by the directory’s Reports to field, not by groups: it is for anyone whose own directory entry has direct reports with active accounts linked to their entries. It shows the three policies with the smallest share of those reports confirmed, only policies in spaces the manager can read, each with a bar and “4 of 6” (confirmed of those the policy asks). Hover or focus the count for who is still to confirm (“Still to confirm: Tom Becker, Aisha Rahman”, up to five names and how many more). See who’s outstanding opens Compliance › Team when the manager can open it, that is, when they lead a group or hold Read team policies.

The tile follows Group leads see policy standing: with it off, only holders of Read team policies (compliance.team_read) see it. It is left out when nothing is owed, and isn’t offered in Edit dashboard without the Policy acknowledgements entitlement.

Every policy has a Read confirmation page, reached from the View record link in the popover, with three tabs under one header: Record, History and Settings. Ask everyone to re-confirm… and an Export menu sit in the header, so they are one click away on any tab. The Export menu offers Export CSV (the per-policy record, columns below), Ledger CSV (all confirmations) (every confirmation ever recorded for the policy; shown only to people who hold compliance.export, because it carries the IP address and user agent of each confirmation) and Print…. The printed page opens with a title line, “Read confirmations · {policy} · as of {date}”, and leaves every control off paper.

The subtitle is one sentence:

42 of 50 people have confirmed version 12 · asked 3 Mar 2026 by Matt: “Section 4 now covers contractors”

The reason is omitted when there is none, and “by Matt” is omitted when the ask came from an edit by the version’s own author (the document already says who published that version). Before the first decision on 1.10.0 the sentence ends “asked {date}”: the date everyone was last asked, with no name and no reason, because earlier resets left no record.

Below it, the roster is a table with three controls over it. Show narrows it to All, Pending, Confirmed or, from 1.13.0, Overdue or Exempt people; Filter people narrows it to names, usernames or email addresses that contain what you type, with a live count (“3 of 42 people”); and View switches between Current version and All confirmations. Long rosters are paged at 50 rows.

Current version lists everyone expected to confirm: from 1.13.0 the people on the policy’s open assignment rows (see Who must confirm), which by default means active accounts that can see the document’s space, so private spaces only count the granted groups; exempt people stay listed with their chip, and people who left the audience do not. Each row shows the person and their email (from 1.14.0 the name links to the person’s compliance record, for a viewer who may open it), their role, a Confirmed or Pending chip, the confirmation time and the version confirmed (“v12”). From 1.13.0 the chip reads Overdue once the person’s due date and the policy’s grace have passed, or Exempt on an excused row, and a person asked to re-confirm on their own (a real re-ask with a reason, never a row the audience merely re-opened) also carries Asked again 3 Mar 2026 until they confirm; a Due column carries each person’s date (blank when the policy sets no deadline) and an Exemption column, in plain text, the exemption’s reason, who recorded it and until when (“On leave until 1 Dec 2026 · Priya Shah · 3 Oct 2026”), the reason someone was asked to re-confirm on their own and who asked, or “Snoozed until 22 Oct 2026” while reminders are paused (see Deadlines and defaults and Exemptions, extensions and snooze). Show narrows the table to Pending (everyone who has not confirmed, overdue people included, never an exempt person), Overdue, Confirmed or Exempt. Each row has a checkbox and its own actions (see Acting on a row). Rows from before 1.10.0 show “v11 (inferred)”, and a footnote under the table explains it: Confirmations before 1.10.0 did not record the version; where a version is marked (inferred) it is the one that was current at the time of the confirmation. A Signed column shows the name the person typed when the policy requires a signature, with a pen icon before it, and is blank otherwise.

Two more columns carry the reading evidence (1.11.0). Read time is how long the document was open on screen for that person since everyone was last asked: “6 min”, “under 1 min”, or blank when no view was recorded (including every confirmation from before 1.11.0, and a confirmation made through the API without opening the page). It is shown in amber only when it is shorter than a quarter of a typical read of the document at 200 words per minute, measured against the live text, with the tooltip “Shorter than a typical read of this document”. Reached end (self-reported) shows a check mark when the reader’s browser reported that the end of the text had been on screen, or that focus had entered the confirmation card, when they confirmed; nothing otherwise. A footnote under the table repeats what the two columns mean and that neither is proof of comprehension.

All confirmations (1.12.0) is the ledger: every confirmation ever recorded for this document, newest first, including the ones the current view no longer shows because the person confirmed again later or everyone was asked again after it. Each row shows the person, the confirmation time, the version, the signature and the read time; a viewer who holds compliance.export also sees the IP address the confirmation was recorded from. A confirmation whose account has since been removed shows the name recorded at the time with an Account removed chip. The Show control does not apply here (every row is a confirmation); the people filter does.

A collapsed Former staff and disabled accounts section lists the newest confirmation by each person who is no longer asked, with a Left or Disabled chip, the date, the version, the signature and the same two evidence columns. They are kept for the record and never counted in the numbers above.

Export CSV in the Export menu downloads the per-policy record; Ledger CSV (all confirmations) downloads the ledger (both below).

If you hold document.ack_require on the policy’s space or compliance.program_manage, each row of the roster (on the Record tab and in the portal’s expanded policy row) offers actions as icon buttons (the rules behind each are under Exemptions, extensions and snooze):

  • Extend due date… asks for the number of days (7 by default, up to 365) and an optional reason, recorded on the compliance record.
  • Exempt… asks for a reason (required, shown on the record and in exports) and an optional end date. You cannot exempt yourself; ask another compliance manager.
  • Remove exemption returns the person to Pending with a fresh due date from the policy’s rule.
  • Snooze pauses automatic reminders for a week (the date is in the toast: “Snoozed reminders for Ana Ruiz until 27 Oct 2026”); Unsnooze resumes them, and its Undo re-snoozes for the days that were left. Snoozing only pauses automatic reminders until the date. It never changes the person’s status, the numbers, the record or an export; the sentence is repeated under the table whenever a snoozed row is on the page.
  • Ask to re-confirm… (on a row with a current confirmation) asks for a reason of at least 10 characters and asks that one person again; their earlier confirmation stays in the record and nobody else is affected (see Asking one person again).

Every action is reversible from its toast (“Exempted Ana Ruiz from “Expense policy”“ with Undo; “Extended 12 deadlines by 7 days”; “Snoozed reminders for Ana Ruiz until 27 Oct 2026”), except asking someone to re-confirm, whose notice is already on its way (“Ana Ruiz will be asked to re-confirm.”). Tick several people and the bar above the table (“3 selected”, with Clear selection) offers the same actions, under the same labels, over all of them, up to 200 at a time; each action only reaches the rows it applies to (an extension never touches a confirmed person, a re-ask never touches a pending one). A steward acts through the document’s own route, a program manager through the admin route (see the assignments API); a control is disabled while its request is in flight, and focus returns to the row after a dialog. Remind stays the policy row’s button in the portal; from 1.14.0 a single person is reminded from the team view by their group lead, or from their record with Remind about everything outstanding.

The History tab is the decision ledger: one row for every time the text of a confirmed document changed, whichever way the editor decided, and every time a manager asked everyone to re-confirm, so “why were 42 people not asked again?” is answered by a row rather than by an absence. Newest first, the columns are Date; Decision (an Asked again or Kept chip, with suggested keep, chose reset beneath it when the editor overrode the guess); Version (the version the decision concerns, “v12”); By (who decided; when no signed-in person took the decision the row names the author of the version, or Automatic when there is none; an approval row also names the person who submitted the change and, from 1.11.0, what they recommended when it differs from what was applied: “recommended by Alex”, or “recommended material change by Alex” when the approver kept confirmations against a reset recommendation); Source (where the change came from: an edit, a branch merge, a restore, an approval, the API, MCP, an import, a bulk edit, a schedule, Image for a picture placed by the Claude connector, Ticket for a file dropped through an upload link (both keep confirmations and are recorded, never silent), Manual for Ask everyone to re-confirm…, or Default when the document’s edit default decided); Reason (the change note, or the manager’s reason); and People (“42 affected”, the confirmations the decision touched). A row whose scope is a named list of people (a per-person re-ask, 1.13.0) is still Asked again with source Manual; its “n affected” is the number of people asked, and it does not move the popover’s or the portal’s “last asked” date, which speak for the whole audience.

The ledger starts on the day the workspace first runs 1.10.0. Until the first text change the tab reads “No decisions recorded yet. Edits to this document from 1.10.0 on are listed here.”, and earlier resets left no trace that could be reconstructed.

Version history shows the same decisions in place: on a document that requires read confirmation, a version with a ledger row carries an Asked everyone to re-confirm (from 1.13.0 Asked 3 people to re-confirm for a per-person ask) or Editorial (kept) chip. Versions saved before 1.10.0, and every version of a document that does not require confirmation, carry no chip.

GET /api/documents/{id}/ack returns the read-confirmation summary the editor and the record pages use: required, eligible, current, pending, reset_at (the marker), version_id and rev (the version people were asked to confirm), latest_version_id and latest_rev (the newest text), edit_default (ask, material or reset, the document’s own value), workspace_edit_default (the program setting the popover marks “(workspace default)”), keep_reason_required (the program setting the editor’s keep side follows) and last_decision (the newest History row taken for the whole audience, scope all, or null; a per-person re-ask, scope users, is a row on the History tab but never the document’s last decision). It never lists names. Add ?baseline=1 to include baseline (version_id, rev, title, content): the text readers last confirmed, for a what-changed comparison. Anyone who may edit the document or read its roster may call it, but a draft is hidden (404) from anyone who may not read drafts, exactly as the document itself is; the response is never cached (Cache-Control: no-store); 402 when read confirmation is not licensed.

GET /api/v1/documents/{id} (the public API, bearer token) returns ack_required, ack_edit_default and ack: { confirmations, eligible, version } for the same standing, counts only, so an integration can show a person the numbers before it decides (see the API reference).

PATCH /api/documents/{id}/ack (needs document.ack_require in the document’s own space; holding it in another space is answered 403) takes any of required (boolean), edit_default (ask | material | reset) and notify (boolean, with required: true: queue the request notice, as Require and notify does; omit or false for a quiet switch-on). It answers { ok, ack_required, edit_default, queued, eligible }, read after the change, whichever fields were sent. A body with neither required nor edit_default, or a field of the wrong type, is refused with 400; required: true on an unpublished document is refused with “Publish the document before requiring a read confirmation.”, and on a retired policy with 409 “This policy is retired. Un-retire it first.” (see Retiring a policy).

POST /api/documents/{id}/ack takes { version_id, reached_end?, signed_name?, password? }. version_id is the version on the page (400 when absent; 409 { stale: true } when it is no longer the newest version or the marker has moved past it). reached_end is the card’s own report and is recorded only for the JSON boolean true; a missing field, a string or a number records false. The read time is never read from the body: the server sums the person’s view heartbeats for the document since everyone was last asked (each view bounded by the time the page was actually open) and stores that with the row. The JSON record (GET /api/documents/{id}/acknowledgements) carries read_seconds and reached_end on every row and former-staff row (null on a pending row). On a policy that requires a typed signature, signed_name must match the account’s display name (its username when it has none; case and spacing do not matter) and a local account’s password must check out, else 409 with the sentence the card shows and nothing is recorded; 429 while the sign-in throttle holds. The 200 body carries signed_name. The record stores the statement the server served, never one the client sends, and the JSON record carries statement_text, signed_name and identity_proof on every row (null while pending).

GET /api/documents/{id}/acknowledgements?view=all (1.12.0) answers the ledger behind the All confirmations view: rows, newest first, each with name, username, email, role (the account as it is today, or the name and email recorded at the time when account_removed is true), acknowledged_at, current (counts against the marker), superseded (the current view no longer shows it), version_id, rev, content_sha256, version_source, statement_text, signed_name, identity_proof, read_seconds, reached_end, ip and user_agent. The last two are blank unless the caller holds compliance.export; the roster permission (document.ack_roster_read on the space) opens the rest. can.export says which the caller got.

PATCH /api/change-requests/{id} takes { action: "approve" | "reject", note?, ack_policy?, ack_reason? } and needs change_request.decide on the document’s own space (and on the target space when the request moves the document); a role holding it on another space is answered 403 before anything is read. On a request that changes the text of a published document that requires read confirmation, ack_policy (keep or reset, with an ack_reason: a reset needs a reason of at least 10 characters; a keep follows Program settings’ “Require a reason for editorial fixes too”) is the approver’s own decision, and its reason is also the version note; without it the submitter’s recommendation on the request applies (its stored reason is checked against the same rule at approval; a recommendation whose reason fails it counts as none), and when the request carries none the call is answered 428 { error, decision_required: true, confirmations, eligible, baseline, change, suggested } and nothing is applied. 400 for a bad policy (validated whenever it is sent, substantive change or not) or an unusable reason; 409 once the request has been reviewed. The response echoes ack_outcome as a save does. POST /api/documents/{id}/merge and POST /api/documents/{id}/versions/{vid}/restore take ack_policy and ack_reason with the same 428 and 400 answers, and note for the version note (the app sends the note from the question as both); the merge route also takes base_updated_at and answers 409 “The live document changed since this branch was created. Review it before merging.” when the source was saved since.

Both exports keep every column they had before 1.10.0 in place and append the new ones at the end, so existing spreadsheets and scripts keep working. Both guard against spreadsheet formula injection: a cell that starts with =, +, - or @ is prefixed with an apostrophe, so a name, a title or a reason is never run as a formula when the file is opened.

The per-policy CSV (Export CSV on the record page, or GET /api/documents/{id}/acknowledgements?format=csv):

Column Meaning
name, username, email, role, status, acknowledged_at, document As before. status is acknowledged, pending or, from 1.13.0, exempt.
revision_as_of The confirmation marker: when everyone was last asked. Changed meaning: before 1.10.0 this was the document’s last save.
version The version confirmed, as Version history numbers it (“v12”).
version_id The internal id of that version, stable across renumbering.
content_sha256 SHA-256 of that version’s text, recorded at confirmation. Empty on inferred rows.
version_source recorded (the hash was taken when the person confirmed) or inferred (a confirmation from before 1.10.0; the version was worked out from its time).
former_staff left, disabled, or empty for a current account.
asked_at, asked_by When everyone was last asked to re-confirm, and by whom.
reason The change note, or the manager’s reason, recorded with that decision.
kept_since_asked How many text edits were saved as editorial fixes since the last ask.
ip The IP address recorded with the confirmation. Filled only when the caller holds compliance.export; blank otherwise (1.10.1).
user_agent The browser’s user agent string recorded with the confirmation (1.12.0; blank on confirmations made before it). Filled only when the caller holds compliance.export; blank otherwise.
version_published_by Who published the version the row cites (1.10.1; the byline for versions written before the stamp). Empty on inferred rows.
version_approved_by Who approved that version when it came through the review queue (1.10.1); empty otherwise.
read_seconds How long the document was open on screen for the person since everyone was last asked, in whole seconds, summed on the server when they confirmed (1.11.0). Blank when no view was recorded, and on every confirmation from before 1.11.0.
reached_end_self_reported yes when the reader’s browser reported the end of the text on screen when they confirmed, no when it did not (a confirmation through the API, or one from before 1.11.0), blank while pending (1.11.0).
statement The confirmation statement the person confirmed under, as the card showed it (1.12.0; blank on earlier confirmations and while pending).
signed_name The name the person typed when the policy requires a signature (1.12.0); blank otherwise.
identity_proof How the signer was identified (1.12.0): password (a local account re-entered its password), sso_session (a single sign-on account, identified by its session) or typed_name_only; blank when the policy requires no signature.
recert_months The policy’s re-confirmation interval in months (1.12.0); blank when it has none. Read from the policy, so the same on every row.
effective_at The policy’s effective date, YYYY-MM-DD (1.12.0); blank until set.
next_recert_at When everyone is next asked to re-confirm (1.12.0); blank when the policy has no interval.
exempt_reason The reason recorded when the person was exempted from this policy (1.13.0); blank unless the row’s status is exempt.
exempt_by Who recorded the exemption (1.13.0); blank unless exempt.
exempt_until When the exemption ends by itself, if an end date was set (1.13.0); blank for a standing exemption or a row that is not exempt.
snoozed_until When automatic reminders resume, while the row is snoozed (1.13.0); blank otherwise. A snooze changes nothing else on the row.
due_at The person’s deadline for this policy (1.13.0); blank when the policy sets none.
set The policy sets the policy belongs to, joined with “; “ (1.14.0); blank when it is in none. Read from the policy, so the same on every row.
asked_through The handbook the person is asked through, when they can’t open the policy’s space (1.18.3): its title, or “a handbook” when you can’t see which; blank for everyone who can open the space.

In full, the per-policy header is (36 columns):

name,username,email,role,status,acknowledged_at,document,revision_as_of,version,version_id,content_sha256,version_source,former_staff,asked_at,asked_by,reason,kept_since_asked,ip,user_agent,version_published_by,version_approved_by,read_seconds,reached_end_self_reported,statement,signed_name,identity_proof,recert_months,effective_at,next_recert_at,exempt_reason,exempt_by,exempt_until,snoozed_until,due_at,set,asked_through

The portal-wide CSV (Matrix CSV in the Export menu on the Compliance page) leads with the document and space, has no status column at position 5, appends the same nine columns, version to kept_since_asked, with the same meanings, has no IP column, then the same two provenance columns, then the same six columns of 1.12.0 (statement to next_recert_at), from 1.13.0 the same five exemption and deadline columns (exempt_reason to due_at) and, from 1.14.0, two more at the end: status (confirmed, pending, overdue or exempt, the same rule as the Overview’s tiles and the bars, with the policy’s grace applied; a former-staff row reads confirmed) and set (the policy’s sets, joined with “; “). Before 1.14.0 the file had no status column, so an exempt row was one whose exempt_reason is filled. The reading evidence columns (read_seconds, reached_end_self_reported) are on the per-policy record only. From 1.18.3 asked_through comes last, as in the per-policy file. 33 columns in all:

document,space,revision_as_of,name,username,email,role,acknowledged_at,version,version_id,content_sha256,version_source,former_staff,asked_at,asked_by,reason,kept_since_asked,version_published_by,version_approved_by,statement,signed_name,identity_proof,recert_months,effective_at,next_recert_at,exempt_reason,exempt_by,exempt_until,snoozed_until,due_at,status,set,asked_through

The ledger (1.12.0) is every confirmation ever recorded, not one row per person: a person who confirmed v10, then v12 after everyone was asked again, is two rows. Ledger CSV (all confirmations) on a policy’s record page (GET /api/documents/{id}/acknowledgements?format=ledger) covers that policy; GET /api/admin/compliance?format=ledger covers the whole workspace, including policies since purged from the Trash (named by the title recorded at the time, with an empty document_id and space), grouped by document and newest first within each. Both require compliance.export and answer 403 without it, because every row carries the IP address and user agent.

document,document_id,space,name,username,email,role,acknowledged_at,superseded,version,version_id,content_sha256,version_source,statement,signed_name,identity_proof,read_seconds,reached_end_self_reported,ip,user_agent,account_removed

superseded is yes when the current view no longer shows the row: a later confirmation by the same person exists, or everyone was asked again after it. account_removed is yes when the account is gone; the name and email are then the ones recorded at the time, and username and role are empty. version is empty for a purged policy (the numbering went with its versions; version_id and content_sha256 stay, and the archived text still resolves). The other columns mean what they mean in the per-policy file.

Every download of any of these files is written to the audit log as ack.export (see Who can manage the program).

Every row carries version_id and content_sha256, and the hash is taken from the stored version’s text, never from the live document, so a record can never cite words the person did not see. To check one:

  1. Open the version in Version history and take its Markdown exactly as stored. If the policy has since been purged from the Trash, ask an administrator for the archived text (see Retention and leavers).
  2. Compute SHA-256 over the UTF-8 bytes of the Markdown body. The title is not part of the hash, and a trailing newline added by an editor will change the result.
  3. Compare with content_sha256.
  4. Check who published and approved that version (below).

A version_source of inferred means there is no hash to compare: the row predates 1.10.0, and its version is the newest one that existed when the person confirmed. The cut-over date is the day the workspace first started 1.10.0.

The question after “who confirmed v12?” is “who published v12, and who approved it?”. From 1.10.1 every version records that as data, not only as text in its note:

  • Published by: the signed-in person who saved the version. A version written through the API, a merge, a restore or a schedule names the acting account; a version with no signed-in person behind it names the author on the byline.
  • Approved by, and the change request that carried it: for a version that came through the review queue, the reviewer who approved it, with the submitter as the publisher.

The names are kept on the version itself, so they outlive a deleted account (the account link is cleared; the name stays). On the History tab an Approval row names the same two people, so the ledger and the version agree; Version history shows “approved by” on the version row, the card at the end of the document names the approver beside the publisher, and both CSVs carry the pair as version_published_by and version_approved_by.

Versions written before 1.10.1 are filled in once at the upgrade from what they already held: the publisher from the byline, and the approver from the “approved by” suffix of the version note where the review queue wrote one. Versions with neither stay blank; provenance is complete from the upgrade onward.

Everything one person must confirm, has confirmed and was told, on one page (from 1.14.0, Compliance › People › the person, or /compliance/person/{id}; your own is My policies › Your record or View your record on the card at the end of a policy you confirmed, /compliance/person/me). The page lists:

  • Current requirements: one row per policy the person is asked to confirm now (or is exempt from), with its set, the standing (Confirmed, Pending, Overdue or Exempt), how and when they were assigned (“everyone in Security space”, “by group”, “named person”, “by role”, “by hand”, “new joiner”, “Onboarding set”), the deadline, the confirmation that counts (the version, marked “(inferred)” for a confirmation from before 1.10.0 as on the Record tab, its hash, the typed signature and what it was checked against (password, SSO session), the read time and whether the end of the document was on screen), the IP address where you may see it, the exemption with its reason, who recorded it and until when, a snooze and a per-person re-ask with its reason.
  • Prior confirmations: every earlier confirmation the current view no longer shows, each with what replaced it (“Superseded by: v13 on 3 Sep 2026 - “Section 4 now covers contractors”“, “annual re-confirmation on 1 Oct 2026”, or the person’s own later confirmation); assignments that ended when the person left an audience (“Required 1 Mar 2026 to 30 Sep 2026 (left Engineering)”); rows on a policy that stopped asking (its requirement switched off, or the policy paused or trashed with it on), marked No longer required, reading “Required from 1 Mar 2026” and keeping the standing frozen when it stopped (a confirmation stays on the record after a manager stops the policy); and rows on retired policies with the standing frozen at retirement.
  • Notices: every request, reminder, re-confirmation notice, escalation and digest the person was sent, with the kind, the policy, the channels that went out and who sent it.

CSV downloads the same rows (22 columns, below, under the same filter as the page); Print / PDF prints the page as evidence with a footer naming who generated it, when, and the CompassDocs version. A line “{n} policies you can’t view” appears when the viewer may not see everything.

Who can open a record. Any signed-in account may ask; what comes back depends on the viewer, and the rule is applied inside the query, so nothing a viewer may not see reaches the page or the file:

Viewer Sees
The person themselves every row
A holder of the Compliance section (compliance.program_read) every row
A holder of Read team policies (compliance.team_read), for anyone in a group (everyone the team page lists); or a lead of a group the person belongs to, while Group leads see policy standing is on under Program settings the person’s policies in spaces the viewer can read; the rest is a count (“2 policies you can’t view”)
A holder of Read the confirmation record (document.ack_roster_read) on a space, for someone with an assignment or a confirmation on a policy in that space; a workspace-wide grant, for anyone the person’s policies in the spaces the grant covers; the rest is a count
Anyone else Page not found

The filter runs in the database, so a lead never receives a private-space policy’s title or IP address, not even in the CSV. The recorded IP address is shown only on rows whose space the viewer holds document.ack_roster_read on, or to a holder of compliance.export; it is blank for everyone else, the person included.

Actions on a record. Holders of compliance.program_manage see an Actions card (off paper): Remind about everything outstanding queues one bundled reminder listing every policy the person still has to confirm (the bulk endpoint’s remind_people over the person’s open rows, in one call). It asks first, Remind Ana Ruiz about 3 policies? (“One notice listing every outstanding policy goes out now. A policy reminded within the hour is skipped. This cannot be undone.”), and toasts Reminded Ana Ruiz about 3 policies., or the reason when every policy was skipped; the button is disabled while nothing is outstanding. Ask to re-confirm…, Exempt from…, Extend… and Remove exemption each list the policies they apply to and ask in the same words as the Record tab, the reversible ones offering Undo in the toast. Administrators who may delete accounts also see Anonymise compliance record… here, the same irreversible erasure as on the Users page (see Retention and leavers).

Where it is linked. The Record tab’s and the portal’s roster rows, the People page, the needs-attention queue, the team view and the Users page’s Compliance record button open a person’s record; View your record on the confirmed card and Your record on My policies open your own.

API. GET /api/admin/compliance/people/{id} answers { person, current, history, notices, hidden, viewer_kind } (viewer_kind one of self, program, lead, roster; me is the viewer’s own record; 404 “No such person.” through the rule above, the same answer an unknown id gets; 402 without the licence). Each confirmation row carries version_source (recorded, or inferred for a confirmation from before 1.10.0). ?format=csv downloads the same rows with 22 columns: policy, set, status, source, assigned_at, removed_at, due_at, acknowledged_at, version, version_id, version_source, content_sha256, signed_name, identity_proof, read_seconds, reached_end, ip, exempt_reason, exempt_by, exempt_until, superseded_by, asked_through (asked_through from 1.18.3). status is confirmed, pending, overdue or exempt on a current, closed, stopped or retired row (a row on a policy that no longer asks is in the file with its frozen standing) and superseded on a prior confirmation; removed_at is filled on a closed assignment (blank on a stopped row), and superseded_by on a prior confirmation, or “no longer required” on a stopped row. Every JSON row carries stopped, true only on such a row. Every download is audited as Exported a person’s compliance record (ack.record_exported) with the person, the number of rows, the viewer’s kind and the hidden count.

Nothing is sent while you wait. Requests, reminders, re-confirmation notices and, from 1.13.0, automatic reminders and escalations are queued and delivered within a minute by the server, so a material change to a policy with hundreds of readers saves at once, and a dropped connection can never leave half the people emailed.

  • Edits are debounced and coalesced. A notice caused by a text change (an edit, a merge, a restore, an approval, a scheduled publish, the API or the Claude connector) waits two minutes before it goes out, and notices for the same policy that are waiting together become one notice carrying the latest change note, so three saves in an afternoon are one message. At delivery, a person who already heard about the same policy in the last 24 hours is not told again unless they confirmed it since. In short: a person hears at most once a day per policy about changes, unless they confirmed in between.
  • Manual asks are immediate. Request, Remind, Require and notify and Ask everyone to re-confirm… go out at once and are never coalesced; their brake is the one-hour limit per policy.
  • Who is told is decided at delivery, not when the button was clicked: someone who confirms in the minute in between is not told, and if the policy stops requiring confirmation, is unpublished or is trashed before the notice goes out, nobody is told.
  • Every notice is recorded per person. The record keeps, for each person and notice: the date, the kind (request, reminder, updated and, from 1.13.0, due for an automatic reminder and escalation), the policy, who asked, and the channels that actually went out (in-app, email, webhook). Email counts only when the mail server accepted the message. A person who turned the Read confirmations email off under your name → Notifications still gets the in-app notice, and their row shows “email opted out”. The record outlives the account (name and email snapshot) and the policy (title snapshot), and anonymising a person rewrites it like every other record.
  • Failures are retried. If the mail server is down, the delivery is retried after ten minutes, up to three times, emailing only the people who were not reached and never repeating the in-app notice. The third failure closes the queue entry with its error and is written to the audit log as ack.notice_failed; an entry whose server instance died during its last attempt is closed and audited the same way by the next delivery run, so nothing stays half-done and unreported.
  • Every app instance delivers. The queue is claimed row by row with a database lock, and an instance keeps its claim fresh while it delivers, so running several instances never sends a notice twice, however long a delivery takes. Only an instance that dies mid-delivery leaves rows that another instance picks up ten minutes later, and that instance carries on from the record: people already told in-app get the email only.
  • What people typed stays text. A change note or a re-ask reason appears in the email exactly as written: markdown or HTML in it is shown as characters, not rendered, so a note cannot add a link or an image to the mail your workspace sends.
  • The audit log gets one entry per delivery, when it finishes: ack.requested, ack.reminder_sent, ack.reminder_auto, ack.reset_notified or ack.escalated, with how many people were pending, emailed, recorded, skipped as already told today, and (for automatic reminders) bundled.

A deliberate exception: compliance emails ignore each person’s master email-notification switch. A read confirmation request is a workplace requirement, not a courtesy update, and muting document chatter shouldn’t hide a policy you’re required to read. The per-event switches for Read confirmations and, from 1.13.0, Policy reminders and Compliance escalations under your name → Notifications are honoured, email only, and the record shows it. Links in the emails use the workspace’s custom domain (Settings → Domain & HTTPS, see Custom domain & HTTPS) or, when none is set, the address the request that queued the notice was made to. The wording of every notice is an editable template under email templates: Read confirmation request for a request, a manual reminder and a re-confirmation (its default subject is [{{org_name}}] {{request_label}}: confirm "{{doc_title}}"{{due_suffix}}) and, from 1.13.0, Read confirmation due reminder, Read confirmation reminders, bundled and Overdue read confirmations, escalation for the hourly sweep’s notices (see Reminders and escalation); the tags each carries are listed there.

Since 1.13.0 the request, reminder and re-confirmation emails say when: {{due_date}}, {{due_line}} (“Please confirm by 29 Oct 2026.”), {{due_suffix}} (“ by 29 Oct 2026“, on the end of the default subject) and {{days_left}} are filled from each recipient’s own assignment row and left blank for a person with no due date. One notice to people with different deadlines is rendered once per distinct date, and the in-app line ends “by 29 Oct 2026” the same way. Dates follow the workspace’s date format and time zone.

From 1.14.0 the workspace’s outgoing webhooks (Settings → Notifications, see Notifications) can carry five policy events to a Teams, Slack, Webex, email or generic JSON channel, so a #compliance channel hears what the programme is doing:

  • Read confirmation requested (policy.requested): when a policy starts asking people: required with notice (from the portal, the shield or a set assignment), Ask everyone to re-confirm…, an audience save that adds people, or an annual cycle starting. Once per policy. Priya Shah asked 412 people to confirm “Acceptable Use Policy” by 29 Oct 2026. / Annual re-confirmation of “Acceptable Use Policy” started: 412 people to confirm. A re-ask from the Policies bulk bar sends one per policy, as the row’s own does.
  • Confirmation deadline approaching (policy.deadline_approaching): the hourly sweep finds a live policy whose next deadline is inside its days before due window (the policy’s number, else its space’s, else Program settings’). Once per policy per due date. “Acceptable Use Policy” is due in 3 days: 88 people still to confirm. A due date is claimed only when a subscribed channel covers the policy’s space (no space filter, or one that includes it): while every subscribed channel is scoped to other spaces nothing is claimed, so a channel added later still hears a deadline that is still ahead. The hourly housekeeping removes a purged policy’s claim row.
  • Confirmations overdue (policy.escalated): the escalation sweep escalated someone on the policy (past the due date, the grace and escalate after). Once per policy per sweep. 7 people are more than a week overdue on “Acceptable Use Policy”.
  • Compliance summary (policy.weekly_summary): the first hourly tick of each ISO week, one message per channel, only to channels with no space filter. Policy compliance this week: 94% · 23 overdue across 4 policies · lowest: Data Handling 71%., plus Monthly snapshot sha256 a1b2c3… when the month’s evidence snapshot has been taken (see Evidence).
  • Policy retired (policy.retired): a policy is retired from its Lifecycle card. “Travel policy” was retired by Priya Shah; replaced by “Expenses and travel”.

Counts only, never names. A channel message carries the counts and the policy’s title, never the names of the people who have not confirmed: those go to the policy’s owner and to group leads through the escalation and reminder notices, and to nobody else. The one name a message may carry is the manager’s who asked or retired the policy. Every event is sent once per policy, after the notice to the people themselves has been queued, and never waits on the channel: a slow or dead endpoint never slows a Request or a retire; a per-person re-ask sends nothing to a channel. Deliveries show on the channel’s row as before (“last delivery … ok (200)”). The same holds for the generic JSON envelope: its policy object is { id, title, due_at, due_label, counts: { required, acked, overdue }, people } (plus days on the deadline and escalation events, cycle: "annual" on a cycle’s request, and superseded_by: { id, title } on a retirement), and the summary’s summary object is { week, pct, policies, overdue, lowest: { id, title, pct }, snapshot_sha256, snapshot_period }.

Scope your channels. Each event carries the policy’s space, so a channel limited to certain spaces hears only about their policies; a channel with no space filter hears about every policy, private spaces included (as it does for Document published today), so give a channel that everyone can read a space filter if any policy lives in a private space. The weekly summary goes only to channels with no space filter. The events appear in the channel’s checklist under a Compliance heading while read confirmations are licensed; a channel saved with one keeps it even if the licence lapses (it then simply never fires).

Attestation records are kept when a person leaves and when a policy is removed, because an auditor samples exactly those cases.

  • What is kept, and why. Each record stores the person’s name and email address as they were at the time, the confirmation time, the version number and id, the SHA-256 of the text, the IP address and, from 1.12.0, the browser’s user agent. Together they let someone show, years later, which words a named person confirmed and when. Since 1.12.0 each record also keeps the confirmation statement as the server showed it at that moment (the policy’s own, or the workspace default under Program settings) and, on a signed confirmation, the name the person typed and how they were identified (see What the reader confirms); earlier records carry no statement text. Since 1.10.1 the record also keeps every notice sent to the person: the date, the kind, the policy, who asked and the channels used, so “show me that Ana was reminded twice” is answered by rows, not by counts.
  • Deleting an account keeps its confirmations and its notices. They move out of the live count and into the Former staff and disabled accounts section of the record, shown from the stored name, and the deletion’s audit entry records how many were retained. Disabling an account does the same without deleting anything.
  • Removing a policy. When a document that has confirmations is purged from the Trash, by hand or by the retention sweep, the text of every version those confirmations cite is copied to an archive first. The document is gone, but the words behind every hash stay verifiable, and the notice log keeps the policy’s title. Since 1.12.0 the retention sweep leaves a trashed document with confirmations on record alone: it waits in the Trash, marked “Retired policy · 42 confirmations on record” (or “Policy · …” for one that was never retired), until an administrator chooses Delete forever. Retire the policy first (see Retiring a policy) if the record should stay visible in the portal rather than in the Trash.
  • Erasure. The erasure path is Settings → Users → Anonymise compliance record… (in the row’s ⋯ menu from 1.19.1), offered on accounts that have attestation records to administrators who can both delete users and run the compliance program (user.delete and compliance.program_manage). The username must be typed to confirm. It replaces the person’s name, email address and IP address on every confirmation, every decision and every notice that names them with “Former staff #id” (the notice queue’s requester name is rewritten with the rest; the queue never stores a recipient’s address, its error entries name people by id), and from 1.14.0 blanks the typed signature and the browser’s user agent on the person’s confirmations too (an unsigned confirmation still reads as unsigned). Counts, dates, versions and hashes stay, so the compliance numbers do not change. From 1.18.0 it also anonymises the person’s whole-handbook confirmation records. It cannot be undone and is written to the audit log as ack.anonymised. The audit log itself is not rewritten: each document.acknowledge entry keeps the name, IP address and browser recorded with that confirmation, and the ack.anonymised entry names the account that was anonymised, so anonymisation alone does not remove the person from the audit log. From 1.14.0 the erasure also rewrites every stored compliance snapshot that named the person and re-hashes it, writing ack.snapshot_rewritten with the hash that no longer applies (see Evidence), and the name of whoever took a manual snapshot, on the Evidence page’s list, becomes the same “Former staff #id”.
  • Legal basis. CompassDocs keeps these records for as long as the database exists; nothing expires them automatically. The workspace owner decides the legal basis for keeping them and for how long, and uses anonymisation when a record must stop naming someone.
  • Upgrading is reversible. The 1.10.0, 1.10.1, 1.11.0, 1.12.0, 1.13.0 and 1.14.0 schema changes keep the previous release working (see Upgrading to 1.14.0, Upgrading to 1.13.0, Upgrading to 1.12.0, Upgrading to 1.11.0, Upgrading to 1.10.1 and Upgrading to 1.10.0). Records whose person or policy was deleted while on 1.10.x are simply not shown by 1.9.x and reappear on re-upgrade.

An auditor asks two things of a record: show me what it held on a date, and prove it has not changed since. From 1.14.0 Compliance › Evidence answers both. The tab appears for anyone holding Export compliance records (compliance.export), the same key as every CSV: the hashes are evidence handles and every download carries personal data. The Compliance section alone does not open it.

A snapshot is the whole programme written once as JSON text and hashed with SHA-256: the settings in effect (the Program settings defaults and, per policy, its edit default, statement, signature, deadline rule, re-confirmation interval and dates, audience mode and rules, owner, reminder cadence and retirement), the totals the Overview’s tiles show, every policy with the version people are asked to confirm and its hash, one cell per policy and person (confirmed, pending, overdue, exempt or removed, with the dates and reasons), every current confirmation with its version, hash, signature, statement and reading evidence, the decision ledger, the notice log folded per person, and the row count and hash of the full confirmation ledger (hashed, not embedded: it grows without bound and the hash gives the same point-in-time proof). Keys are in a fixed order, arrays by id, dates ISO, so the same data serialises to the same bytes. The text is stored exactly as hashed and downloaded exactly as stored, so sha256sum compliance-snapshot-2026-09.json reproduces the hash the workspace shows (also in the response header X-Content-SHA256).

Three kinds: a monthly snapshot is taken automatically on the first hourly tick of each calendar month (UTC), once per month however many app instances run (after the upgrade to 1.14.0 the first one is taken by the first hourly tick, labelled with the current month); a manual one whenever a programme manager asks (Take snapshot now, compliance.program_manage; the toast reads Snapshot stored · sha256 a1b2c3d4e5f6); a retirement snapshot is written for a policy the moment it is retired, scoped to that policy.

The page’s table lists every stored snapshot, newest first: when it was taken and by whom (“automatic” for the monthly one), its kind (Monthly with the month, Manual, Retirement with the policy’s title), the first 12 characters of its SHA-256 with a copy control for the whole hash, Verify and a download, 25 rows a page. Verify asks CompassDocs to recompute the hash over the stored record: Hash matches means the record is as it was taken; Hash does not match the stored record means it changed since, which the audit log explains (an anonymisation rewrites snapshots and records the previous hash, below). The download is the stored record byte for byte; the file name is the month for a monthly snapshot, manual-{id} or retirement-{policy}-{id} otherwise. Until the first snapshot the page reads Monthly snapshots start automatically; take one now for a point-in-time record. On paper the page prints the lead sentence, the anchored hash and the table without its controls.

Audit package (ZIP) downloads everything as one ZIP (GET /api/admin/compliance/audit-package, compliance.export), streamed rather than built in memory:

  • summary.json: a snapshot of the same instant.
  • matrix.csv: one row per policy and person (status, source, dates, deadline, snooze, exemption, removal, the current confirmation’s version, hash, signature, statement, reading evidence, IP and user agent).
  • acknowledgements.csv: the full confirmation ledger (every confirmation ever recorded, superseded rows and removed accounts included), the same columns as the workspace ledger CSV.
  • decisions.csv, waivers.csv (the exemptions), notices.csv (every notice delivered), audit-extract.csv (every read confirmation audit row).
  • policies/{id}/: policy.json (the settings in effect), roster.csv, history.csv (that policy’s ledger), waivers.csv, decisions.csv, and versions/v{N}.md: the exact text of every version a current confirmation or a decision cites, read from the version history or, for a purged policy, from the archived texts. Its hash in the manifest is the content_sha256 the rows carry.
  • snapshots/: every stored snapshot (2026-09.json, manual-12.json, retirement-42-13.json).
  • manifest.json, last: { generated_at, app_version, workspace, files: [{ name, sha256 }], generated_by }.

The verification recipe: unzip, then sha256sum each file against manifest.json (or jq -r '.files[] | "\(.sha256) \(.name)"' manifest.json | sha256sum -c). The manifest’s hash of acknowledgements.csv equals summary.json’s ledger_sha256. The hash printed beside the button on the Evidence page is the latest monthly snapshot’s (the latest of any kind until a month has passed), the same value the scheduled report email and the weekly channel summary carry, so it is anchored in two places outside the database. Each download is audited as ack.audit_export with the size that went out.

One policy’s package: GET /api/documents/{id}/acknowledgements?format=package is the same ZIP scoped to that policy (its rows, its ledger, its decisions and notices, its audit rows, its retirement snapshot and the texts its confirmations cite), so an exporter can hand an auditor one policy’s bundle. It takes the roster permission the record page takes (document.ack_roster_read) plus compliance.export, exactly as the policy’s ledger CSV does, because the file carries IP addresses and user agents; document.ack_require alone is answered 403.

Limitation. A snapshot proves what the record held at that instant; it does not prove the record was complete. Confirmations recorded after a snapshot are in the next one and in the ledger. Erasing a person’s record (Anonymise compliance record…) rewrites the snapshots that named them and re-hashes them: the audit log’s ack.snapshot_rewritten rows carry the hash that no longer applies, so a hash printed before the erasure will not match and the audit entry says why.

API. GET /api/admin/compliance/snapshots (compliance.export) answers { rows: [{ id, taken_at, kind, period, document_id, document_title, taken_by_name, sha256 }] }, the newest 100; POST (compliance.program_manage) takes a manual snapshot and answers { ok, id, sha256 }; GET /api/admin/compliance/snapshots/{id} downloads one, with X-Content-SHA256; ?verify=1 answers { ok, stored, computed }. Every snapshot is audited as ack.snapshot with its id, kind and hash.

Under Compliance › Settings, the Compliance report card (1.14.0): switch Send the compliance report on, choose Weekly (Monday) or Monthly (with the snapshot), and pick the recipients (active accounts, up to 50). The card shows when the report last went out. Save with the page’s Save; a report that is on needs at least one recipient, and a recipient whose account is no longer active is refused with Every recipient must be an active account.

The weekly report goes out on the first hourly tick of each ISO week (Monday, UTC); the monthly one rides with the monthly snapshot and carries its SHA-256. The email is the editable template Compliance programme report (ack_program_report, see email templates): the compliance figures, the people overdue, the period’s escalations, new joiners (accounts created in the period and enrolled in it, by their space or by an onboarding set) and exemptions, the five policies with the lowest compliance and the deadlines falling in the next 14 days, then the latest monthly snapshot’s SHA-256 and a link to Evidence. Nothing is sent without SMTP. The week is claimed only once SMTP is configured and at least one recipient is still an active account with an address, so a list whose accounts were all deactivated since the save sends nothing and the week is not counted as sent. Every send is audited as ack.report_sent; every change to the card as ack.report_settings.

API. GET / PUT /api/admin/compliance/report (compliance.export to read, compliance.program_manage to write): ack_report is { enabled, cadence: "weekly" | "monthly", recipients: [user ids] }; the GET names the recipients and when the report last went out (last_sent); the PUT takes any subset and refuses the first problem with 400 before anything is written. PUT /api/admin/compliance/settings takes the same ack_report object with the same checks (see Settings API).

Compliance in the main navigation (from 0.50; previously under Settings, and old links redirect) is a section of pages under one header and a tab row. Its first page, My policies, is everyone’s own standing. The portal, the place to run the whole read-confirmation programme rather than checking documents one by one, is the next tabs, shown to people with compliance.program_read: Overview (/compliance/overview), Policies (/compliance/policies), People (/compliance/people) and, from 1.14.0, Sets (/compliance/sets); Evidence (/compliance/evidence, 1.14.0) follows for people with compliance.export, Team (/compliance/team, 1.14.0) for a group lead the view opens for or a holder of compliance.team_read, and Settings (/compliance/settings) for people with compliance.program_manage. Opening a portal page without its permission answers the not-found page, as the training pages do. For someone who runs the programme the header’s subtitle says what it is for: Read-confirmation progress across every policy: who has confirmed what, and who still owes a confirmation.

The Overview answers two questions: how the programme stands, and what to do next. Seven tiles sit over the live policies (a paused or retired policy is left out of every number, so unpublishing one for a rewrite never drops the percentage): Policies, Compliance (confirmed over the people asked; exempt people are outside the denominator), Overdue (open rows whose due date and grace have passed with no current confirmation, in red above zero, with the number of people behind it under it: one person can owe several), Due in 7 days (open, unconfirmed rows whose deadline falls inside the next seven days), Pending (everyone asked who has not confirmed, the overdue included), Exempt (exempt rows on the live policies) and Re-confirmation due in 30 days (see Annual re-confirmation and effective dates). Before 1.14.0 the tiles were named at more length (“Policies requiring confirmation”, “Outstanding confirmations”), and “Fully compliant people” is no longer a tile: the People page’s Standing chip and its Show control carry it. A workspace upgraded from 1.12.0 whose rows carry no due date shows 0 overdue until deadlines are set.

Under the queue, for programme managers, the Team leads block: the same block Training’s Overview shows, one picker per group naming who leads it. A lead named here is a lead for Training too (the list is one), gets the scoped team view and the weekly digest. While Group leads see policy standing is off the block says so, with a link to Program settings. A compliance manager needs neither the Training section nor its licence for it.

Between the tiles and the Team leads block, Needs attention (1.14.0) is the queue a compliance manager works to zero. Each row is a person, the policy and the reason, with the actions that close it; the header counts what is not on the page, showing 12 of 19 · 7 snoozed (the list is cut at 200 rows; a snoozed row leaves the list and is counted), showing 19 of 19 when nothing is cut, and no count line on an empty queue. The reasons, in the order the queue lists them:

  • Confirmed text drifted 7.8% since v12 and Kept against the suggestion 3 times are policy-level rows and lead the list: edits the editor kept (without asking everyone to re-confirm) have moved the text past the re-ask threshold (the drift is measured against the version people confirmed, so three edits of one paragraph count once), or were kept against the suggestion three times or more since the last ask. The action is Ask everyone to re-confirm…, the same question as on the Policies row; once everyone is asked, the row leaves.
  • Overdue · 29 Oct 2026: past the due date and the grace with no current confirmation.
  • Escalated: the escalation notice has gone out and the person is still unconfirmed.
  • Never opened: asked over 14 days ago and the person has not opened the policy since being asked (a view from before the ask, of an earlier version, does not count).
  • Opened, not confirmed: opened, still unconfirmed 14 days after the ask.
  • Standing exemption · review: an exemption with no end date, recorded over a year ago, shown with its reason so someone decides whether it still applies. The action is Remove exemption, which asks the person again with a fresh due date (Undo exempts them again with the same reason).

The policy-level rows come first, then the people: overdue by due date, then escalated, never opened and opened-not-confirmed by how long they have waited, then the standing exemptions by age. The person’s name opens their compliance record; the policy’s title opens its Record tab. The row actions (compliance managers only): Remind (Send a reminder now) queues a reminder for that person on that policy through the bulk endpoint’s remind_people and toasts Reminder queued for Ana Ruiz. (if that policy reminded the person within the hour nothing is sent, and the toast reads Nothing changed. with the policy and the reason); Extend 7 days pushes that person’s due date out a week and clears their escalation and snooze (Undo puts the date back); Snooze takes the row out of this queue and pauses that person’s automatic reminders for seven days (Undo brings it back); Exempt… records an exemption with a reason and an optional end date, as the roster does (Undo removes it). Snoozing changes nothing else: not the person’s status, the tiles, the policy’s progress, the People page, the rosters, the CSVs or the evidence snapshot, so overdue stays a function of the deadline and the confirmation alone and nobody can snooze the organisation to zero overdue; the card says so while a snooze hides rows. An empty queue reads Nothing needs attention.

In the API, GET /api/admin/compliance carries attention: { rows, total, snoozed }; each row is { assignment_id, user_id, user_name, document_id, title, space_id, reason, detail, due_at, since } with reason one of overdue, escalated, never_opened, opened_not_confirmed, standing_exemption, drifted, kept_n; a policy-level row has assignment_id, user_id and user_name null; detail is the due date for an overdue row, the exemption’s reason for a standing exemption, and the sentence above for the two policy-level rows; since is when the reason began.

Above the rows, from 1.14.0, Sort orders the list by Most overdue (the default), Lowest compliance, Due soonest (fixed dates first, then rules by their number of days) or Title A-Z; In set (shown once any live policy belongs to a set) narrows the list to one set’s policies; Filter policies narrows it by title, space, owner or set name, with a live count. Paused and retired policies keep their own groups below, unaffected by either. The card’s Export menu (for compliance.export holders) holds Matrix CSV (one row per policy and person: the former “Export CSV”, same route, same file, same audit row), Ledger CSV (every confirmation ever recorded, with IP and user agent), Audit package (ZIP) (see Evidence) and Print…. Every download is on the audit log. For compliance managers each row has a checkbox, and ticking any opens the bulk bar (see Working in bulk).

  • Policy progress: every live policy with a stacked bar (1.14.0): confirmed (the accent colour), pending (grey), overdue (red) and exempt (hatched, beside the others because exempt people are outside the denominator), with the numbers spelled out under it, 312 confirmed · 88 pending · 12 overdue · 4 exempt (pending includes the overdue ones; the percentage beside the bar is still confirmed over required); when it was last saved, when the last reminder went out, a chip naming its edit default (Asks editor, Asks when material or Always re-asks), from 1.14.0 a chip per set the policy belongs to (New starter onboarding), and a Last asked line from the ledger (“Last asked 3 Mar 2026 by Matt: Section 4 now covers contractors”; it names the last time everyone was asked, so a later editorial keep does not replace it). While edits have been kept since that ask, the second meta line ends · 3 edits kept since v12 and the row carries a chip 7.8% drift since v12 (drift is how far the live text now differs from the version people confirmed, in percent of words; the chip shows only while something has drifted, in amber once the drift reaches the threshold at which the Overview’s queue lists the policy), so cumulative editorial fixes do not go unnoticed. The count also includes an image placed by the Claude connector or dropped through an upload link, which keep confirmations and are recorded from source Image or Ticket, so a picture that was the policy is seen here and re-asked with Ask everyone to re-confirm…. On a policy with a re-confirmation interval the row also says when everyone is next asked (Next re-confirmation 1 Jan 2027). From 1.13.0 the row’s meta text is two lines. The first is the audience and the deadline: when the text last changed, +6 enrolled this week when people joined the policy’s audience in the last seven days (a new account enrolled on arrival, someone a rule or a group change admitted, everyone a switch-on enrolled; a person returning to the audience keeps their row and is not counted), and the policy’s deadline rule, Due 14 days after assignment (the policy’s own number of days, else the space’s, the workspace’s or the built-in 14) or Due 29 Oct 2026 for a fixed date; once anyone is past their due date and the grace with no current confirmation the line ends · 12 overdue and, from 1.14.0, · owner Priya Shah (the owner set on the policy’s Settings tab, else the space’s Policy owner, else whoever switched the requirement on). The second is the reminders and the cycle: last reminded 2 days ago · next automatic reminder 29 Oct 2026 (“last reminded” is the later of the manual Remind and the hourly sweep, which stamp the same rows, so it reads “last reminded today” right after Remind; “next automatic reminder” is when the sweep will next remind someone on this policy by its own rule, a snooze pushing a person’s date out; never reminded while nobody has been), then the next re-confirmation date as before. Each live row also carries an icon button, Read confirmation settings, for compliance managers (compliance.program_manage), opening the policy’s Settings tab, where who must confirm, the deadline, the reminders, the re-confirmation interval, the statement and the signature are set; the portal itself sets none of these. Expanding a policy row shows the same roster as the policy’s Record tab: Person with email (from 1.14.0 linking to their compliance record), Role, the Confirmed / Pending chip (from 1.13.0 Overdue, Exempt and Asked again too), when they confirmed, the version, the typed signature, Read time and Reached end and, from 1.13.0, the Due and Exemption columns, with Show (All / Pending / Overdue / Confirmed / Exempt), Filter people with its live count, and a pager past 50 people; for a compliance manager the roster’s checkbox column, row actions and bar over ticked rows are there too (see Acting on a row), and every action and every Undo reloads that roster and the counts above it; for everyone else it is read-only. The All confirmations view and the IP column stay on the Record tab and in the exports. The per-policy CSV downloads from the row.
  • Paused (unpublished) · 3: a collapsed group at the foot of Policy progress, its heading carrying the count, for policies that are unpublished with the requirement still on. Each row carries an Unpublished chip (“Read confirmation is paused while the document is unpublished. Confirmations are kept.”) and its standing, and no action. The group is out of the tiles above and of the People table; publishing the document again moves it back into the live list. See Unpublishing a policy pauses it. Retired policies (1.12.0) sit in a collapsed Retired group below the paused ones, with the date, the frozen count, the reason and the replacement; see Retiring a policy.
  • Request read confirmation: pick any published document and click Request read confirmation. Before anything happens the portal asks Require read confirmation for “Expense policy”? and names the audience at that moment (“Everyone who can see this space (42 people) will be asked to read and confirm it.”). Require and notify switches the requirement on and queues a notice for everyone who can see the document (a dashboard notice and an email with a direct link, delivered within a minute); the toast says how many: “Read confirmation required: 42 people will be notified.” Require quietly switches it on and tells nobody, which is the right choice while you are still assembling a set of policies or when a reminder will follow anyway. Cancel sends nothing and changes nothing. Two Request clicks at the same moment (two managers, or the portal and the popover) queue one notice: only the call that switched the requirement on queues it. The count is the audience when you ask; people who join the space later are enrolled as they arrive (see New joiners and directory groups).
  • Remind: notifies only the people who have not confirmed yet. Because sending mail cannot be undone, it asks first: Remind 88 people? with the policy’s name (“Everyone who has not confirmed “Expense policy” yet gets a notice and, unless they opted out, an email. This cannot be undone.“) and Send reminders. The toast reads “Reminders queued for 88 people.” Reminders are limited to one per policy per hour so nobody gets spammed, and the last-reminded time is stamped on the row; inside the hour the portal says so (“A reminder went out less than an hour ago. Give people a moment.”) instead of sending. The hour is enforced in the same database statement that stamps the reminder, so two Remind clicks inside the same second queue one reminder, not two. From 1.13.0 Remind also stamps the rows it reaches, so the automatic cadence counts from it: the row reads “last reminded today · next automatic reminder 29 Oct 2026” (see Reminders and escalation).
  • Ask everyone to re-confirm…: the deliberate re-ask with a reason, on the policy’s row beside Remind (see Asking everyone to re-confirm).
  • Stop requiring: switches the policy off from the portal, exactly as the shield on the document does. Nothing is deleted: the record keeps every confirmation, the policy leaves the progress list, and the toast “Read confirmation no longer required for “Expense policy”“ carries Undo, which switches it back on quietly (“Read confirmation required again for “Expense policy”. No notice sent.“).
  • Matrix CSV, in the Export menu on the Policy progress card (it was the page header’s until the header became the section’s in 1.12.0, and Export CSV until 1.14.0): the full portal-wide compliance matrix (policy × person with timestamps and the columns above) for auditors, over the published, unretired policies; a paused or retired policy’s record is on its own page and in its per-policy CSV. Every export, the matrix and the per-policy record alike, writes an ack.export entry to the audit log.

Each person’s row shows Required, Confirmed, Overdue (the count, in red above zero, counted with the same grace as the Overdue tile) and, from 1.14.0, Standing: Compliant when everything required is confirmed, 2 overdue when any confirmation is past its date, else 3 outstanding, so you can chase individuals, not just documents. Show narrows the table to All, Outstanding (anyone with a confirmation left), Overdue or Compliant; Filter people matches the name, username or role, with a live count; the table pages past 50 rows. The person’s name opens their compliance record. The page also knows how many policies each person is exempt from (users[].exempt in the API); an exempt policy is in none of the other three counts. For compliance managers each row has a checkbox, and ticking any opens the people bulk bar (see Working in bulk).

Sets groups policies into named, ordered units assigned to people, groups or everyone, and to every new joiner (see Policy sets). Evidence holds the monthly snapshots and the audit package (see Evidence: snapshots and the audit package). Team is a group lead’s view of their members’ standing (see Team leads). Settings, the last tab of the section, is the programme’s defaults (see Program settings), shown to anyone who can run the programme, not to read-only roles.

Four permissions govern the portal (three until 1.14.0), and from 1.10.1 every portal action checks its own one:

Permission What it opens
compliance.program_read The portal’s Overview, Policies, People and (1.14.0) Sets tabs: the KPIs, the needs-attention queue, the policy list, the People view, the sets and the audience count behind the Request question, and (1.14.0) every person’s compliance record. The Compliance entry in the main navigation itself, and My policies, need no permission: they show for anyone with a policy in a space they can see.
compliance.program_manage Request read confirmation, Remind, Stop requiring, Ask everyone to re-confirm… from the portal, Retire policy… and Un-retire (also open to document.ack_require on the space, as is any policy’s Settings tab), Exempt…, Remove exemption, Extend…, Snooze, Unsnooze and Ask to re-confirm… on any policy’s roster (1.13.0; also open to document.ack_require on the policy’s space through the space-scoped endpoint), the program settings page and, from 1.14.0, the queue’s row actions, the Policies and People bulk bars, creating, editing, assigning, deactivating and deleting sets, Take snapshot now, the Compliance report card and the actions on a person’s record.
compliance.export The Export menu on the portal (Matrix CSV, Ledger CSV, Audit package (ZIP)), the per-policy download from a policy row, the two ledger files, the ip and user_agent columns of the per-policy record and the IP column of its All confirmations view and, from 1.14.0, the Evidence tab with its snapshots, Verify and downloads, the one-policy package and reading the report settings.
compliance.team_read Read team policies (1.14.0): the Team tab for every group, setting or not, and the compliance record of anyone in a group (the policies in spaces the holder can read); its Remind needs the holder to lead the member’s group or to hold compliance.program_manage. A lead who holds no permission gets the same view for their own groups and members while Group leads see policy standing is on (see Team leads).

Administrators hold all four, and so does the built-in Compliance manager role. To delegate the portal to an HR or compliance manager who should not need full admin rights, grant that role under Settings → Roles & permissions: the Assignments tab has a Delegate a section shortcut that picks it, and you choose the person or group. A custom role can hold any subset: a role with compliance.program_read alone sees the programme and nothing else; the portal hides the controls it cannot use (Request, Remind, Stop requiring, Retire policy…, Un-retire, the roster’s row actions and its bar over ticked rows, the checkboxes and bulk bars, the queue’s actions, the Sets controls, the Export menu, the per-policy download, the ledger files, the Evidence tab, Settings), and the API answers 403 if they are called anyway.

The per-policy record (View record on the document, or GET /api/documents/{id}/acknowledgements) is governed by the space-scoped document.ack_roster_read instead, which the Approver preset holds: a steward can read and download the record for their own space, with the version, hash and name columns an auditor verifies against. A draft is hidden (404) from anyone who may not read drafts, as the document and the summary are. The recorded IP address and user agent are personal data and are filled in only for a caller who also holds compliance.export; for anyone else the ip and user_agent columns are blank, the All confirmations view has no IP column, and the ledger files answer 403.

Requests, reminders, exports and every re-confirmation decision are recorded in the audit log, grouped under the ack category: ack.requested, ack.reminder_sent and ack.reset_notified (written when the notices have gone out, with the real counts), ack.notice_failed, ack.reconfirmation_requested, ack.edit_default_set, ack.export (every CSV download: the format, roster, matrix or ledger, the document or all, the row count and whether the IP column was filled), ack.confirmations_reset, ack.confirmations_kept, ack.anonymised, ack.recert (“Asked for annual re-confirmation”: from 1.12.0 written twice per cycle, once by the sweep with the version, reset, eligible and recert_months, and once by the delivery with its counts), ack.settings_changed (a policy’s own settings: whether the statement changed, the signature setting, the interval and dates, the replacement), ack.retired (“Retired policy”: the reason, the replacement, the frozen counts, the version and the notices cancelled), ack.unretired (“Resumed retired policy”: the standing after the switch), and from 1.13.0 one entry per person for the roster’s levers: ack.exempted (the reason and the end date), ack.exemption_revoked (by a manager, or by the sweep when the end date passed), ack.due_extended (from, to, the days and the reason), ack.due_extension_undone, ack.snoozed (until) and ack.unsnoozed, plus one ack.bulk_extended summary (the count and the days) when several deadlines are extended at once; the deadline keys ack.due_set (“Set read confirmation deadline”: a policy’s rule saved from its Settings tab, with due_days or due_at and moved_existing), ack.due_moved (“Moved read confirmation deadlines”: {policy, n, from_rule, to_rule}, a rule being {due_days, due_at}) and ack.defaults_set (“Changed policy defaults”: {scope: "workspace" | "space" | "policy", space_id, key, from, to}, one row per key that changed, against the setting key, the space or the policy; policy is the Settings tab’s reminder numbers and owner, its deadline rule being ack.due_set); the audience keys ack.audience_set (“Set who must confirm a policy”: the mode, the rules and how many rows the save added and closed), ack.auto_enrolled (“Enrolled new account in policies”: the account and its policies) and ack.audience_reconciled (“Updated a policy’s audience”: added, closed and reopened, written by the system only when a row moved); and the hourly sweep’s ack.reminder_auto (“Sent automatic read confirmation reminders”) and ack.escalated (see What is recorded). Since 1.13.0 ack.reconfirmation_requested carries scope (all or users), user_ids and skipped, so a per-person ask is told apart from a whole-audience one, and its label is “Asked to re-confirm”, covering both. From 1.14.0: the evidence keys ack.snapshot (“Took read confirmation snapshot”: the id, the kind, the hash and the period or policy), ack.snapshot_failed, ack.snapshot_rewritten (“Rewrote read confirmation snapshot for an erasure”: the previous hash and the new one), ack.audit_export (“Exported read confirmation evidence”: the format, the bytes, the policies), ack.report_settings and ack.report_sent (the period, the cadence, the recipients, the snapshot hash); ack.record_exported (“Exported a person’s compliance record”: the person, the rows, the viewer’s kind, the hidden count); the sets keys ack.set_created, ack.set_updated, ack.set_deactivated, ack.set_deleted and ack.set_assigned (the set, the people, the policies, the members switched on); ack.reminder_sent written for a lead’s Remind with by: "lead" and the person; and one summary row per bulk action, ack.bulk_remind, ack.bulk_due_set, ack.bulk_owner_set, ack.bulk_reminders_set, ack.bulk_reasked, ack.bulk_added_to_set, ack.bulk_people_reminded, ack.bulk_exempted and ack.bulk_extended (see Working in bulk); ack.anonymised gains snapshots_rewritten. Three older keys stay in the document category for compatibility: document.acknowledge (a reader confirmed; the entry records the version id, number and hash), document.ack_required (the requirement was switched on, from the document or the portal, with notify and the audience count) and document.ack_removed (switched off, with the standing at that moment). Two scheduler entries carry what happened to confirmations: document.scheduled_publish records ack_policy (reset, keep, or null when there was nothing to decide) and confirmations_reset, and document.scheduled_unpublish records ack_paused: true on a policy. The same { ack_policy, confirmations_reset } rides on document.branch_merge, document.restore_version and change_request.approve, in the same vocabulary: ack_policy is keep or reset for what was applied, or null when the merge, restore or approval decided nothing (a change that was not substantive, a draft, a document that asks nobody), so a recommendation an approval did not act on is never audited as applied. The API, connector and import decisions are echoed as the same ack.confirmations_kept / ack.confirmations_reset, with source api / mcp / import and the caller’s IP address (the connector’s rows carried none before 1.11.0); an import run is one document.import row with the file, the counts and whether the overwrite box was ticked. Changes to the program settings are settings.compliance (from 1.13.0 the deadline and reminder numbers are ack.defaults_set instead).

Everyone a policy asks is required until they confirm, and until 1.13.0 the only way to excuse someone was to take them out of the group or the space, which recorded nothing. From 1.13.0 a compliance manager (or a steward with document.ack_require on the policy’s space) has three levers on a person’s row in the roster, on the policy’s record and in the portal’s expanded roster, each with a reason or a date on the record and its own audit entry (the controls themselves, and the bar over ticked rows, are described under Acting on a row).

Exempt. The person is excused from this policy. Exempt… asks for a reason (required, up to 300 characters; “Shown on the compliance record and in exports”) and an optional Until date. An exempt person leaves every denominator at once: the Overview tiles, the policy’s progress, the People view, their own My policies page, the pending lists and the reminder and escalation sweeps all stop counting them the moment the row is stamped. The roster keeps the row with the status Exempt and the exemption in plain text in its Exemption column (“On leave until 1 Dec · Priya Shah · 3 Oct”: the reason, the end date if any, who recorded it and when), so the gap is explained rather than hidden. The ledger is untouched: a confirmation the person gave before stays on record. You cannot exempt yourself: the server answers “Ask another compliance manager to record your exemption.” and writes nothing from that request, so an author who should not be in their own policy’s audience asks a colleague.

Remove exemption returns the person to pending with a fresh deadline: the policy’s own deadline rule if it has one, else the space’s default, else the workspace default, else 14 days (a fixed policy date still in the future wins over every days rule), and their reminders start over. An exemption with an end date ends by itself: the hourly sweep clears it once the date passes and re-arms the deadline the same way, writing ack.exemption_revoked by the system. A standing exemption (no end date) stays until it is removed; from 1.14.0 one that has stood for more than twelve months is listed on the Overview’s Needs attention queue for review, with its reason.

Extend. Extend… pushes one person’s deadline out by 1 to 365 days (7 by default) with an optional reason (“Recorded on the compliance record”). The new date counts from today or from the current deadline, whichever is later, so an overdue person gets the full extension and a person still ahead keeps their lead. Extending clears the escalation and any snooze on the row, and every move is kept on the row (due_history, the last ten), so Undo on the toast (“Extended 12 deadlines by 7 days”) restores exactly the date the extension replaced. A deadline moved by the policy’s own rule (see Moving existing deadlines) is not an extension and is not undone by it.

Snooze pauses automatic reminders for 1 to 30 days (7 from the roster’s Snooze, the date in its toast). This is the one lever that is deliberately invisible everywhere else: snoozing never changes a person’s status, the numbers, the roster, an export or the record, and overdue remains a function of the deadline and the confirmation alone. It only stops the reminder sweep from writing to the person until the date (and, from 1.14.0, hides the row from the Needs attention queue); a per-person re-ask still tells them, because that notice is not a sweep reminder. Unsnooze resumes the reminders.

All of this is reversible, so the roster asks no confirmation: each action answers with a toast (“Exempted Ana Ruiz from “Acceptable Use Policy”“) and an Undo that is simply the inverse call.

The per-policy CSV’s status column reads exempt for an exempt row (an exempt row is in no count, whatever confirmation it holds), and both the per-policy and the portal-wide CSV end with five columns after the six of 1.12.0: exempt_reason, exempt_by, exempt_until, snoozed_until and due_at. See CSV columns. The JSON record (GET /api/documents/{id}/acknowledgements) carries the same on every row: assignment_id (what the endpoints below take), due_at, exempt, exempt_reason, exempt_by_name, exempt_at, exempt_until, snoozed_until and, after a per-person re-ask, asked_again_at (the person’s own cycle start when they were asked again on their own; null otherwise), asked_again_reason and asked_again_by; beside rows, defaults.overdue_grace_days is the policy’s resolved grace.

POST /api/admin/compliance/assignments takes { action, assignment_ids, days?, reason?, until? } with action one of exempt, unexempt, extend, undo_extend, snooze, unsnooze or reask (the per-person re-ask; a reason of at least 10 characters) and 1 to 200 assignment ids (the assignment_id on each row of the policy’s record). until is a date (YYYY-MM-DD) after today; days is 1 to 365 for extend and 1 to 30 for snooze. It needs compliance.program_manage. The space-scoped twin, POST /api/documents/{id}/acknowledgements/actions, takes the same body for a holder of document.ack_require on the policy’s space, and every write it makes is bound to that document: ids from another policy’s roster are never touched and come back in skipped. A retired or switched-off policy takes no lever: the twin answers 400 “This document doesn’t ask for a read confirmation.” and the admin route reports such ids in skipped; a paused (unpublished) policy keeps its rows and takes them, so an exemption can be recorded ahead of its resumption. Both answer { ok, action, changed, skipped } (until on a snooze), 400 with the first problem named, 429 on a reask when every person named was asked inside the last hour, and 402 without the licence. The roster calls the twin for a steward and the admin route for a program manager; Undo is the inverse action over the changed ids.

Every portal action was one policy or one person at a time: thirty Remind clicks every Monday, and no way to put a deadline on ten policies at once or to remind one person about the six things they owe. From 1.14.0 the Policies and People pages have a checkbox column for compliance managers (compliance.program_manage; a read-only role sees no checkboxes and no bar, and the API answers it 403). On Compliance › Policies each live row has a checkbox, and the checkbox at the head of the toolbar, Select all shown, selects what the filter and the sort leave (shift-click selects a range in that order); a bar sits above the table while anything is ticked, and the selection clears on reload and after an action. Up to 200 policies or people go in one action.

{n} policies selected (Compliance › Policies) offers:

  • Remind: the policy’s reminder to everyone still pending on each ticked policy. It asks first, Remind 2,118 people?, naming the policies it will act on and the ones left out (“1 policy is skipped because it was reminded within the hour”; a policy with nobody pending is left out too), and sends only on Send reminders; the toast reads Reminders queued for 2,118 people · 1 policy skipped. Reminders are queued, not sent from the page: the outbox delivers them within a minute or two, exactly as the row’s Remind does, and the same once-per-policy-per-hour rule applies.
  • Set deadline…: the Deadline card’s three choices (inherit, days after assignment, a fixed date) and Also move existing deadlines, ticked by default. A fixed date always moves every open deadline on the ticked policies; a number of days applies to future assignments unless the box is ticked. Undo puts each policy’s previous rule back (and moves the deadlines back when the change moved them).
  • Set owner…: names one account as the owner of every ticked policy, or clears the owner so each falls back to whoever switched the requirement on, then its space’s approvers. The owner must be an active account that can see each policy’s space; a policy whose space the owner cannot see is skipped. Undo restores each policy’s previous owner.
  • Set reminders…: the Reminders card’s four numbers. A blank field is left as each policy has it; tick Clear the blank fields so they inherit again to remove a policy’s own number instead. Undo restores each policy’s previous numbers.
  • Ask everyone to re-confirm…: one question for all of them, Ask people to re-confirm 3 policies? (“Acceptable Use, Expenses, Travel. This resets 118 confirmations and notifies 412 people.”), with the reason (at least 10 characters) and Ask 412 people to re-confirm. Each policy is reset as the row’s own re-ask resets it; a policy asked or reminded within the hour is skipped and the toast says so (“asked again 12 minutes ago”), as is one nobody has confirmed yet. There is no Undo: the notices are on their way.
  • Add to set…: adds the ticked policies to an active set, in the set’s order (see Policy sets). Nobody is assigned by this: assign the set from the Sets page.
  • Export CSV (selected) (for compliance.export holders): the matrix CSV for the ticked policies only.

On Compliance › People each row has a checkbox, Select all shown selects whoever Show and the filter leave, on every page of the table, and {n} people selected offers:

  • Remind about everything outstanding: one notice per person listing every policy they still need to confirm, queued for the outbox (a person on more than three policies gets one bundled notice; fewer, one per policy). A policy that reminded the person within the hour is left out of their notice, someone with nothing outstanding is skipped, and the toast names what was skipped.
  • Ask to re-confirm…: the people’s confirmed policies, all of them, with a reason; their confirmations stay on record and they are asked to read and confirm again.
  • Exempt from…: pick the policies among those the selected people owe, give the reason (shown on the compliance record and in exports) and an optional end date. Undo removes the exemptions again, every row the action changed, however many.
  • Extend due…: days (1 to 365) and an optional reason over the people’s open deadlines. Undo puts the dates back.

After an action on either bar the selection clears and focus returns to Select all shown.

A manager cannot exempt their own row in bulk any more than one at a time. Nothing is sent while you wait: every reminder and every re-ask is queued and delivered within a minute or two by the server, exactly as a single Remind is (see How notices are sent), and the per-person proof is the notice record. Each action writes one summary row to the audit log (ack.bulk_remind, ack.bulk_due_set, ack.bulk_owner_set, ack.bulk_reminders_set, ack.bulk_reasked, ack.bulk_added_to_set, ack.bulk_people_reminded, ack.bulk_exempted and ack.bulk_extended) beside the per-policy and per-person rows the underlying actions write (ack.due_set, ack.defaults_set, ack.reconfirmation_requested, ack.set_updated, ack.exempted, ack.due_extended).

API. POST /api/admin/compliance/bulk (compliance.program_manage) takes { action, doc_ids?, user_ids?, assignment_ids?, params }: remind, set_deadline (due_days or due_at, move_existing), set_owner (owner_id or null), set_reminders (any of remind_before_days, remind_every_days, escalate_after_days, nudge_every_days; null = inherit), reask (reason), add_to_set (set_id) over up to 200 doc_ids; remind_people, reask_people (reason), exempt_people (doc_ids, reason, until), extend_people (days, reason) over up to 200 user_ids, or assignment_ids for the named rows only. It answers { done, queued, skipped: [{ id, title, reason }] } (the shape of /api/documents/bulk), plus previous (the values each policy held, for Undo) on the three reversible policy actions and changed (the assignment ids) on the people actions; 400 names the first problem (an unknown action, a reason under 10 characters, days outside 1 to 365); 402 without the licence. GET /api/admin/compliance/bulk?user_ids=1,2 lists the live policies the named people owe (what Exempt from… offers). GET /api/admin/compliance?format=csv takes &doc_ids=1,2,3 to narrow the matrix to those policies; the ack.export row names them.

Settings, the last tab of the Compliance section, opens Program settings (/compliance/settings): the defaults every policy inherits, on one page with one Save changes button. Anyone who holds compliance.program_manage can open it; a read-only compliance role sees no button and the address answers “not found”. The page has a URL, so it can be linked from a runbook, and it warns before you leave with unsaved changes. Saving says “Program settings saved.”

Each card ships with the feature that reads it, so the page never shows a setting nothing honours. From 1.10.1:

Setting Default Read by
Editing → When the text of a policy changes Ask the editor each time (recommended) The edit default a policy receives when it is switched on while still at the built-in value. The popover on the document shows the same three choices and marks this one “(workspace default)”; a policy can choose differently for itself, and changing the workspace value never rewrites a policy’s own choice.
Editing → Require a reason for editorial fixes too On The editor’s question. A material change always needs a change note; with this on, an editorial fix needs one as well, so a kept edit is never silent on the record. Off, an editorial fix can be saved without a note.
Confirmation statement → Default statement “I confirm that I have read and understood this document.” The card at the end of every policy. One sentence, up to 300 characters; the preview under the field shows the card as readers will see it. A policy can carry its own statement on the Settings tab of its Read confirmation page; the default applies to every policy that does not (see What the reader confirms).
Re-confirmation → Re-confirm every Never The interval a policy receives when it is first set to require read confirmation (its next date is then now plus the interval, and the day becomes its effective date). Never, 6, 12 or 24 months. A policy can choose differently on its Settings tab (any whole number of months from 1 to 60), and changing the workspace value never rewrites a policy’s own interval. See Annual re-confirmation and effective dates.
Deadlines and reminders (1.13.0) Blank: the built-ins (14 days to confirm, 14 for new joiners, 0 grace days, the first reminder 3 days before the due date, every 7 days after, escalation 7 days overdue, no nudges) The seven numbers every policy inherits unless its space or the policy itself says otherwise: Days to confirm, Days for new joiners, Grace days after the due date, First reminder (days before the due date), Days between reminders, Days overdue before escalation, and Days between nudges for a policy with no due date. A blank field keeps the built-in, shown under the field as “Built-in: 14”. A value must be a whole number of days: 1 to 365 for most, 0 to 90 for the grace, 0 to 365 for nudges (0 means never). The deadline numbers are the last layer of the cascade (see Deadlines and defaults); the four reminder numbers and the grace days feed the hourly sweep (see Reminders and escalation). Each change is audited as “Changed policy defaults” (ack.defaults_set) with the key, the old and the new value.
Compliance report (1.14.0) Off The scheduled summary email: Send the compliance report, Weekly (Monday) or Monthly (with the snapshot), and up to 50 recipients among active accounts; the card shows when the report last went out. Saved through the report’s own route inside the page’s one Save (a report that is on needs at least one recipient; a recipient whose account is not active is refused). See Scheduled report. Audited as ack.report_settings.
Team leads → Group leads see policy standing (1.14.0) On for a new workspace; off for one upgraded from 1.13.0 that had saved program settings “Leads of a group see where its members stand on policies in spaces they can read, and get the weekly summary.” The Team tab, the My policies banner, the team view, a lead’s access to a member’s record and the digest’s Policies section (see Team leads). The leads themselves are named on the Overview’s Team leads block. A first save keeps it on, whatever keys the save carries. Audited as settings.compliance.

Every change is written to the audit log as settings.compliance, one entry per setting that changed, with the key and the old and new values; from 1.13.0 the seven deadline and reminder numbers are written as ack.defaults_set (“Changed policy defaults”) instead, with scope: "workspace", the key and the old and new value, so a space’s and the workspace’s defaults read the same way in the log. Saving a value a setting already has writes nothing.

GET /api/admin/compliance/settings returns the settings in effect, with the defaults filled in for anything never saved:

{
"settings": {
"ack_defaults": {
"edit_default": "ask", "keep_reason_required": true, "recert_months": null,
"leads_see_policies": true,
"due_days": null, "new_joiner_days": null, "overdue_grace_days": null,
"remind_before_days": null, "remind_every_days": null, "escalate_after_days": null, "nudge_every_days": null
},
"compliance_statement_default": "I confirm that I have read and understood this document.",
"ack_report": { "enabled": false, "cadence": "weekly", "recipients": [] }
}
}

PUT /api/admin/compliance/settings takes any subset of ack_defaults (edit_default: ask | material | reset; keep_reason_required: boolean; recert_months: null | 6 | 12 | 24; from 1.13.0, the seven deadline and reminder numbers due_days, new_joiner_days, overdue_grace_days, remind_before_days, remind_every_days, escalate_after_days and nudge_every_days, each an integer in range or null for the built-in; the GET answers null for a key never saved; and, from 1.14.0, leads_see_policies, a boolean, which the GET answers as false for a workspace whose saved row predates 1.14.0 and true for one with no saved row; the first save that writes ack_defaults on such a workspace stores it as true, whatever keys the save names) and compliance_statement_default (text). Every key is checked: an unknown key ({"error":"Unknown setting \"cooldown_hours\"."}), a wrong type, an empty statement or one over 300 characters, a recert_months the Select does not offer ({"error":"\"ack_defaults.recert_months\" must be 6, 12 or 24 months, or null for never."}) or a number out of range ({"error":"\"ack_defaults.due_days\" must be a whole number from 1 to 365 days, or null for the built-in (14)."}) answers 400 and nothing is saved. The response carries the settings now in effect and changed, the keys whose value moved (nested keys are dotted: ack_defaults.edit_default). Both need compliance.program_manage (403 otherwise) and the policy_ack licence (402). From 1.14.0 ack_report ({ enabled, cadence, recipients }, a list of user ids) is writable here too, with the same checks as PUT /api/admin/compliance/report (see Scheduled report); a recipients stored in the shape 1.10.1 reserved, { users, groups }, reads as its users.

GET /api/admin/compliance (needs compliance.program_read) returns the KPIs, the policy rows, the People rows, the published documents that do not yet require confirmation (candidates) and can: { manage, export }, what the caller may do. ?format=csv (needs compliance.export) downloads the matrix (from 1.14.0 &doc_ids=1,2,3 narrows it to those policies); ?format=ledger (needs compliance.export) downloads the workspace-wide ledger. ?preview={docId} returns { eligible, pending, required } for one published document: the audience count the Request question shows. Each policy row carries status: published, or draft for a paused policy (unpublished with the requirement on), and, from 1.12.0, ack_recert_months, ack_next_recert_at and ack_effective_at (the policy’s re-confirmation interval, its stored next date and its effective date, YYYY-MM-DD; null when unset) and the retirement: ack_retired_at, ack_retired_by_name, ack_retired_reason, ack_superseded_by, superseded_by_title and retired_rev, when the row is a retired policy (ack_retired_at is null otherwise). A retired row’s required and acked are the counts frozen on its ledger row, not the live audience; retired rows come last; the kpis leave them out and candidates never lists one. kpis and the CSV matrix cover the published, unretired rows only, and kpis gains recert_due_30 (1.12.0): the live policies whose next re-confirmation falls within 30 days, the Overview’s fifth tile. From 1.13.0 every docs row carries enrolled_week, overdue, due_rule ({ due_days, due_at, resolved_days }: the policy’s own rule, both null when it inherits, and the days the cascade settles on) and reminder ({ last_reminded_at, next_reminder_at }, null on a paused or retired row); kpis carries overdue (the sixth tile) and users rows carry overdue; all additive. From 1.14.0 kpis also carries overdue_people, due_7d, pending (the new name of outstanding, which stays), exempt, escalated, snoozed and drifted, and the answer carries attention (see Needs attention); docs rows carry confirmed, pending, overdue, exempt, last_reminded_at, owner_user_id, owner_name, edit_default, kept_since_reset, overdue_grace_days and set_names; users rows carry exempt. All additive; a client built against 1.13.x ignores them. POST on a paused policy answers 404 Document not found or not published.; switch it off from the document’s shield instead. POST on a retired policy, whatever the action, answers 409 This policy is retired. Un-retire it first.

POST /api/admin/compliance (needs compliance.program_manage) takes { docId, action, notify? } (notify, when sent, must be true or false; anything else is refused with 400):

  • action: "request" switches the requirement on. With notify omitted or true, a request notice is queued for everyone who can see the document and delivered within a minute; with notify: false nobody is told and the one-hour Remind limit is not started. Answers { ok, queued, eligible }; 400 if the document already requires confirmation, including when a parallel request switched it on a moment earlier: only the request that switched it on queues the notice.
  • action: "remind" queues a reminder for everyone still pending. Answers { ok, queued }; 429 inside the hour after the last request, reminder or re-ask; the hour is taken atomically, so two clicks in the same second send one reminder and the other is answered 429.
  • action: "stop" switches the requirement off and keeps the record. Answers { ok, acked, required } as they stood at that moment.
  • Any other action is answered 400 Unknown action. and changes nothing. (Before 1.10.1 anything but remind was treated as request.)

404 when the document does not exist or is not published; 402 when read confirmation is not licensed; 403 when the caller lacks the key. queued is how many people were pending when the notice was queued; the delivery resolves the list again a minute later, and the audit entry carries the final counts.

Each is described in its own section; this is the index:

  • GET / POST /api/admin/compliance/sets, PATCH / DELETE / POST /api/admin/compliance/sets/{id}: see Policy sets.
  • GET / POST /api/admin/compliance/snapshots, GET /api/admin/compliance/snapshots/{id} (?verify=1), GET /api/admin/compliance/audit-package, GET /api/documents/{id}/acknowledgements?format=package: see Evidence.
  • GET / PUT /api/admin/compliance/report: see Scheduled report.
  • GET /api/admin/compliance/people/{id} (me; ?format=csv): see A person’s record.
  • GET / POST /api/compliance/team, GET / PUT /api/compliance/leads: see Team leads.
  • POST /api/admin/compliance/bulk, GET /api/admin/compliance/bulk?user_ids= and GET /api/admin/compliance?format=csv&doc_ids=: see Working in bulk.
  • GET /api/compliance/mine gains set_names, set_position and lead_team: see My policies.
  • Nothing to run. No schema change.
  • More people are reminded. People asked to confirm a policy only through a handbook now get requests, reminders and escalations, and count in every figure (see People asked only through a handbook). Expect compliance percentages to move if many of your readers reach policies only through handbooks.
  • Email templates. ack_request and ack_due gain {{handbook_line}} and {{handbook_title}}, blank for people who can open the space. A customised template without them keeps working and leaves the line out.
  • CSV consumers. The per-policy CSV (36 columns), the portal matrix (33) and the person’s record (22) append asked_through at the end. Every other column is where it was.
  • Three tables, one column, and nothing to run. The schema adds compliance_snapshots (one row per snapshot: when, the kind, the period, the policy for a retirement snapshot, who took it, the SHA-256 and the text it was taken over) with its partial unique index on the month, so one instance stores the monthly snapshot; policy_sets and policy_set_docs (a set and its ordered policies) with an index on the document; and one nullable column on policy_audience_rules, set_id (which set wrote the rule; ON DELETE SET NULL) with a partial index under it, idx_policy_rules_set, so deleting a set never scans the rules. Every line is a catalog-guarded CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS or guarded index: no one-shot migration, nothing backfilled, no count moves, and a second boot moves nothing. The first monthly snapshot is taken by the first hourly tick after the upgrade, labelled with the current month; from then on one is taken by the first tick of each calendar month (UTC). Take a manual one at any time for a point-in-time record.
  • Two settings keys, one of them off until you opt in. ack_report ({ enabled, cadence, recipients }) is off until an administrator turns the report on under Compliance › Settings and names recipients. ack_defaults.leads_see_policies is off for an upgraded workspace that had saved program settings and on for a new install: group leads see nothing new (no Team tab, no banner, no Policies section in their digest, no member’s record) until an administrator turns on Program settings → Team leads → Group leads see policy standing, because widening what every existing lead can see is a permissions change to make on purpose. On a new install a first save keeps it on, whichever keys the save carries.
  • The weekly claim keys. The team digest’s claim key is now team_digest_week (claimPeriod in place of claimTrainingPeriod, kept as an alias for one release); 1.13.0’s training_team_digest_week is read before the claim and written after a won one, so the week of the upgrade is sent once, whichever binary sent it first. The digest is sent by a neutral scheduler, so a workspace licensed for read confirmations alone now gets it. The weekly report, the weekly channel summary and each deadline notice claim their periods under ack_report_week, ack_weekly_hook and ack_deadline_hook_{docId}, rows that appear in the settings table as the sweeps first claim them and that a 1.13.0 binary ignores; the hourly housekeeping deletes an ack_deadline_hook_{docId} row once its policy is purged.
  • One permission. compliance.team_read (Read team policies) is added to the Compliance manager role on boot, as every seeded role is refreshed; a custom role gains it only when granted.
  • Audit log. New keys ack.snapshot_failed, ack.snapshot_rewritten, ack.report_settings, ack.report_sent, ack.record_exported, ack.set_created, ack.set_updated, ack.set_deactivated, ack.set_deleted, ack.set_assigned, ack.bulk_remind, ack.bulk_due_set, ack.bulk_owner_set, ack.bulk_reminders_set, ack.bulk_reasked, ack.bulk_added_to_set, ack.bulk_people_reminded and ack.bulk_exempted; ack.snapshot and ack.audit_export, labelled since 1.10.1, are now written; ack.reminder_sent is also written for a lead’s Remind (by: "lead") and ack.anonymised carries snapshots_rewritten. Existing keys are unchanged; training.leads_set is now labelled “Set team leads” (it was “Set training leads”), since the leads list is shared. See Who can manage the program.
  • Emails and notices. One template is new, Compliance programme report (ack_program_report), and one notice kind on the record, digest. Team digest (leads) (training_team_digest) gains the tag {{policy_summary}} and a neutral default subject, “[{{org_name}}] Your team this week”: an existing training_team_digest override must add {{policy_summary}} to show the new section; without it the override renders exactly as before, and keeps its own subject. Five webhook events are new (policy.requested, policy.deadline_approaching, policy.escalated, policy.weekly_summary, policy.retired); no channel receives them until it subscribes (see Chat channels).
  • CSV consumers. The portal matrix appends status and set at the end (32 columns); the per-policy CSV appends set (35 columns). Every existing column is where it was. Scripts that read the files by column name are unaffected; scripts that count columns need the new totals. The person’s record CSV is new (21 columns).
  • Scripts. The portal’s JSON (GET /api/admin/compliance) gains attention, seven kpis keys and fields on docs and users rows, and GET /api/compliance/mine gains set_names, set_position and lead_team; all additive, and a client built against 1.13.x ignores them. The new routes are indexed under the 1.14.0 routes.
  • Rollback to 1.13.0 is supported. A 1.13.0 binary boots on a 1.14.0 database and ignores the three tables and the column (its unique rule per document, kind, ref and effect still holds, and a rule a set wrote reads as a manager’s). While rolled back: no snapshot is taken and the report does not send, a set assigns nothing new and its auto-assign does not run, the lead view and the digest’s Policies section are closed, and the channel events are not sent. Snapshots taken on 1.14.0 are listed again after the re-upgrade, and sets resume. Three 1.13.0 writes do not carry over: saving a channel under Settings → Notifications removes its compliance events (tick them again after the re-upgrade); saving a policy’s Settings tab unlinks that policy’s rules from their set (people stay assigned; assign the set again to re-link them); and an erasure recorded while rolled back does not rewrite the snapshots taken on 1.14.0 (record it again after the re-upgrade so the stored snapshots are rewritten and re-hashed). Take a database backup before upgrading all the same.
  • Three tables, one migration, and no count moves. The schema adds policy_settings (one row per policy: its audience mode, the new-joiner and steward switches, its owner, its deadline rule and its reminder cadence), policy_audience_rules (the targeted rules) and policy_assignments (one row per person a policy asks: when their cycle started, their due date and its history, their reminder, escalation, snooze and exemption stamps, and when and why the row was closed), with four indexes; one column on spaces, policy_defaults (jsonb, an empty object until a space sets something); and one on ack_outbox, payload (jsonb, nullable: what a sweep hands the delivery beside the recipient list). Every line is a catalog-guarded CREATE TABLE IF NOT EXISTS, an ADD COLUMN IF NOT EXISTS with a constant default or nullable, or a guarded index, so a boot against an up-to-date database takes no lock and rewrites nothing. On the first boot a one-shot migration (flag policy_assignments_migrated in settings, counts logged as [ack] policy audiences materialised: … policy setting row(s) and … assignment(s)) writes one settings row per policy that requires confirmation or was retired, in space mode with new joiners enrolled, and one open assignment row per person who owed it under the old rule, dated from the policy’s confirmation marker. No count moves: every roster, every tile and every confirmation’s currency are the same before and after, nothing is sent, and a second boot moves nothing. The welcome notice is for accounts created from then on; a policy switched on by a 1.12.0 instance during a rolling deploy is brought in line at the next hourly sweep.
  • Nobody starts overdue, and nothing is reminded by itself at once. The migration gives no existing person a due date: dates appear as people are enrolled, asked again or cycled from then on. To date an existing audience, set a rule on the policy’s Settings tab and choose Also move existing deadlines, which dates every open row from its cycle start. The hourly sweep reminds a row only once its due date is within the cadence (3 days before, by default) and escalates only once it is past the due date by the grace and the escalate days (7, by default), so a workspace that dates its policies 14 days out sees the first automatic reminders eleven days later; set the numbers under Compliance › Settings › Deadlines and reminders (see Program settings) before then if the built-ins are not wanted. Every existing space inherits everything, and the workspace’s existing ack_defaults keys (edit_default, keep_reason_required, recert_months) are untouched.
  • Who is asked is now a row, not a query. Every count reads the open assignment rows: the portal’s tiles and rows, the Record tab, the People view, My policies, the sidebar badge, the dashboard row, the summary behind the editor’s question and the notices. In space mode (every existing policy) that is the same set of people as before; a person a rule later names who cannot see the space is not enrolled until access is granted. The Compliance entry in the navigation now shows for people a policy asks, not for anyone with a policy somewhere in scope, so someone outside every audience no longer sees it. The Record tab lists exempt people with their chip and leaves out people who left the audience; their confirmations stay in the All confirmations view and the ledger.
  • Emails and notices. The default Read confirmation request subject now ends {{due_suffix}} (“ by 29 Oct 2026“, blank without a date) and its default body carries {{due_line}} on its own line; both, with {{due_date}} and {{days_left}}, are filled from each recipient’s own row, so only people whose row carries a date see the clause and everyone else gets the 1.12.0 copy. A workspace that overrode the template keeps its own subject and body and can adopt the tags when it next edits them. Four templates are new (Read confirmation due reminder, Read confirmation reminders, bundled, Overdue read confirmations, escalation and Welcome: policies to read), two notification rows (Policy reminders and Compliance escalations, email on by default and ignoring the master switch like read confirmation requests) and two notice kinds on the record (due, escalation); see How notices are sent.
  • The hourly tick. The read-confirmation work of the hourly sweep is now one run, tagged [compliance] in the log: the audience reconcile, expired exemptions, automatic reminders, escalations, then the annual cycles, each its own atomic claim with its own error handling, in place of the 1.12.0 inline cycle call. A crash between a claim and its notice drops that step’s notice (the next cadence step sends the next one), as the training reminders do, so no restart can send a reminder twice.
  • Scripts. POST /api/documents/{id}/ack/reconfirm now accepts user_ids (it answered 400 “Per-person re-asks arrive with audiences.” before) and answers user_ids and skipped beside reset and queued; without user_ids it is the whole-audience ask it was. Two action routes are new, POST /api/admin/compliance/assignments and its space-scoped twin POST /api/documents/{id}/acknowledgements/actions (see the assignments API). PATCH /api/documents/{id}/ack-settings takes the audience, deadline and reminder fields and GET answers them with the resolved defaults and where each comes from; PUT /api/admin/compliance/settings takes the seven numbers under ack_defaults and PATCH /api/admin/spaces/{id} takes policy_defaults (see Settings API and Spaces & permissions). The portal’s JSON (GET /api/admin/compliance) and the record’s JSON rows gain fields; both additive, and a client built against 1.12.0 ignores them. The summary’s last_decision and the portal’s “Last asked” line now speak for the whole audience only (scope all).
  • CSV consumers. Both the per-policy CSV and the portal matrix append five columns at the end (exempt_reason, exempt_by, exempt_until, snoozed_until, due_at): 34 columns on the per-policy file, 30 on the portal-wide file; the per-policy status column takes a third value, exempt. Every existing column is where it was. Scripts that read the files by column name are unaffected; scripts that count columns need the new totals.
  • Audit log. New keys ack.audience_set, ack.auto_enrolled, ack.audience_reconciled, ack.due_set, ack.due_moved, ack.defaults_set, ack.exempted, ack.exemption_revoked, ack.due_extended, ack.due_extension_undone, ack.bulk_extended, ack.snoozed, ack.unsnoozed and ack.reminder_auto; ack.escalated is now written; ack.recert carries rearmed; and ack.reconfirmation_requested gains scope, user_ids and skipped (existing keys unchanged). The seven deadline and reminder numbers under Program settings audit as ack.defaults_set with scope: "workspace" rather than settings.compliance; the other program settings are unchanged. See Who can manage the program.
  • Rollback to 1.12.0 is supported. 1.12.0 ignores the three tables and the two columns, leaves them in place, and counts eligibility the old way (everyone who can see the space). While rolled back: a targeted audience reads as everyone in the space, an exemption recorded on 1.13.0 is not honoured (everyone is counted again), no deadline, reminder or escalation is applied, a per-person re-ask is refused as before, and the queue still delivers rows without a payload. Nothing is lost: the rows wait for the re-upgrade, which honours them again, and the next hourly sweep heals a policy switched on meanwhile. Take a database backup before upgrading all the same.
  • Nothing to migrate, and nothing fires. The upgrade adds columns and three indexes and runs no backfill. Every existing confirmation stays current; existing confirmations read as unsigned, with no statement text and no user agent, and nothing is guessed. No policy has a re-confirmation interval until someone sets one (the workspace default applies only to policies switched on from then), existing policies carry no effective date until someone dates them, and no policy is retired.
  • Schema additions, all metadata-only. On documents: ack_statement (text, null = the workspace default), ack_signature (integer, default 0), ack_recert_months, ack_next_recert_at, ack_effective_at (a date), ack_retired_at, ack_retired_by (references users, cleared when the account goes), ack_retired_by_name, ack_retired_reason and ack_superseded_by (references documents, cleared when the replacement is purged); on acknowledgements: statement_text, signed_name, identity_proof and user_agent; and three partial indexes: idx_documents_ack_recert for the hourly sweep, and idx_documents_ack_retired_by and idx_documents_ack_superseded_by under the two references, each over retired rows only, so removing an account or purging a replacement does not scan documents. Every column is added with a constant default or as nullable and every index is catalog-guarded, so a boot against an up-to-date table takes no lock and rewrites nothing. The Record tab, the ledger views and My policies add no schema of their own: they are served by idx_ack_doc and idx_ack_user from 1.10.0.
  • Rollback to 1.11.2 is supported. 1.11.2 ignores the new columns and the indexes and leaves them in place. While rolled back, a confirmation it records takes the defaults (unsigned, no statement text, no user agent), a policy that requires a signature records unsigned confirmations and its own statement waits unused, a retired policy reads as one whose requirement is off (the retirement columns and its ledger row stay for the re-upgrade; if someone re-requires a retired policy from 1.11.2’s request picker while rolled back, it comes back as retired after the re-upgrade with its requirement reading as on in the record: Un-retire resumes it, and Retire policy… is refused as already retired), no cycle fires (an interval is honoured again on re-upgrade, and a date that passed meanwhile fires at the next hourly tick), and the retention sweep purges trashed documents with confirmations as it did before (the archive still keeps every cited text). Take a database backup before upgrading all the same.
  • CSV consumers. The per-policy CSV and the portal matrix append six columns at the end (statement, signed_name, identity_proof, recert_months, effective_at, next_recert_at); every existing column is where it was. user_agent, reserved since 1.10.1, is filled for confirmations made on 1.12.0 or later, under the same rule as ip. Scripts that read the files by column name are unaffected; scripts that count columns should expect six more at the end of both files. The matrix leaves out retired policies, as the tiles do; a retired policy’s evidence stays in its own per-policy CSV and in the ledger files.
  • The ledger files need compliance.export. GET /api/documents/{id}/acknowledgements?format=ledger, GET /api/admin/compliance?format=ledger and the IP column of the All confirmations view answer 403, or stay hidden, for a role without it, because every ledger row carries the IP address and user agent. A role with document.ack_roster_read alone reads the same rows as JSON (?view=all) with those two fields blank. Administrators and the built-in Compliance manager role already hold the key; a custom role that should download evidence needs it added under Settings → Roles & permissions.
  • Compliance is routed, and everyone with a policy sees it. /compliance is My policies for every signed-in person; the portal’s tiles are at /compliance/overview, its lists at /compliance/policies and /compliance/people, and /compliance/settings is unchanged. Bookmarks to /compliance open My policies, so a runbook that links to the portal should name the page. The sidebar entry now shows for anyone with a policy in a space they can see, not only for portal managers, with a badge counting the policies they have not confirmed; someone with no policy in scope and without compliance.program_read still sees no entry, and the portal pages answer not found to them. The dashboard row counts every pending policy (it stopped at twenty) and opens My policies when more than one is pending. The portal’s JSON (GET /api/admin/compliance) gains kpis.recert_due_30 and three re-confirmation columns on each docs row; both additive, and a client built against 1.11.x ignores them.
  • Refusals scripts may meet. A retired policy cannot be switched back on except by un-retire: PATCH /api/documents/{id}/ack { "required": true } and the portal’s POST /api/admin/compliance (request, remind or stop) answer 409 “This policy is retired. Un-retire it first.”; retiring twice answers 409 “This policy is already retired.” and un-retiring a live policy 409 “This policy is not retired.”. On a policy that requires a typed signature, POST /api/documents/{id}/ack answers 409 when the name or password does not check out and 429 while the sign-in throttle holds (the username-and-address pair’s lock, which the sign-in form shares); nothing is recorded either way. An unknown key sent to PATCH /api/documents/{id}/ack-settings is 400, never ignored.
  • Audit log. New keys ack.retired, ack.unretired and ack.settings_changed; ack.recert is now written twice per cycle (the sweep’s ask and the delivery), and ack.export carries format: "ledger" for the two ledger downloads. The password a local account re-enters to sign is checked against the stored hash and discarded: it reaches no row, no audit entry and no log line. A wrong password on a signed confirmation (the card’s, or a training deck’s e-signature) is written as auth.login_failed with via: "attestation" against the document, and as auth.lockout (minutes: 15) when it engaged the throttle; ack.unretired carries superseded_by_cleared, the replacement the un-retire removed.
  • Integrations that edit the text of a confirmed policy must now decide. Until 1.10.1 a PATCH /api/v1/documents/{id} or a connector update_doc that changed the text or title of a published document people have confirmed went through, and the document’s edit default decided for it (under the recommended default everyone was asked again, silently from the caller’s point of view). From 1.11.0 such a call, and a status: "published" or publish: true that puts a draft live whose text drifted from the confirmed version, is refused until it says ack_policy (keep or reset) with an ack_reason (a reset needs a reason of at least 10 characters; a keep follows Program settings’ “Require a reason for editorial fixes too”): the v1 API answers 428 Precondition Required with the standing and the measured change, the connector returns decision_required with a hint to ask you, and nothing is written; a policy without a usable reason, or any other value of ack_policy (validated whenever it is sent, substantive change or not), answers 400. A metadata-only change, a draft that stays a draft and a document that does not require read confirmation are never refused for want of a decision. A script that edits policies through the API or the connector must send ack_policy and ack_reason on those calls; a script that only reads, creates, or edits ordinary documents is unaffected (see the API reference and the connector). The app’s own merge, restore and change-request approval routes answer the same 428 and 400 on the same condition (see Change request decisions (API)).
  • Tokens never confirm. A personal API token or an OAuth access token may edit a policy and must decide keep or reset when it does; it can never record a read confirmation. POST /api/documents/{id}/ack and POST /api/documents/{id}/ack/reconfirm accept the signed-in session only and answer 401 to any bearer, and the connector has no tool that confirms.
  • The submitter recommends, the approver decides. The schema adds change_requests.ack_policy (default auto) and change_requests.ack_reason. Requests pending at upgrade carry no recommendation: approving one that changes the text of a confirmed policy asks the approver first (428 through the API).
  • Reading evidence starts now. The schema adds acknowledgements.read_seconds (integer, null when unknown) and acknowledgements.reached_end (integer, 0 or 1), and the index idx_views_doc_user on doc_views. No backfill: the evidence starts at upgrade; older rows show a blank read time and no check mark, and the CSV writes blank and no for them. The per-policy CSV gains two trailing columns, read_seconds and reached_end_self_reported, after version_approved_by; the portal-wide CSV is unchanged, and no existing column moved. No new tracking is added: the read time is a sum of the view records the workspace already keeps for analytics, stored only on the attestation record and shown only to people who may read the record, and the reached-end flag is the reader’s own report (see Notes).
  • Unpublished policies reappear as paused. A document that was unpublished before 1.11.0 with the requirement still on was invisible to the portal, the People table and the dashboard; it now shows in the portal’s Paused (unpublished) group with its confirmations intact. Nothing is sent and nothing is reset by the upgrade. Decide for each whether to publish it again or to stop requiring it from the shield.
  • Images through the connector and upload links no longer reset anyone. On 1.10.x a picture placed by add_image or an upload link into a confirmed policy followed the edit default (under the recommended default it asked everyone again). From 1.11.0 it keeps confirmations and is recorded as Image or Ticket on the History tab and on the drift chip; re-ask by hand when the picture matters.
  • Rollback to 1.10.1 is supported. 1.10.1 ignores the new columns and the index and leaves them in place: a request submitted on 1.11.0 with a recommendation is approved there as the document’s edit default decides, and the recommendation stays on the request unused and unrecorded; rows written on 1.10.1 during a rollback carry no read time and no report; paused policies are hidden again, unchanged; images through the connector and upload links follow the document’s edit default again (under the recommended default everyone is asked); and the import checkbox is absent. Take a database backup before upgrading all the same.
  • People are now told when a confirmation lapses. Until 1.10.0 a reset sent nothing; from 1.10.1 everyone whose confirmation lapsed gets the Updated notice (see How notices are sent). If your editors make many small material changes, tell them a person hears about a policy at most once a day unless they confirm in between. Nothing is sent for the past: the notice log starts at the upgrade, and no notice goes out for resets that happened before it.
  • The request email body changed. The default Read confirmation request template now says “read and confirm”, carries the new tags (change_note, reason, due_date, days_left) and ends with “Open the document and confirm at the end that you have read and understood it.” A workspace that overrode the template keeps its own body: edit or reset it to pick up the new sentence and tags. The subject is unchanged.
  • Read-only compliance roles are read-only now. A custom role holding compliance.program_read alone could request, remind and export before 1.10.1 because only the section gate was checked; it now gets 403 on every manage and export action. If such a role is meant to run or export the programme, add compliance.program_manage and/or compliance.export to it under Settings → Roles & permissions; the built-in Compliance manager role already holds all three.
  • Scripts that call the portal API must send one of request, remind or stop; anything else is now refused with 400 rather than treated as a request.
  • The per-policy CSV gains four trailing columns, ip, user_agent, version_published_by and version_approved_by; the portal-wide CSV gains the last two. No existing column moved.
  • Program settings reproduce today’s behaviour until changed. The new page starts with every default set to what 1.10.0 did: editors are asked on every text change, both choices need a change note, and the confirmation statement is the house sentence. Nothing is written to the database until an administrator saves, and a workspace that never opens the page behaves exactly as before. Every existing policy starts with the edit default Ask the editor each time.
  • Schema additions. Two tables, ack_notices (the per-person notice log) and ack_outbox (the delivery queue); five columns on doc_versions (published_by, published_by_name, approved_by, approved_by_name, change_request_id, created once in a guarded block); and documents.ack_edit_default (default ask). A one-shot backfill fills the provenance names for existing versions and records migrated_version_provenance = 1 in settings, beside the two 1.10.0 keys. Closed queue rows (delivered, or given up after three attempts, with their error) are dropped after 30 days by the hourly maintenance; the audit log keeps ack.notice_failed.
  • Boot takes no lock on an up-to-date table. Every schema statement that would lock an existing table before checking whether it has anything to do (adding a column, creating an index, the users ladder trigger, the training history’s foreign keys, the spaces default) now looks the catalog up first. A boot against an up-to-date database takes no exclusive and no share lock on users, documents, doc_versions or any other table, so a rolling restart no longer blocks or deadlocks against a save on another instance. The first boot after the upgrade runs the same DDL as before.
  • Rollback to 1.10.0 is supported. 1.10.0 ignores the new tables and columns: a reset sends no notice (the portal’s Request and Remind send at once, as before), the editor is asked on every text change whatever a policy’s edit default says, and the version columns are left as they are. A notice queued and not yet delivered goes out when 1.10.1 boots again, so a policy you also reminded from the 1.10.0 portal may be noticed twice. Versions published while on 1.10.0 show no publisher or approver after re-upgrade (the backfill runs once), and an account anonymised on 1.10.0 keeps its name in the notice log until it is anonymised again on 1.10.1. A policy’s edit default is honoured again on re-upgrade. Take a database backup before upgrading all the same.
  • Existing confirmations stay current. The upgrade copies each document’s last-saved time into the new confirmation marker inside the same transaction that adds the column, so anything that counted as confirmed on 1.9.4 counts as confirmed on 1.10.0. Nobody is asked again by the upgrade.
  • Older rows show “version inferred”. Confirmations recorded before 1.10.0 carry no hash; their version is worked out from the confirmation time and marked inferred on the record and in both CSVs.
  • The decision ledger starts now. The History tab and the asked_at, asked_by, reason and kept_since_asked columns are empty for anything that happened before the upgrade.
  • What asks people again has changed. Metadata edits, unpublish and republish, scheduling, bulk edits, trash and restore and empty saves no longer reset anyone. Text edits now ask the editor to choose; a text change that reaches a published policy with nobody to ask (an approved change request, a scheduled publish of a drifted draft, the API, MCP, a merge or a restore) still asks everyone again and is recorded on the History tab. If your team relied on “any save resets everyone”, tell your editors about the question and the required change note.
  • CSV consumers. Both exports gain trailing columns (version, version_id, content_sha256, version_source, former_staff, asked_at, asked_by, reason, kept_since_asked), and revision_as_of now holds the confirmation marker date rather than the document’s last save. No existing column moved or was renamed.
  • The request email subject changed. The default subject of the Read confirmation request template is now [{{org_name}}] {{request_label}}: confirm "{{doc_title}}". A workspace that overrode the template keeps its own subject and body; only the default changed. If you want the new wording, reset the template or edit your copy. The default body is unchanged in this release.
  • Two one-shot migrations run on first boot and record ack_marker_schema = 1 and ack_retention_schema = 1 in settings: the version inference for existing rows, and the swap of the attestation table’s foreign keys so that deleting a user or a document no longer deletes their records (added as not valid and then validated, so a large ledger is checked without blocking writes). Support can check those two keys to tell whether a workspace has been through the backfill.
  • Rolling back to 1.9.x is supported. 1.9.x ignores the new columns and tables. Four things to know. Edits made on 1.9.x during a rollback move only the document’s last-saved time, so on re-upgrade confirmations that 1.9.x showed as stale become current again until the next text change. Records whose person or policy was deleted while on 1.10.0 are not shown by 1.9.x (its queries join the users and documents tables) and reappear on re-upgrade. The one-shot version inference does not run again, so documents and confirmations created on 1.9.x carry no version id after the re-upgrade (they show “version inferred” with no number; a document’s marker catches up at its next text change). And accounts deleted on 1.9.x leave their records without a name or email snapshot. Take a database backup before upgrading all the same.
  • Pairs with space subscriptions: subscribers get their “document updated” email whenever a policy changes, while the re-confirmation notice goes to everyone whose confirmation lapsed, subscriber or not, and lands on the card at the end.
  • A confirmation has two tiers. A plain confirmation is a record of reading: the person, the moment, the version and its hash, the statement they agreed to and the reading evidence. A signed confirmation (a policy with Require typed signature on) adds an identity assertion: the name they typed and how they were identified, a re-entered password for a local account or the sign-in session for an SSO account. Neither is proof of comprehension. Each confirmation records who, when, from which IP address, which version and the hash of its text, and, from 1.11.0, two pieces of reading evidence. Read time is how long the document was open on screen for that person since everyone was last asked, summed on the server from the browser’s view heartbeats, each bounded by the time the page was actually open, at the moment the confirmation is posted; the confirmation itself carries no time. Reached end is what the reader’s own browser reported: that the end of the text had been on screen, or that focus had entered the confirmation card, when they confirmed. The column is headed “self-reported” because it is. A short read time is not evidence that someone did not read the document (they may have read it printed, or in an earlier version), and a long one is not evidence that they understood it. That is what most compliance frameworks ask for: a verifiable record of what was shown and confirmed, with the evidence labelled for what it is.
  • Privacy: the reading evidence adds no new tracking. The read time is a sum of the view records the workspace already keeps for analytics (the browser’s 15-second heartbeats, each view capped at 30 minutes and bounded by the time the page was actually open), copied onto the attestation record because view records are not retained with the document; it is stored only there and shown only to people who may read the record (document.ack_roster_read). The reached-end flag is the reader’s own report. Neither gates anything: the button on the card is as available as it was.