Skip to main content

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

RuleTypeDefaultDescription
max_delegation_depthinteger3Maximum nesting depth of the delegation chain. A parent delegating to a child is depth 1; that child delegating further is depth 2.
allowed_delegatesstring[][]Allowlist of agent names that may act as delegates. Empty list means no restriction.
blocked_delegatesstring[][]Blocklist of agent names that are never permitted as delegates. Takes priority over allowed_delegates.
inherit_policiesbooleantrueWhether delegates must inherit the parent's governance policies. Checked in after_workflow; generates WARNs only.
require_approvalbooleanfalseWhether each delegation must be explicitly approved. If true, any delegation without approved: true is blocked.
max_concurrent_delegatesinteger5Maximum 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

PhaseWhat it checksAction on violation
before_workflowIf the current agent is a delegate: checks delegation depth, blocked list, allowed listBLOCK
mid_executionConcurrent delegate count; per-delegation depth, blocked list, allowed list, approval statusBLOCK
after_workflowFull delegation tree audit -- flags blocked delegates used, missing policy inheritanceWARN 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:

  1. Is delegation_depth greater than max_delegation_depth? → BLOCK
  2. Is agent_name in blocked_delegates? → BLOCK
  3. Is allowed_delegates non-empty and agent_name not 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:

  1. Is concurrent_delegates greater than max_concurrent_delegates? → BLOCK
  2. For each delegation: does depth exceed max_delegation_depth? → BLOCK
  3. Is the delegate in blocked_delegates? → BLOCK
  4. Is allowed_delegates non-empty and delegate not in it? → BLOCK
  5. If require_approval is true: does the delegation have approved: 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_delegations in metadata

After_workflow never blocks. It is an audit-only phase.

Matching Logic

ScenarioRuleMatch?Why
Depth 3, max_delegation_depth 3depth > maxNo3 is not > 3
Depth 4, max_delegation_depth 3depth > maxYes → BLOCK4 > 3
delegate "summarizer", allowed: ["summarizer"]name in allowedALLOWExact match
delegate "admin-agent", allowed: ["summarizer"]name not in allowedBLOCKNot in list
delegate "admin-agent", blocked: ["admin-agent"]name in blockedBLOCKExact match
delegate "helper", blocked: ["admin-agent"]name not in blockedContinue checkingNo match
6 concurrent, max 5concurrent > maxBLOCK6 > 5
5 concurrent, max 5concurrent > maxNo5 is not > 5
Empty allowed_delegates means no restriction

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.

blocked_delegates takes priority

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

ParameterTypeDefaultDescription
delegatestrrequiredName of the delegate agent
depthint1Delegation depth (nesting level)
approvedboolTrueWhether delegation was pre-approved
policies_inheritedboolTrueWhether 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

  1. Navigate to Governance > Policies
  2. Click New Policy
  3. Select category Delegation
  4. Configure max_delegation_depth, allowed_delegates, blocked_delegates
  5. Set require_approval and max_concurrent_delegates as needed
  6. Set scope to target the orchestrator agent (e.g., delegation-orchestrator)
  7. 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):

FieldExample
Phasemid_execution
Actionallow
Reason"Delegation within policy (2 delegation(s))"

mid_execution (blocked):

FieldExample
Phasemid_execution
Actionblock
Reason"Delegation to blocked agent 'admin-agent'"
Metadata{"blocked_delegate": "admin-agent"}

after_workflow (allowed):

FieldExample
Phaseafter_workflow
Actionallow
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

  1. Empty allowed_delegates means no restriction, not "block all". To restrict delegates, add at least one entry to the list. An empty list is the default permissive state.

  2. blocked_delegates takes priority over allowed_delegates. If an agent name appears in both lists, it is blocked. Check for accidental overlap when configuring both.

  3. Depth check uses strict > comparison. A max_delegation_depth of 3 allows depth 1, 2, and 3. Depth 4 is blocked. The comparison is depth > max_delegation_depth, not >=.

  4. inherit_policies only 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.

  5. require_approval checks the approved field on each delegation record. The orchestrator must explicitly pass approved=True to ctx.record_delegation(). If the field is missing or False, the delegation is blocked when require_approval=True.

  6. 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.

  7. before_workflow checks the orchestrator itself as a delegate. This phase only does work if is_delegate=True on the context. For top-level orchestrators, before_workflow always returns ALLOW.

  8. 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