---
title: "Set the workspace message-action policy"
method: PUT
path: "/v1/message-action-policy/tenant"
tags: ["message-action-policy"]
---

# Set the workspace message-action policy

`PUT /v1/message-action-policy/tenant`

## Request

- Base URL: `https://api.patricia.app` (the document declares no server; this is the origin it was published from)
- URL: `https://api.patricia.app/v1/message-action-policy/tenant`
- Auth: HTTP bearer

Replace the emoji allowlist, proactive toggles, and per-channel/hour caps. Admin only. Patricia may react ONLY with an allowlisted emoji. She adds one on her own, and answers in an existing message's thread on her own, unless this workspace turns those toggles off; she only starts a NEW top-level message on her own if this workspace turns that one on. Every cap is the most a channel may receive in an hour, counting both the ones she is asked for and the ones she does on her own. For REACTIONS and REPLIES the unprompted ones stop short of the cap, keeping a slice back so a directed request still has room, and at a cap of 1 the unprompted lane gets none. New top-level messages have no such split: unprompted ones can use the whole cap, so at a cap of 1 an unprompted post can take the only slot. This REPLACES the whole policy, so EVERY POLICY FIELD IS REQUIRED: an omitted field is a 422 naming it, never a silent write of some default. Read the current policy with GET, change what you want, and send the whole thing back — the GET response is accepted as-is, so there is no need to strip the read-only fields it carries. One exception: an echoed stored_policy_unreadable=true is refused. That flag means the stored policy could not be read and the GET showed the fail-closed fallback, so saving it back would record the fallback as a deliberate choice and erase the only signal that anything was wrong — decide what the policy should be and send that.

## Headers

- `X-Tenant-Id` string, nullable

## Request body

- MessageActionPolicyBody: Request body for setting a workspace's message-action policy (admin only, full replace). **EVERY POLICY FIELD IS REQUIRED, AND THAT IS THE WHOLE DESIGN OF THIS CLASS.** It carries no defaults for any field it can STORE, which is what makes it impossible for this body and :class:`MessageActionPolicy` to declare two different defaults for the same field. (The two GET echoes below do default to ``None`` — they are accepted so a GET response round-trips, excluded from every dump, and never stored, so neither is a second declaration of a policy value that could drift.) There is exactly one set of field defaults for the policy in this module — the policy's own — and it answers exactly one question: what a workspace that has NEVER been configured gets. The read path's :data:`_ABSENT_KEY_FALLBACKS` is not a second set: it narrows a STORED block that omits a key, where absence is missing information with nobody left to ask. Absence in a REQUEST is different — the caller is present to be told — so "what if they left it out" is not a defaults question at all; it is a malformed request. It was not always so, and the failure that changed it is worth keeping. Every field used to carry its own default, which meant the same field was declared twice with two independent values, and they drifted: ``proactive_reactions_enabled`` was ``True`` on the policy (shipped 2026-08-03) and ``False`` here, so a ``PUT`` that omitted it silently disabled the capability that had just shipped, answered 200, and published ``default: false`` in the OpenAPI contract as if that were correct. The console had already diagnosed this from the outside and worked around it client-side (``useMessageActionPolicy.ts``'s ``Required<…>`` guard, whose comment called a partial body "silent data loss" until the hole closed); the hole itself was server-side and stayed open. Syncing the two sets of defaults was the obvious repair and is the wrong one: it only changes WHICH silent thing an omission does. With them synced, omitting ``proactive_replies_enabled`` would GRANT unprompted replies — text that cannot be taken back — to a workspace that had turned them off. The same argument, about the allowlist, was already written in this file before this change: defaulting it to empty "would make the same partial body silently disable every reaction instead, trading one surprise for a worse one." So the second set of defaults is deleted rather than corrected. An omitted field is now a 422 naming it, which is what the endpoint description and the owner checklist have always told callers to expect ("send every field"), and what the console already enforces at compile time. Loud beats either flavour of silent.
  - `stored_policy_unreadable` false, nullable
  - `dropped_allowlist_entries` integer, nullable
  - `enabled` boolean, required: Allow message actions in the workspace.
  - `reaction_mode` 'all' | 'custom', required: Choose all to allow any reaction supported by Patricia's chat connection, or custom to allow only the names in reaction_allowlist.
  - `reaction_allowlist` string[], required: The emoji names Patricia may use for reactions when reaction_mode is custom. The list stays saved when reaction_mode is all.
  - `proactive_reactions_enabled` boolean, required: Allow unprompted message reactions.
  - `proactive_replies_enabled` boolean, required: Allow unprompted replies in message threads.
  - `proactive_top_level_posts_enabled` boolean, required: Allow unprompted top-level channel messages.
  - `max_reactions_per_channel_per_hour` integer, required: The hourly reaction limit in each channel.
  - `max_replies_per_channel_per_hour` integer, required: The hourly reply limit in each channel.
  - `max_top_level_posts_per_channel_per_hour` integer, required: The hourly limit on top-level messages in each channel.

## Response `200`

Successful Response

- MessageActionPolicyResponse: The effective message-action policy for a workspace.
  - `enabled` boolean, required
  - `reaction_mode` 'all' | 'custom', required
  - `reaction_allowlist` string[], required
  - `proactive_reactions_enabled` boolean, required
  - `proactive_replies_enabled` boolean, required
  - `proactive_top_level_posts_enabled` boolean, required
  - `max_reactions_per_channel_per_hour` integer, required
  - `max_replies_per_channel_per_hour` integer, required
  - `max_top_level_posts_per_channel_per_hour` integer, required
  - `stored_policy_unreadable` boolean
  - `dropped_allowlist_entries` integer

## Other responses

- `400`: Bad request
- `401`: Unauthorized
- `403`: Forbidden. Inspect response body `code` for the specific reason (for example: PERMISSION_DENIED, EMAIL_NOT_VERIFIED, TENANT_ACCESS_REQUIRED, ROLE_UNRESOLVED)
- `409`: Conflict
- `422`: Validation error

---

[API](https://skmtc.dev/patricia/apis/patricia-api.md) · [All operations](https://skmtc.dev/patricia/apis/patricia-api/llms.txt) · [OpenAPI document](https://skmtc.dev/patricia/apis/patricia-api/revisions/0dfb44495ea1?raw)
