Delegation Policy
The delegation policy category governs multi-agent trust relationships -- who can delegate to whom, how deep delegation chains can go, and how many agents can run concurrently.
Use it when your system uses orchestrator-worker patterns and you need to enforce which agents are trusted to act as sub-agents, cap recursion depth, and control concurrency.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
max_delegation_depth | integer | 3 | Maximum nesting depth of the delegation chain. A parent delegating to a child is depth 1; that child delegating further is depth 2. |
allowed_delegates | string[] | [] | Allowlist of agent names that may act as delegates. Empty list means no restriction. |
blocked_delegates | string[] | [] | Blocklist of agent names that are never permitted as delegates. Takes priority over allowed_delegates. |
inherit_policies | boolean | true | Whether delegates must inherit the parent's governance policies. Checked in after_workflow; generates WARNs only. |
require_approval | boolean | false | Whether each delegation must be explicitly approved. If true, any delegation without approved: true is blocked. |
max_concurrent_delegates | integer | 5 | Maximum number of simultaneously active delegate agents. |
How It Works
The delegation handler runs at all three phases and reads context data populated by ctx.record_delegation().
Enforcement Phases
| Phase | What it checks | Action on violation |
|---|---|---|
| before_workflow | If the current agent is a delegate: checks delegation depth, blocked list, allowed list | BLOCK |
| mid_execution | Concurrent delegate count; per-delegation depth, blocked list, allowed list, approval status | BLOCK |
| after_workflow | Full delegation tree audit -- flags blocked delegates used, missing policy inheritance | WARN only |
before_workflow — Delegate Self-Check
When an agent starts with is_delegate=True (it was spawned by a parent), the handler validates the agent itself:
- Is
delegation_depthgreater thanmax_delegation_depth? → BLOCK - Is
agent_nameinblocked_delegates? → BLOCK - Is
allowed_delegatesnon-empty andagent_namenot in it? → BLOCK
mid_execution — Delegation Enforcement
Each time the orchestrator calls ctx.record_delegation() and then records a tool call, the handler checks the accumulated delegations list:
- Is
concurrent_delegatesgreater thanmax_concurrent_delegates? → BLOCK - For each delegation: does
depthexceedmax_delegation_depth? → BLOCK - Is the delegate in
blocked_delegates? → BLOCK - Is
allowed_delegatesnon-empty and delegate not in it? → BLOCK - If
require_approvalistrue: does the delegation haveapproved: true? → BLOCK if not
after_workflow — Delegation Audit
After the agent completes, the handler audits the full delegation tree:
- Were any blocked delegates actually used? → WARN
- Did any delegates fail to inherit policies? → WARN
- Records
delegates_used,max_depth,total_delegationsin metadata
After_workflow never blocks. It is an audit-only phase.
Matching Logic
| Scenario | Rule | Match? | Why |
|---|---|---|---|
| Depth 3, max_delegation_depth 3 | depth > max | No | 3 is not > 3 |
| Depth 4, max_delegation_depth 3 | depth > max | Yes → BLOCK | 4 > 3 |
| delegate "summarizer", allowed: ["summarizer"] | name in allowed | ALLOW | Exact match |
| delegate "admin-agent", allowed: ["summarizer"] | name not in allowed | BLOCK | Not in list |
| delegate "admin-agent", blocked: ["admin-agent"] | name in blocked | BLOCK | Exact match |
| delegate "helper", blocked: ["admin-agent"] | name not in blocked | Continue checking | No match |
| 6 concurrent, max 5 | concurrent > max | BLOCK | 6 > 5 |
| 5 concurrent, max 5 | concurrent > max | No | 5 is not > 5 |
An empty allowed_delegates list ([]) means any agent name is allowed. This is the default. Set it to a non-empty list to create a strict allowlist.
If an agent name appears in both blocked_delegates and allowed_delegates, it is blocked. The blocked check runs first.
Example Policies
Strict Orchestrator
Small trusted allowlist, limited depth, approval required:
{
"max_delegation_depth": 2,
"allowed_delegates": ["data-fetcher", "summarizer", "calculator"],
"blocked_delegates": ["admin-agent", "root-agent"],
"inherit_policies": true,
"require_approval": true,
"max_concurrent_delegates": 3
}
Permissive Multi-Agent System
No allowlist, high depth, no approval:
{
"max_delegation_depth": 10,
"allowed_delegates": [],
"blocked_delegates": [],
"inherit_policies": false,
"require_approval": false,
"max_concurrent_delegates": 20
}
Blocklist Only
Block specific dangerous agents, allow everything else:
{
"max_delegation_depth": 5,
"allowed_delegates": [],
"blocked_delegates": ["admin-agent", "root-agent", "privileged-executor"],
"inherit_policies": true,
"require_approval": false,
"max_concurrent_delegates": 10
}
SDK Integration
Recording Delegations
Use ctx.record_delegation() to register each delegation the orchestrator makes. Call it before recording the tool call that triggers mid_execution governance:
import waxell_observe as waxell
from waxell_observe.errors import PolicyViolationError
try:
async with waxell.WaxellContext(
agent_name="orchestrator",
workflow_name="multi-agent-task",
inputs={"query": query},
enforce_policy=True,
) as ctx:
# Record each delegation before simulating child work
ctx.record_delegation(
delegate="data-fetcher",
depth=1,
approved=True,
policies_inherited=True,
)
# Tool call triggers mid_execution governance
ctx.record_tool_call(
name="delegate_task",
tool_type="agent",
input={"delegate": "data-fetcher", "task": "Fetch market data"},
output={"result": "1,240 records retrieved"},
)
# Add a second delegate
ctx.record_delegation(
delegate="summarizer",
depth=1,
approved=True,
policies_inherited=True,
)
ctx.record_tool_call(
name="delegate_task",
tool_type="agent",
input={"delegate": "summarizer", "task": "Summarize findings"},
output={"result": "Summary: tech sector up 18%"},
)
except PolicyViolationError as e:
print(f"Delegation blocked: {e}")
# e.g. "Delegation to blocked agent 'admin-agent'"
# e.g. "Delegation to 'executor' at depth 4 exceeds maximum (3)"
# e.g. "Concurrent delegates (8) exceeds limit (5)"
record_delegation Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
delegate | str | required | Name of the delegate agent |
depth | int | 1 | Delegation depth (nesting level) |
approved | bool | True | Whether delegation was pre-approved |
policies_inherited | bool | True | Whether delegate inherits parent policies |
Each call to record_delegation() appends to context.delegations, updates context.delegation_depth, and increments context.concurrent_delegates.
Enforcement Flow
Agent starts (WaxellContext.__aenter__)
│
└── before_workflow governance runs
│
├── Is this agent a delegate (is_delegate=True)?
│ ├── Yes → check depth, blocked list, allowed list
│ │ └── Violation? → BLOCK
│ └── No → store rules, return ALLOW
│
└── Rules stored for mid_execution use
During execution (after each record_delegation + tool call):
│
└── mid_execution governance runs
│
├── concurrent_delegates > max_concurrent? → BLOCK
│
└── For each delegation in context.delegations:
├── depth > max_delegation_depth? → BLOCK
├── delegate in blocked_delegates? → BLOCK
├── allowed_delegates non-empty and delegate not in list? → BLOCK
└── require_approval and not approved? → BLOCK
Agent exits (WaxellContext.__aexit__):
│
└── after_workflow governance runs
│
├── Any blocked delegates used? → WARN
├── Any delegates without policy inheritance? → WARN
└── Record delegation tree audit (delegates_used, max_depth, total)
Creating via Dashboard
- Navigate to Governance > Policies
- Click New Policy
- Select category Delegation
- Configure
max_delegation_depth,allowed_delegates,blocked_delegates - Set
require_approvalandmax_concurrent_delegatesas needed - Set scope to target the orchestrator agent (e.g.,
delegation-orchestrator) - 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": "Multi-Agent Delegation",
"category": "delegation",
"rules": {
"max_delegation_depth": 3,
"allowed_delegates": ["data-fetcher", "summarizer", "calculator"],
"blocked_delegates": ["admin-agent"],
"inherit_policies": true,
"require_approval": false,
"max_concurrent_delegates": 5
},
"scope": {
"agents": ["orchestrator"]
},
"enabled": true
}'
Observability
Governance Tab
Delegation evaluations appear for each phase:
mid_execution (allowed):
| Field | Example |
|---|---|
| Phase | mid_execution |
| Action | allow |
| Reason | "Delegation within policy (2 delegation(s))" |
mid_execution (blocked):
| Field | Example |
|---|---|
| Phase | mid_execution |
| Action | block |
| Reason | "Delegation to blocked agent 'admin-agent'" |
| Metadata | {"blocked_delegate": "admin-agent"} |
after_workflow (allowed):
| Field | Example |
|---|---|
| Phase | after_workflow |
| Action | allow |
| Reason | "Delegation audit passed (2 delegations, max depth 1)" |
| Metadata | {"delegation_tree": {"delegates_used": ["data-fetcher", "summarizer"], "max_depth": 1, "total_delegations": 2}} |
Common Gotchas
-
Empty
allowed_delegatesmeans no restriction, not "block all". To restrict delegates, add at least one entry to the list. An empty list is the default permissive state. -
blocked_delegatestakes priority overallowed_delegates. If an agent name appears in both lists, it is blocked. Check for accidental overlap when configuring both. -
Depth check uses strict
>comparison. Amax_delegation_depthof 3 allows depth 1, 2, and 3. Depth 4 is blocked. The comparison isdepth > max_delegation_depth, not>=. -
inherit_policiesonly generates WARNs in after_workflow, never blocks. It is an audit signal, not an enforcement gate. To enforce policy inheritance, you need orchestrator-side logic. -
require_approvalchecks theapprovedfield on each delegation record. The orchestrator must explicitly passapproved=Truetoctx.record_delegation(). If the field is missing orFalse, the delegation is blocked whenrequire_approval=True. -
Concurrent delegates count reflects accumulated delegations, not active ones. Each call to
ctx.record_delegation()increments the concurrent count. The count never decreases during a run. It represents the total number of delegations made, not the number running at any instant. -
before_workflow checks the orchestrator itself as a delegate. This phase only does work if
is_delegate=Trueon the context. For top-level orchestrators, before_workflow always returns ALLOW. -
mid_execution runs after every
record_delegation()call followed by a tool call or step. Record the delegation and then immediately record the associated tool call to ensure mid_execution sees the latest delegation state.
Next Steps
- Policy & Governance -- How policy enforcement works
- Compliance Policy -- Validates that required sibling policies are active
- Privacy Policy -- Consent, residency, and purpose governance
- Communication Policy -- Govern output channels
- Policy Categories & Templates -- All 26 categories