Skip to main content

Approval Workflows

When a policy rule fires with on_violation: require_approval, the action is held in a pending state and routed to a human reviewer. This page documents the approval queue API, the three approval modes, and the polling pattern your agent uses to wait for a decision.


Action statuses vs rule outcomes

These are two distinct concepts. Conflating them is the most common source of confusion when first using Control.

Action statuses (what POST /v1/actions returns)

Every submitted action resolves to one of three HTTP-level statuses:

StatusMeaning
allowedThe action passed all policies and either executed or is safe to execute. May still include policy_result.warnings if any warn rules fired.
blockedAt least one rule with on_violation: block fired. The action is rejected.
pending_reviewAt least one rule with on_violation: require_approval fired. The action is held in the approval queue. The response includes policy_result.action_id — the agent polls this via nx.action_status(action_id) for the outcome. The same identifier is surfaced as approval_id in the queue API.

Rule outcomes (what on_violation controls)

A rule's on_violation field is a separate concept — it controls what happens when that specific rule fires, not the final action status. There are three possible rule outcomes:

Rule outcomeEffect
blockAction status becomes blocked.
require_approvalAction status becomes pending_review.
warnAction status becomes allowed, but the warning is appended to policy_result.warnings. The action proceeds.

warn is not a fourth action status. A warn rule produces an allowed action with a warning attached. If you're looking for warn in the top-level status field, you'll never find it — check policy_result.warnings instead.

When multiple rules fire, the most severe outcome wins: block > require_approval > warn.

If on_violation is omitted from a rule, Novyx falls back to a default based on severity (CRITICAL→block, HIGH→require_approval, MEDIUM/LOW→warn). See Custom Policies for the full mapping.


The three approval modes

When an action enters pending_review, Novyx routes it through one of three approval modes. The mode is configured per-tenant.

Solo mode

A single user can approve their own actions, but only after a confirmation phrase and a short delay. Designed for individual developers running governed agents in their own workspace.

  • Requires the user to type the confirmation phrase (ROLLBACK for destructive operations)
  • Enforces a 5-second delay before the approval is accepted
  • Available on all tiers

Team mode

Approvals require a different person, OR the same person after a 10-minute cooling-off period. Prevents accidental rubber-stamping while still allowing solo operators in low-risk situations.

  • A different reviewer can approve immediately
  • The submitting user can self-approve only after (now - submitted_at) >= 10 minutes
  • Designed for small teams running shared agents
  • Available on Starter+

Enterprise mode

Configurable multi-person approval chains. Novyx accumulates approval records and only finalizes the decision when the configured min_approvals threshold is met.

  • min_approvals is configurable (default 2)
  • Optional require_manager flag and allowed_roles list
  • Designed for regulated environments with documented approval chains
  • Available on Enterprise

Polling pattern

When your agent submits an action that requires approval, the API returns immediately with status: "pending_review" and an action_id inside policy_result. The agent polls until the action transitions to approved, denied, or executed.

The typed nx.submit_action() helper (Python 3.4.0 / JS 3.2.0) wraps POST /v1/actions against the main cloud governance flow.

import time
from novyx import Novyx

nx = Novyx(api_key="nram_your_key")

# Submit an action that triggers a require_approval rule
result = nx.submit_action(
"slack.send_message",
{
"channel": "#external-customers",
"text": "Hi! Here's the user's email: alice@example.com",
},
agent_id="support-bot",
)

if result["status"] == "pending_review":
action_id = result["policy_result"]["action_id"]
print(f"Action {action_id} pending — polling for decision...")

while True:
status = nx.action_status(action_id)
if status["status"] in ("approved", "executed"):
print("Approved — action executed")
break
if status["status"] == "denied":
print(f"Denied: {status.get('reason', 'no reason given')}")
break
time.sleep(2)
elif result["status"] == "blocked":
print(f"Blocked: {result['message']}")
else:
print("Allowed and executed")

nx.submit_action() vs nx.action_submit() — these are two different methods. submit_action is the Phase-1-5 governance path that hits POST /v1/actions on the main cloud API. action_submit is the legacy strata.action.v0 envelope path that requires a separate Control instance configured via control_url. Most users want submit_action.


List the approval queue

GET /v1/approvals

Returns the current pending queue for the tenant. Each entry is the latest event per approval_id (the same value as the action_id returned at submission), ordered by sequence_number from the audit chain — not by wall-clock timestamp. This is intentional: ordering by sequence_number survives cross-worker clock drift and guarantees a consistent view of the queue regardless of which Fly machine handled which event.

Query parameters

ParameterTypeDefaultDescription
limitnumber50Max results.
status_filterstringFilter by status (e.g., pending_review).

Examples

queue = nx.list_approvals(limit=20)
print(f"{queue['total']} pending")
for a in queue["approvals"]:
print(f" {a['approval_id']}: {a['action']} by {a['agent_id']}")

Response

{
"approvals": [
{
"approval_id": "act-tenant-1775831481-dcf5264e",
"action": "send",
"connector": null,
"agent_id": "support-bot",
"status": "pending_review",
"submitted_at": "2026-04-10T14:30:00Z",
"risk_score": 0.72
}
],
"total": 1
}

The queue entry's approval_id is the same identifier returned as action_id from POST /v1/actions. Pass it directly to the decision endpoint below.


Approve or deny an action

POST /v1/approvals/{approval_id}/decision

Submits a decision for a pending action. The decision is recorded in the cryptographic audit chain and emits an ACTION_PENDING_REVIEW-resolution event so the polling agent can pick up the result.

Path parameters

ParameterTypeDescription
approval_idstringThe approval identifier — same value as the action_id returned at submission. Both names refer to the same identifier; the path uses approval_id for consistency with the queue API.

Request body

FieldTypeRequiredDescription
decisionstringYesapprove or deny.
reasonstringNoOptional explanation logged with the decision.
approver_idstringNoID of the human reviewer. Defaults to the tenant's API key owner.

Examples

result = nx.approve_action(
"act-tenant-1775831481-dcf5264e",
decision="approve",
reason="Reviewed — internal recipient confirmed",
)
print(result["status"]) # "approved"

Response

{
"approval_id": "act-tenant-1775831481-dcf5264e",
"decision": "approve",
"reason": "Reviewed — internal recipient confirmed",
"status": "approved"
}

Errors

StatusCodeCause
400novyx_ram.v1.control.invalid_decisiondecision must be approve or deny.
404novyx_ram.v1.control.approval_not_foundNo approval with that ID, or the action was never in pending_review state.
409novyx_ram.v1.control.approval_already_decidedThe approval has already been decided. Decisions are immutable.

See also