Outcome
Replace the complete exact organization/user policy scope sequentially with new ids; storage is separate from dispatch or action approval.
These examples prove local handler behavior with real isolated notification SQL and explicitly simulated identity dependencies.
Before you start
-
Verify pinned identity, the exact action and intended organization/user. Organization policy requires its manager/platform authority; user policy permits self/platform or organization management. Ordinary preference resolution for another user requires platform authority, not organization admin alone.
-
Before a whole-scope replacement capture complete prior semantic settings, including disabled rules and JSON objects. Review intended wildcard dimensions and effects. Empty rows clears that explicit scope; replacements are sequential, can fail partially and always create new rule ids.
-
Ordinary user resolution must omit Notify-only default_channel/transactional. Separate local service-context captures prove real ephemeral Notify signature checks, not a public gateway transport or live service credential. Do not copy service-only inputs into user OAuth calls.
-
Stored policy, candidate channels, resolved delivery mode, actual Notify dispatch/receipt, timers and application action approval are separate outcomes. This family sends no messages and performs no application action.
Discover the target
topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.user_notification_action_policy.replace --detail --json
Discover intended exact organization/user. Read organization_notification_action_policy.list and user_notification_action_policy.list for their separate scopes, plus ordinary global/organization preferences and stored channel flags. Resolve a specific actual producer/event/surface/category/priority context without Notify-only defaults on user OAuth.
Input fields
| Field | Required | Meaning |
|---|---|---|
orgId | Yes | Exact stored organization scope from authorized discovery. Organization rules require its management authority. User action-policy rules permit self/platform or that organization manager; the storage handler does not independently verify organization/user membership or existence. |
userId | Yes | Exact intended Auth user from authorized discovery. User action-policy storage allows self/platform or management of orgId; ordinary preference resolution allows self/platform, not organization manager alone for another user. Neither a known user id nor rule id grants authority. |
rows | Yes | Complete explicit organization or user action-policy scope, up to 500 rows with unique six matching dimensions. Omitted dimensions become *. Empty rows clears that scope; replacement uses sequential writes and new rule ids, and can fail partially. Capture previous semantic settings before approval. |
rows[].source_app_id | No | Source application matching dimension: * wildcard or a 1–128-character discovered canonical id/slug using letters, digits, dot, underscore, colon or hyphen. Row omission defaults to *. Resolver omission matches its * context; keep the actual event producer identifier consistent. |
rows[].event_type | No | Specific two-to-four-segment lowercase event type, or * in a stored policy row. Resolver requires a specific event and refuses *. Stored row omission becomes *. |
rows[].surface | No | Stored notification/action surface or * wildcard; omitted row value becomes *. Resolver uses notification by default and accepts notification/action only. This describes delivery policy, not permission to execute an application action. |
rows[].category | No | Matching event/action category string (same bounded dimension syntax) or *. Stored omission and resolver omission use *. Use the producer contract category rather than invent a label. |
rows[].priority | No | Matching event/action priority string (same bounded dimension syntax) or *. Stored and resolver omission use *. Priority matching does not independently bypass delivery/approval rules. |
rows[].channel | No | Stored matching dimension: email, push, sms, in_app, whatsapp or *. Omission becomes *. A specific rule channel must be in the resolver computed candidate channels; it does not create an address. |
rows[].delivery_mode | Yes | Stored policy hint: immediate, digest, in_app_only or muted. Auth resolution converts muted plus required_action true to in_app_only while preserving the original stored row. Returning digest does not assemble or send a digest. |
rows[].required_action | Yes | Required boolean; when true it prevents matched muted policy from fully suppressing a required action by resolving to in_app_only. This is presentation/delivery policy, not action execution or user approval. |
rows[].quiet_hours | No | Optional nullable JSON object, limited to 8192 serialized characters, stored and returned with its TEXT encoding. Auth does not enforce its schedule. Current Notify source understands start/end clock strings with timezone or explicit defer_until/next_at/ends_at; actual scheduling requires separate Notify/runtime proof. |
rows[].digest | No | Optional nullable JSON object, limited to 8192 serialized characters, retained as metadata. Auth does not assemble a digest or prove later delivery; verify the intended owning Notify workflow separately. |
rows[].escalation | No | Optional nullable JSON object, limited to 8192 serialized characters, stored with its TEXT encoding. Current Notify source reads explicit at/escalates_at or positive after_minutes; Auth resolution alone creates no timer or escalation delivery. |
rows[].enabled | Yes | Required boolean. Disabled rules remain in storage lists but are excluded from effective action-policy lookup. Whole-scope replacement still retains or removes them according to the supplied complete set. |
Effects
-
Sequentially replaces only the explicit selected organization/user rules, keeping organization-level and other-user rules.
-
Generates new rule ids; rows [] clears the user scope and may expose organization/default policy.
Interpret the response
Replaces the whole explicit organization/user scope while preserving organization-level and other-user scopes. Defaults are wildcard dimensions, unique six-dimension keys are required and rows [] clears only this user scope.
Sequential delete/inserts can leave partial rows; repeated replacement generates new rule ids. Capture prior semantic settings, then verify the exact scope and effective resolution separately. Setting required_action does not execute or approve an application action.
Worked examples
These are complete raw target-handler responses, not CLI/gateway envelopes or live authorization proof. Synthetic users/organizations exist only in isolated fixtures. Ordinary examples use simulated user identity while real authority, SQL and resolution are exercised. Discover actual intended targets, save only reviewed input as payload.json, validate and inspect the unchanged plan before an authorized call. --confirm assumes intended approval. Stored rows and computed modes do not prove dispatch, receipt, timers or application action execution.
Self stores required muted policy as muted; the resolver applies its in-app guard separately
Input · payload.json:
{
"orgId": "org_example",
"userId": "usr_self",
"rows": [
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
},
{
"delivery_mode": "immediate",
"required_action": true,
"enabled": false,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
},
"source_app_id": "app_topolo_campaigns"
}
]
}
CLI:
topolo actions validate app_topolo_auth.user_notification_action_policy.replace --data @payload.json --json
topolo actions plan app_topolo_auth.user_notification_action_policy.replace --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_notification_action_policy.replace --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.user_notification_action_policy.replace",
"input": {
"orgId": "org_example",
"userId": "usr_self",
"rows": [
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
},
{
"delivery_mode": "immediate",
"required_action": true,
"enabled": false,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
},
"source_app_id": "app_topolo_campaigns"
}
]
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "User action policy replaced",
"data": {
"orgId": "org_example",
"userId": "usr_self",
"rows": [
{
"rule_id": "nap_P3Vnfea_aScP6RHa",
"organization_id": "org_example",
"user_id": "usr_self",
"source_app_id": "*",
"event_type": "*",
"surface": "*",
"category": "*",
"priority": "*",
"channel": "*",
"delivery_mode": "muted",
"required_action": true,
"quiet_hours_json": "{\"start\":\"22:00\",\"end\":\"07:00\",\"timezone\":\"Europe/London\"}",
"digest_json": null,
"escalation_json": "{\"after_minutes\":30}",
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"digest": null,
"escalation": {
"after_minutes": 30
},
"enabled": true,
"updated_at": 1790972360
},
{
"rule_id": "nap_1aKyfCQx9DWUYk6a",
"organization_id": "org_example",
"user_id": "usr_self",
"source_app_id": "app_topolo_campaigns",
"event_type": "*",
"surface": "*",
"category": "*",
"priority": "*",
"channel": "*",
"delivery_mode": "immediate",
"required_action": true,
"quiet_hours_json": "{\"start\":\"22:00\",\"end\":\"07:00\",\"timezone\":\"Europe/London\"}",
"digest_json": null,
"escalation_json": "{\"after_minutes\":30}",
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"digest": null,
"escalation": {
"after_minutes": 30
},
"enabled": false,
"updated_at": 1790972360
}
]
},
"timestamp": "2026-10-02T00:00:00.000Z"
}
Empty rows clears only the explicit user scope and restores resolution to organization policy
Input · payload.json:
{
"orgId": "org_example",
"userId": "usr_self",
"rows": []
}
CLI:
topolo actions validate app_topolo_auth.user_notification_action_policy.replace --data @payload.json --json
topolo actions plan app_topolo_auth.user_notification_action_policy.replace --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_notification_action_policy.replace --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.user_notification_action_policy.replace",
"input": {
"orgId": "org_example",
"userId": "usr_self",
"rows": []
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "User action policy replaced",
"data": {
"orgId": "org_example",
"userId": "usr_self",
"rows": []
},
"timestamp": "2026-10-02T00:00:00.000Z"
}
Verify the result
-
Read user_notification_action_policy.list with the exact same orgId/userId and compare complete semantic settings rather than prior rule ids.
-
Confirm unrelated scopes remain and resolve the intended event separately. A 500 may follow successful first writes, requiring actual stored read-back before recovery.
Errors and recovery
Foreign action-policy replacement is refused
Captured local refusal · HTTP 401:
{
"credentialPrerequisite": "simulated-user-token",
"input": {
"orgId": "org_foreign",
"userId": "usr_other",
"rows": [
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
}
]
},
"response": {
"success": false,
"error": {
"message": "Access denied",
"code": "AUTH_ERROR",
"details": null,
"timestamp": "2026-10-02T00:00:00.000Z"
}
}
}
Duplicate user matching dimensions are refused before writes
Captured local refusal · HTTP 400:
{
"credentialPrerequisite": "simulated-user-token",
"input": {
"orgId": "org_example",
"userId": "usr_self",
"rows": [
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
},
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
}
]
},
"response": {
"success": false,
"error": {
"message": "Invalid action policy payload",
"code": "VALIDATION_ERROR",
"details": [
"rows[1] duplicates action policy dimensions"
],
"timestamp": "2026-10-02T00:00:00.000Z"
}
}
}
Sequential user replacement can leave only its first rule after failure
Captured local refusal · HTTP 500:
{
"credentialPrerequisite": "simulated-user-token",
"input": {
"orgId": "org_example",
"userId": "usr_self",
"rows": [
{
"delivery_mode": "muted",
"required_action": true,
"enabled": true,
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "Europe/London"
},
"escalation": {
"after_minutes": 30
}
},
{
"delivery_mode": "digest",
"required_action": false,
"enabled": true,
"source_app_id": "app_topolo_campaigns"
}
]
},
"response": {
"success": false,
"error": {
"message": "Failed to replace user notification action policy: Isolated second action-policy insert failure",
"code": "SERVER_ERROR",
"details": null,
"timestamp": "2026-10-02T00:00:00.000Z"
}
}
}
Published recovery guidance
-
state_conflict(HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid. -
AUTH_ERROR(HTTP 401): Verify pinned identity and respect the exact user/organization boundary. A valid other-service signature is not Notify trust; do not switch credentials or infer OAuth expiry from an authenticated target denial. -
VALIDATION_ERROR(HTTP 400): Correct reported fields and unique complete dimension keys. Ordinary user resolution must omit defaults/transactional; only an owning trusted Notify invocation may supply them and must provide explicit transactional true/false. Revalidate and review any replacement. -
SERVER_ERROR(HTTP 500): Preserve request/result and read the exact stored scope. A replacement may have removed prior rows and inserted only part of the intended set; separately review finishing or restoring complete captured semantic settings. Do not rely on old rule ids.
Recovery boundaries
Capture full prior semantic rows before approval. Read actual stored state after any unknown result, then separately review restoring or finishing the complete intended set. Replacement creates new rule ids and may fail partially; it does not restore delivered/queued events, timers or action approvals.
Discovery and evidence
This record belongs to app_topolo_auth. Fixtures prove actual authority, action-policy validation, canonical notification/core SQL, scoring/required-action guard, address/timezone resolution and partial replacement behavior. User tokens/targets/unused addresses are simulated. Trusted Notify captures use real ephemeral local RSA generation, Auth minting and local JWKS verification; no live credential/key is read or serialized. Current Notify source informs optional metadata field meanings, while its scheduling/dispatch is not exercised. No live storage, ownership, gateway credential parity, notification, timer, deployment or production publication is proved.