Communication Policy
The communication policy category governs what output channels agents can use to send messages. It controls which channels are allowed or blocked, enforces message count limits per execution, and requires disclaimer text in outbound messages.
Use it when your agents send notifications, emails, Slack messages, or any other form of external communication.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
allowed_channels | string[] | [] | Channels agents are permitted to use. If set, only these channels are allowed. Leave empty to allow all channels not in blocked_channels |
blocked_channels | string[] | [] | Channels explicitly denied (takes precedence over allowed) |
max_messages_per_execution | integer | 50 | Maximum number of messages an agent can send in a single execution |
require_disclaimer | boolean | false | Whether outbound messages must contain the disclaimer text |
disclaimer_text | string | "This message was generated by an AI agent." | Text that must appear in the message body when require_disclaimer is enabled |
action_on_violation | string | "block" | "block" to prevent the communication, "warn" to log and continue |
How Matching Works
Channel Matching -- Exact String Equality
Channel names use exact string matching (case-sensitive). The channel value passed to record_communication() must match exactly what's configured in the policy.
| Blocked Channel | Recorded Channel | Match? | Why |
|---|---|---|---|
sms | sms | Yes | Exact match |
sms | SMS | No | Case-sensitive: "sms" != "SMS" |
social-media | social-media | Yes | Exact match |
social-media | social_media | No | Hyphens vs underscores matter |
slack | slack-dm | No | Not a prefix/substring match -- must be exact |
Unlike blocked commands in the code-execution policy (which use case-insensitive substring matching), communication channels use exact string equality. "Slack" and "slack" are different channels. Use consistent lowercase channel names.
Allowed vs Blocked Channels
When both allowed_channels and blocked_channels are configured:
- Blocked channels are checked first -- if the channel is in the blocklist, it's denied regardless of the allowlist
- If the allowlist is non-empty, only channels in the allowlist are permitted
- If the allowlist is empty, all channels not in the blocklist are allowed
| allowed_channels | blocked_channels | Channel | Result |
|---|---|---|---|
["slack", "email"] | ["sms"] | slack | ALLOW |
["slack", "email"] | ["sms"] | sms | BLOCK (in blocklist) |
["slack", "email"] | ["sms"] | webhook | BLOCK (not in allowlist) |
[] | ["sms"] | webhook | ALLOW (no allowlist restriction) |
[] | ["sms"] | sms | BLOCK (in blocklist) |
Disclaimer Enforcement
When require_disclaimer is enabled, the disclaimer_text must appear as a substring in the message body. This is not an exact match -- the disclaimer can appear anywhere in the body text.
| disclaimer_text | Message body | Match? |
|---|---|---|
"This message was generated by an AI agent." | "Hello! This message was generated by an AI agent." | Yes |
"This message was generated by an AI agent." | "Update deployed. This message was generated by an AI agent." | Yes |
"This message was generated by an AI agent." | "Hello! No disclaimer here." | No |
"This message was generated by an AI agent." | "THIS MESSAGE WAS GENERATED BY AN AI AGENT." | No (case-sensitive) |
The disclaimer check is a substring match, but it is case-sensitive. Ensure agents include the exact text configured in the policy.
Message Count
The max_messages_per_execution limit counts the total number of record_communication() calls within a single agent execution (one WaxellContext). Each call increments the count regardless of channel.
Example Policies
Internal Only
Block external communication channels, allow internal tools:
{
"allowed_channels": ["slack", "internal-api"],
"blocked_channels": ["email", "sms", "social-media"],
"max_messages_per_execution": 20,
"require_disclaimer": false,
"action_on_violation": "block"
}
Governed External Communications
Allow email to company domains with mandatory disclaimers:
{
"allowed_channels": ["slack", "email", "internal-api"],
"blocked_channels": ["sms", "social-media"],
"max_messages_per_execution": 10,
"require_disclaimer": true,
"disclaimer_text": "This message was generated by an AI agent.",
"action_on_violation": "block"
}
SDK Integration
Using the Context Manager
import waxell_observe as waxell
waxell.init()
@waxell.observe(
agent_name="notifier",
enforce_policy=True,
mid_execution_governance=True,
)
async def send_notification(channel: str, message: str):
# Record the communication -- triggers governance check
waxell.communication(
channel=channel,
recipient="#engineering",
body=message,
subject="",
)
return {"sent": True}
Direct Context Usage
from waxell_observe import WaxellContext
from waxell_observe.errors import PolicyViolationError
try:
async with WaxellContext(
agent_name="notifier",
enforce_policy=True,
mid_execution_governance=True,
) as ctx:
ctx.record_communication(
channel="email",
recipient="user@acme.com",
body="Your report is ready. This message was generated by an AI agent.",
subject="Report Ready",
)
except PolicyViolationError as e:
print(f"Blocked: {e}")
Enforcement Flow
Agent calls record_communication(channel="sms", body="...")
│
├── Buffer communication dict
├── Buffer step (comm:sms) + span (kind=io)
├── Emit OTel span (comm:sms)
├── Log: "Communication: sms → +1-555-0100"
│
└── Mid-execution governance check
│
├── Is "sms" in blocked_channels? → YES → BLOCK
├── Is allowed_channels set AND "sms" not in it? → BLOCK
├── Message count > max_messages_per_execution? → BLOCK
└── require_disclaimer AND disclaimer_text not in body? → BLOCK
The policy is evaluated at three points:
- Pre-execution: Before the agent runs (basic checks)
- Mid-execution: On each
record_communication()call (channel, count, disclaimer checks) - Post-execution: After the run completes (final summary)
Creating via Dashboard
- Navigate to Governance > Policies
- Click New Policy
- Select category Communication
- Configure rules (channels, limits, disclaimer)
- Set scope to target specific agents (e.g.,
communication-agent) - Enable
Creating via API
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://acme.waxell.dev/waxell/v1/policies/ \
-d '{
"name": "Communication Governance",
"category": "communication",
"rules": {
"allowed_channels": ["slack", "email", "internal-api"],
"blocked_channels": ["sms", "social-media"],
"max_messages_per_execution": 10,
"require_disclaimer": true,
"disclaimer_text": "This message was generated by an AI agent.",
"action_on_violation": "block"
},
"scope": {
"agents": ["communication-agent"]
},
"enabled": true
}'
Observability
Trace Spans
Each record_communication() creates a span visible in the Trace tab:
| Span name | Kind | Attributes |
|---|---|---|
comm:slack | tool | io.direction: outbound, io.channel: slack |
comm:email | tool | io.direction: outbound, io.channel: email |
comm:sms | tool | io.direction: outbound, io.channel: sms |
Governance Tab
Policy evaluations appear with:
- Policy name: The configured policy name
- Action:
allow,warn, orblock - Category:
communication - Reason: Human-readable explanation (e.g., "Blocked channel 'sms' found")
Logs
Log entries are automatically correlated to the trace:
Communication: slack → #engineering (Deployment completed successfully...)
Communication: sms → +1-555-0100 (Urgent: Server is down...)
Common Gotchas
-
Channel names are case-sensitive exact match.
"Slack"won't match a blocklist entry of"slack". Always use consistent lowercase channel names. -
Disclaimer is a substring match, but case-sensitive. The exact
disclaimer_textstring must appear somewhere in thebody."THIS MESSAGE WAS GENERATED BY AN AI AGENT."won't match"This message was generated by an AI agent.". -
Message count is cumulative. Every
record_communication()call in a single execution counts towardmax_messages_per_execution, regardless of channel. Sending 5 Slack messages and 6 emails = 11 total. -
Blocked channels take precedence over allowed channels. If a channel is in both lists, it's blocked.
-
Empty
allowed_channelsmeans no allowlist restriction. If you leaveallowed_channelsempty but setblocked_channels, only the blocked channels are denied. All others are permitted.
Next Steps
- Policy & Governance -- How policy enforcement works
- Code Execution Policy -- Govern generated code
- Context Management Policy -- Control conversation boundaries
- Policy Categories & Templates -- All 26 categories