Approver Groups
When a policy rule’s require_human_review action matches, Keel-managed
execution is held before the provider or tool call. Adding an
approval_requirement turns that hold into a typed challenge that defines who
may approve or reject it. Approver Groups are the named, governed surface that
determines who can satisfy an approver_group requirement.
This page documents the typed ApprovalRequirement schema, the per-tier capability matrix, the EnterpriseApproverGroup model, the authorization rules that decide whether a given approver can satisfy a given requirement, and the per-approval audit record that anchors the decision in the trust stack.
For the action that produces a challenge in the first place, see require_human_review.
Review without a typed approver
A require_human_review rule may exist without an approval_requirement.
That form still blocks fresh Keel-managed execution; it is not permission to
dispatch. Fresh public Permit responses serialize the result as
decision: "review". Permit-first callers own the downstream call and must not
dispatch until a new response or documented approval transition authorizes it.
Add a typed approval_requirement when the request should enter Keel’s
resolvable approval workflow. The requirement defines the eligible approver,
timeout, approval count, and any separation-of-duties rule.
Typed ApprovalRequirement schema
A policy rule may carry an approval_requirement of the following shape. Only the fields relevant to the chosen type should be set.
{
"type": "<requirement_type>",
"timeout_seconds": 3600,
"role": "<org_role>",
"group_id": "<uuid>",
"group_slug": "<slug>",
"team_id": "<uuid>",
"team_slug": "<slug>",
"user_id": "<uuid>",
"service_principal_id": "<uuid>",
"min_approvals": 1,
"separation_of_duties": false,
"delegated_approval": false,
"external_workflow": {}
}Requirement types
type | Description | Required field(s) | Minimum plan |
|---|---|---|---|
org_role | Any organization member holding the named role (owner, admin, member, or viewer) | role | Enterprise |
user | A specific dashboard user | user_id | Enterprise |
approver_group | A named approver group (organization-scoped or project-scoped) | group_id or group_slug | Enterprise |
team | Any member of a named Keel team | team_id or team_slug | Enterprise |
service_principal | A dedicated API key carrying the approval scope | service_principal_id | Enterprise |
Each type evaluates membership at the moment of approval, not at the moment the policy was authored. If a user leaves the organization, changes role, or leaves a mapped team between the time the challenge was created and the time it is resolved, that user is no longer eligible to satisfy a requirement that depended on it.
Common controls
These fields apply across requirement types.
| Field | Type | Default | Notes |
|---|---|---|---|
timeout_seconds | integer (> 0) | required | The challenge expires after this many seconds. Expired challenges resolve as denied. |
min_approvals | integer (≥ 1) | 1 | Independent approvals required before the permit clears. Values greater than 1 require Enterprise. |
separation_of_duties | boolean | false | The approver must differ from the permit subject. Enterprise-only. |
delegated_approval | boolean | false | Metadata flag for downstream orchestration. Enterprise-only. |
external_workflow | object | null | null | Arbitrary metadata for handoff to an external workflow (for example, a ticket ID). Enterprise-only. |
Legacy require_attestation
Rules that use the require_attestation shape continue to parse and execute unchanged. Internally, they resolve to a single-approver attestation flow. On Starter and Production this is the only way to name the approver, and the attestor must be project_owner; a bare require_human_review rule binds to the project owner in the same way. On Enterprise, prefer a typed approval_requirement.
Plan tier matrix
Approval governance is gated independently from the policy authoring level. Starter and Production may use a single project-owner review gate; every typed approval requirement requires Enterprise.
| Plan | What may be authored on a require_human_review rule |
|---|---|
| Starter | Project-owner review only: a bare require_human_review rule, or require_attestation with attestor: "project_owner". No approval_requirement. |
| Production | The same project-owner review as Starter. No approval_requirement of any type. |
| Enterprise | All requirement types; min_approvals > 1; separation_of_duties; delegated_approval; external_workflow |
Authoring violations are rejected at policy write time with the approval_governance_tier_exceeded error code. The error envelope identifies the offending rule index, the requirement field that exceeded the tier, and the upgrade target plan. This is a domain-specific code with a richer envelope than the generic plan.upgrade_required.
EnterpriseApproverGroup model
Approver groups are the named, governed roster that an approver_group requirement targets. Groups are administered through the dashboard organization API (Enterprise); the dashboard has no approver-group screen.
Scope
- Organization-scoped when the project belongs to an organization. The same group can be referenced by any policy rule in any project under that organization.
- Project-scoped for personal projects (organizations of one). The group is local to that project.
Each scope ships with a reserved default group named Compliance Officers (slug compliance-officers). The default group exists so a freshly provisioned organization or personal project can author Enterprise approval rules without first creating a group from scratch.
Members
Each group can hold:
- Explicit user members — dashboard users named directly
- Service-principal members — API keys with the
approvalscope - Team-mapped members — members of an existing Keel team, through the group-mapping table. Team membership is read when the approval is resolved.
SSO, SAML, SCIM, and WorkOS are not available. The mapping table accepts sso_group and scim_group entries, but approval resolution evaluates only team mappings, and no identity-provider sync job runs, so an SSO or SCIM mapping never makes anyone eligible to approve.
Group administration
Groups are managed through the dashboard organization API; there is no approver-group screen in the dashboard. The available operations are listing groups, creating a group, adding a member, and adding a team mapping; each requires an authenticated dashboard session with organization-level authorization. Renaming, updating, or deleting a group and removing a member or mapping are not available. Group membership is checked at approval resolution time, not at challenge creation time.
Policy-authoring approval is separate from execution approval
Enterprise can gate policy authoring changes behind a separate authenticated approval. This does not enable execution co-signature, which remains contained and unavailable.
For an organization policy scope, authoring dual control is enabled only when the organization has the Enterprise entitlement, has not opted out, and its active default compliance-officers group has at least two explicit members. An empty or incomplete group does not enable this gate: authoring may proceed normally, with a setup warning. Review the group’s setup before relying on dual control.
Use an authenticated dashboard user with organization authorization and the freshness headers described in Authentication. Existing organization members can be added through POST /v1/organizations/{org_id}/members with user_id and role; email invitation delivery is not available. List the reserved default group with GET /v1/dashboard/organizations/{org_id}/approver-groups and add a user using POST /v1/dashboard/organizations/{org_id}/approver-groups/{group_id}/members with principal_type: "user" and that user_id. Once dual control is enabled, further group membership changes can themselves return a pending change requiring independent approval.
Create an inactive policy with POST /v1/policies, the organization org_id, a top-level name, policy_json, and is_active: false. When authoring dual control applies, HTTP202 returns a requested pending change instead of an installed policy. Its list response identifies the change with id. A different eligible member approves the exact change through POST /v1/dashboard/organizations/{org_id}/pending-changes/{change_id}/approve with an optional reason. The requester cannot approve their own change. The current default authoring requirement is one independent approval; the two-member setup condition does not mean two approvals are required.
Read the resulting policy through GET /v1/policies?org_id={org_id} and review its exact rules. The API uses the top-level policy name as canonical policy_json.name, and stores scope in org_id. Creation remains inactive after approval. Request activation separately with PATCH /v1/policies/{policy_id} and is_active: true. When queued, this PATCH response identifies the change with pending_change_id, not id. The policy remains inactive until an eligible independent member approves that activation. Terminal approval replay is refused; it does not create another policy or reactivate a different request.
Ordinary organization membership
Organization membership controls are separate from approver-group membership and require the organization’s Enterprise dashboard entitlement. Use an authenticated dashboard session and the request headers described above. Follow the capabilities returned for your role: an owner can manage other members, while an admin can change or remove only members and viewers. Users cannot change their own role.
The Organization page uses these existing authenticated dashboard operations:
GET /v1/dashboard/organizations/{org_id}/memberslists the current members and permitted controls. For the following member routes, use the membership record’sid, not the user’s UUID.PATCH /v1/dashboard/organizations/{org_id}/members/{member_id}with arolechanges another member’s role. A privileged role escalation under enabled authoring dual control returns HTTP202 and does not change the role before approval. Approve that exact pending change through the independent approval operation above.DELETE /v1/dashboard/organizations/{org_id}/members/{member_id}removes another member and returns HTTP204. Use the leave operation for yourself.POST /v1/dashboard/organizations/{org_id}/leaveuses the leaving user’s own session and returns a status response. You cannot leave or remove the last owner.
Read back the members after a change before relying on the new role or removal. Organization membership and approval eligibility are evaluated at resolution time; a previous role or group entry does not preserve authority after the user loses eligible organization access. Ordinary member removal does not imply a supported approver-group removal API.
An existing Keel user can obtain their account UUID from GET /v1/auth/session using their own authenticated dashboard session: copy the returned user_id and share that identifier with the organization owner. The owner can then use the POST /v1/organizations/{org_id}/members operation described above with that user_id and the intended role. A dashboard membership record’s id is a different identifier used for role changes and removal.
Email invitation delivery is not available. SSO/SAML/SCIM synchronization is not shipped. These controls do not provide an identity-provider synchronization service.
Authorization model
When a permit challenge is resolved — via POST /v1/permits/{permit_id}/attest or the dashboard attest action — Keel evaluates whether the acting principal satisfies the typed requirement. Two principal types can resolve a challenge:
- Dashboard session. Used to satisfy
org_role,user,approver_group, andteamrequirements. The session’s user identity, organization role, and group/team memberships are evaluated against the requirement. - API key with the
approvalscope. Used to satisfyservice_principalrequirements and to act as a service-principal member of anapprover_group. Execution-scope and client-scope keys do not satisfy approval requirements — only keys with the dedicatedapprovalscope.
Two important rules tighten the model:
- Broad write access does not satisfy a typed requirement. A dashboard user with project-write access cannot resolve an
approver_groupchallenge unless they are a member of that group. Authorization evaluates against the requirement type and its target, not against general write permission. - Separation of duties is enforced at resolution. When
separation_of_dutiesis true, the service layer asserts that the approver’s identity differs from the permit subject before recording the approval. A database constraint provides a belt-and-braces guarantee for the same condition.
For dashboard sign-off, the legacy attestor field is treated as the policy requirement label, not as client-supplied proof. The dashboard route resolves the approving or rejecting user from the authenticated dashboard session and authorizes that user against the typed requirement.
Audit record per approval
Every approval or rejection records the following fields:
- the actor’s user ID and email, or the service-principal API key ID
- the authorization basis and subject (for example, “member of group
compliance-officers”) - the rationale and any evidence URL provided
- the request IP address and user agent
- the policy ID and version captured at sign-off time
- the typed approval requirement snapshot
- the governance event with hash-chain sequence number and record hash
Operational approval rows retain identity, rationale, request IP, user agent, and supporting locators under access controls. Portable signed exports use stable identifiers or keyed HMAC commitments for sensitive values; they do not promise the operational email, raw rationale, evidence URL, IP, or user agent in the verifiable chain payload. Verification checks the exported commitments and bindings, not undisclosed operational values.
Sample challenge flow
1. A request matches a policy rule with action: "require_human_review"
and an approval_requirement of type: "approver_group", group_slug:
"compliance-officers", min_approvals: 2, separation_of_duties: true.
2. Keel issues the permit with decision: "review" and
reason_code: "policy.review_required". The permit is held; no
provider call is made.
3. Two distinct members of the "compliance-officers" group, neither of
whom is the permit subject, each call POST /v1/permits/{permit_id}/attest
to record their approval.
4. After the second approval, Keel records the approval audit and
transitions the permit to allow. The application proceeds with its
provider call (permit-first) or Keel dispatches (execution-backed
surfaces).
5. If the timeout_seconds elapses before two approvals are recorded,
the challenge expires as denied and the permit captures the
timeout in its audit record.What this surface does and does not claim
- Approval requirements gate human review on a single permit. They do not gate write operations on the rest of Keel — for example, policy edits or signing-key rotations.
- Group membership is evaluated at approval-resolution time. A user who qualified at challenge creation but no longer qualifies at resolution (for example after leaving the organization or a mapped team) is ineligible.
- Service-principal keys must carry the dedicated
approvalscope to resolve challenges. Execution and client keys cannot. - Separation-of-duties enforcement compares the approver identity to the permit subject. It does not enforce broader role separation across the wider workflow.