Workflows and Delegation
Overview
In production environments, AI agents rarely operate alone. An orchestrator agent may delegate tasks to specialized sub-agents -- a code reviewer hands off a file read to a filesystem agent, which in turn asks a database agent to check schema constraints. Behavry's workflow and delegation system provides governance for these multi-agent interactions:
- Workflow sessions define a bounded execution context with known participants, a permission ceiling, and a maximum delegation depth.
- Delegation tokens propagate scoped permissions down the call chain, ensuring each sub-agent operates within the boundaries set by its delegator.
- Causal tracing records the full decision tree across all participants, linking every audit event to its parent and tracking delegation depth.
Orchestrator Agent
|
|-- X-Workflow-Session (wf_token JWT)
|
v
Sub-Agent A ──X-Delegation-Token (d_token JWT)──> Sub-Agent B
| |
|-- causal_depth: 0 |-- causal_depth: 1
|-- parent_event_id: null |-- parent_event_id: evt_A_123
Every proxy request within a workflow carries the X-Workflow-Session header. When one agent delegates to another, it also carries an X-Delegation-Token header. Both are RS256-signed JWTs verified by the proxy engine before any tool call proceeds.
Workflow Sessions
A workflow session is a time-bounded execution context for a group of agents working toward a shared goal.
Creating a Workflow
First, register a workflow definition with its participants:
curl -X POST /api/v1/workflows \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"name": "Code Review Pipeline",
"description": "Automated PR review with security scanning",
"owner_agent_id": "orchestrator-agent-id",
"max_depth": 3,
"max_participants": 5,
"participants": [
{"agent_id": "orchestrator-agent-id", "role": "orchestrator", "allowed_actions": ["read", "execute"]},
{"agent_id": "code-review-agent-id", "role": "worker", "allowed_actions": ["read"]},
{"agent_id": "security-scan-agent-id", "role": "worker", "allowed_actions": ["read", "execute"]}
]
}'
Starting a Session
Start a session to mint the wf_token JWT:
curl -X POST /api/v1/workflows/{workflow_id}/sessions \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"initiated_by": "orchestrator-agent-id",
"ttl_seconds": 3600,
"permission_ceiling": {
"tools": ["read_file", "search_files", "run_scanner"],
"resources": ["/repo/**"]
}
}'
The response includes:
{
"id": "session-uuid",
"wf_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2026-03-17T15:00:00Z",
"status": "active"
}
The wf_token is an RS256 JWT containing the session ID, workflow ID, participant list, permission ceiling, and max depth. Distribute it to all participating agents.
WorkflowContext Validation
When a proxy request arrives with an X-Workflow-Session header, the engine validates:
- JWT signature and expiry -- using the same RS256 key pair as agent tokens
- Token type -- must be
workflow_session - Participant membership -- the calling agent must be listed in
participant_ids - Session status -- the
WorkflowSessionrow in the database must have statusactive
If any check fails, the request is denied immediately. On success, a WorkflowContext dataclass is populated and made available to the rest of the enforcement pipeline:
| Field | Description |
|---|---|
session_id | UUID of the workflow session |
workflow_id | UUID of the parent workflow |
workflow_name | Human-readable name |
initiated_by | Agent ID that started the session |
participant_ids | List of all registered participant agent IDs |
permission_ceiling | Tools and resources allowed in this session |
max_depth | Maximum delegation depth permitted |
causal_depth | Current depth in the call chain (from X-Causal-Depth header) |
parent_event_id | Audit event ID of the parent call (from X-Parent-Event-Id header) |
delegation_chain | Ordered list of agent IDs in the delegation path (from X-Delegation-Chain header) |
The WorkflowContext is serialized into the OPA input envelope at input.workflow_session and into every audit event written during the request.
Session Lifecycle
| Endpoint | Action |
|---|---|
POST /api/v1/workflows/{id}/sessions | Start a new session (mints wf_token) |
GET /api/v1/workflows/{id}/sessions | List sessions for a workflow |
GET /api/v1/workflows/{id}/sessions/{sid} | Get session details |
POST /api/v1/workflows/{id}/sessions/{sid}/complete | Mark session as completed |
POST /api/v1/workflows/{id}/sessions/{sid}/abort | Abort session immediately |
Delegation Tokens
When an agent within a workflow needs to delegate a task to another participant, it issues a delegation token (d_token). The d_token is a scoped JWT that grants the delegatee a subset of the delegator's permissions.
Issuing a Delegation
curl -X POST /api/v1/delegations \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"workflow_session_id": "session-uuid",
"delegator_agent_id": "orchestrator-agent-id",
"delegatee_agent_id": "code-review-agent-id",
"scope": {
"tools": ["read_file", "search_files"],
"resources": ["/repo/src/**"],
"max_data_volume_mb": 50
},
"reason": "Code review of PR #42",
"ttl_seconds": 1800
}'
The response includes the d_token JWT (shown only once -- store it securely):
{
"id": "delegation-uuid",
"delegation_depth": 1,
"effective_permissions": {
"tools": ["read_file", "search_files"],
"resources": ["/repo/src/**"],
"max_data_volume_mb": 50
},
"d_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"status": "active"
}
Scope Intersection
The delegation service computes effective_permissions as the intersection of the requested scope and the delegator's ceiling:
- Tools: if the ceiling's tool list is non-empty, only tools present in both the ceiling and the request are included. An empty ceiling tool list is permissive (all requested tools pass through).
- Resources: same intersection logic as tools.
- max_data_volume_mb: the minimum of the ceiling and requested values.
This ensures permissions can only narrow as they flow down the delegation chain -- they can never widen.
Ceiling Guard
Before computing the intersection, the service checks whether the requested scope exceeds the delegator's ceiling. If any requested tool or resource falls outside the ceiling, the request is rejected with:
403: SCOPE_EXCEEDS_DELEGATOR: requested permissions exceed delegator's effective permissions
Depth Guard
Each delegation increments the depth counter. If the delegator already holds a d_token (chained delegation), the new delegation's depth is parent_depth + 1. If the delegator operates directly under the workflow session, the depth starts at 1.
If the delegation depth exceeds the session's max_depth, the request is rejected:
403: DEPTH_EXCEEDS_MAX: delegation depth 4 exceeds session max_depth 3
Chained Delegation
When Agent B (already a delegatee) delegates to Agent C, the system resolves Agent B's effective ceiling from its own active delegation record, not from the top-level workflow permission ceiling. This ensures scope can only narrow at each hop:
Workflow ceiling: [read_file, write_file, delete_file]
|
Agent A delegates to Agent B: [read_file, write_file]
|
Agent B delegates to Agent C: [read_file]
Revocation
curl -X POST /api/v1/delegations/{delegation_id}/revoke \
-H "Authorization: Bearer $ADMIN_TOKEN"
Revocation is immediate. Even if the d_token JWT has not expired, the proxy checks the database row status at enforcement time and rejects revoked delegations.
Delegation Enforcement
When the proxy engine receives a request with an X-Delegation-Token header, it calls enforce_delegation_token() to validate:
- JWT decode -- RS256 signature and expiry check
- Delegatee match -- the
delegatee_idclaim must match the calling agent's ID - Session match -- the
workflow_session_idclaim must match theX-Workflow-Sessionheader - Database row -- the
Delegationrow must exist, have statusactive, and not be pastexpires_at
On success, a DelegationContext is returned containing the effective permissions, depth, and delegation ID. The engine sets requester_verified = True, which satisfies the requester identity policy for high-risk and critical-risk actions.
Scope Probe Tracking
When a request carries a valid d_token but the tool being called is outside the delegation's effective_permissions.tools, the engine does not immediately deny the request (OPA handles that). Instead, it increments an in-memory scope probe counter for the (agent_id, session_id) pair.
When the counter reaches the threshold of 3, a DELEGATION_SCOPE_PROBE alert is fired and the counter resets. This detects agents that systematically test the boundaries of their delegated permissions -- a potential indicator of compromised or adversarial behavior.
Request 1: tool=read_file (in scope) -> allowed
Request 2: tool=delete_file (out of scope) -> scope probe count: 1
Request 3: tool=write_file (out of scope) -> scope probe count: 2
Request 4: tool=execute_cmd (out of scope) -> scope probe count: 3 -> DELEGATION_SCOPE_PROBE alert
Causal Tracing
Every audit event written during a workflow session includes four additional fields that enable reconstruction of the full decision tree:
| Field | Type | Description |
|---|---|---|
workflow_session_id | string | The session this event belongs to |
causal_depth | int | How deep in the delegation chain (0 = top-level orchestrator) |
parent_event_id | string or null | The audit event ID that caused this event |
delegation_chain | list[string] | Ordered list of agent IDs from orchestrator to current agent |
These values are propagated via HTTP headers on each proxy request:
| Header | Maps To |
|---|---|
X-Workflow-Session | workflow_session_id (extracted from JWT sub claim) |
X-Causal-Depth | causal_depth |
X-Parent-Event-Id | parent_event_id |
X-Delegation-Chain | delegation_chain (comma-separated agent IDs) |
The causal chain allows you to answer questions like:
- Which agent initiated the action that ultimately led to this file deletion?
- How many hops deep was the delegation when the DLP violation occurred?
- Which agents were in the delegation chain when the policy escalation fired?
Decision Trace API
The decision trace API reconstructs the full event timeline for a workflow session, including per-agent summaries and a causal tree.
Get Trace
GET /api/v1/workflows/{workflow_id}/sessions/{session_id}/trace
Response:
{
"workflow_id": "wf-uuid",
"workflow_name": "Code Review Pipeline",
"session_id": "session-uuid",
"session_status": "completed",
"started_at": "2026-03-17T14:00:00Z",
"completed_at": "2026-03-17T14:32:00Z",
"total_events": 47,
"events": [
{
"event_id": "evt-001",
"timestamp": "2026-03-17T14:00:12Z",
"agent_id": "orchestrator-agent-id",
"agent_name": "Orchestrator",
"tool_name": "read_file",
"action": "read",
"target": "/repo/src/main.py",
"mcp_server": "filesystem",
"policy_result": "allow",
"policy_reason": "",
"causal_depth": 0,
"parent_event_id": null,
"delegation_chain": [],
"requester_id": "admin@example.com",
"latency_ms": 45,
"error": null
}
],
"agent_summary": {
"orchestrator-agent-id": {"allow": 12, "deny": 0, "escalate": 1, "total": 13},
"code-review-agent-id": {"allow": 28, "deny": 2, "escalate": 1, "total": 31},
"security-scan-agent-id": {"allow": 3, "deny": 0, "escalate": 0, "total": 3}
},
"causal_tree": {
"__root__": ["evt-001", "evt-005", "evt-020"],
"evt-001": ["evt-002", "evt-003"],
"evt-005": ["evt-006", "evt-007", "evt-008"]
}
}
The causal_tree maps each event ID to its child events. Root-level events (those without a parent) appear under the __root__ key.
Export Trace
GET /api/v1/workflows/{workflow_id}/sessions/{session_id}/trace/export
Returns the same JSON payload as a downloadable file with Content-Disposition: attachment; filename="trace-{session_id}.json".
List Session Delegations
GET /api/v1/workflows/{workflow_id}/sessions/{session_id}/delegations
Returns all delegation records issued during a session, ordered by creation time.
Dashboard
The dashboard provides three pages for workflow governance:
Workflows Page
Lists all registered workflows with status, participant count, and session count. Click through to see workflow details and session history.
Workflow Detail Page
Shows workflow configuration, participant list, and a table of sessions with their status and event counts.
Workflow Trace Page
Renders the decision trace as an SVG swimlane visualization. Each participant agent gets a vertical lane. Events are plotted chronologically top-to-bottom, with arrows connecting parent events to their children across lanes. Color coding indicates policy decisions:
- Green: allowed
- Red: denied
- Amber: escalated
Click any event node to open an inspection modal with the full audit event details, including the delegation chain, causal depth, and policy reason.
OPA Policies
Two OPA policy modules govern workflow and delegation requests.
Workflow Policy
Package: behavry.workflows.policy
File: policies/workflows/workflow_policy.rego
| Condition | Decision | Reason |
|---|---|---|
No workflow context (input.workflow_session == null) | allow | Not a workflow request -- skip |
Causal depth exceeds max_depth | deny | Workflow causal depth exceeds max_depth |
Tool not in permission_ceiling.tools (non-empty ceiling) | escalate | Tool not permitted by workflow permission ceiling |
| Depth within limits and tool in ceiling (or ceiling is empty) | allow | Workflow governance check passed |
Delegation Policy
Package: behavry.delegation.policy
File: policies/delegation/delegation_policy.rego
| Condition | Decision | Reason |
|---|---|---|
No delegation context (input.delegation == null) | allow | Not a delegation request -- skip |
| Delegation not verified | deny | Delegation token not verified |
Tool not in effective_permissions.tools (non-empty scope) | escalate | Tool not permitted by delegation effective_permissions |
| Verified and tool in scope (or scope is empty) | allow | Delegation governance check passed |
Both policies follow the same pattern: they short-circuit to allow when their respective context is absent (non-workflow or non-delegation request), so they do not interfere with standard single-agent requests.
API Reference Summary
Workflow Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/workflows | Create a workflow |
GET | /api/v1/workflows | List workflows |
GET | /api/v1/workflows/{id} | Get workflow details |
PUT | /api/v1/workflows/{id} | Update workflow |
POST | /api/v1/workflows/{id}/suspend | Suspend workflow |
DELETE | /api/v1/workflows/{id} | Delete workflow |
POST | /api/v1/workflows/{id}/sessions | Start session (returns wf_token) |
GET | /api/v1/workflows/{id}/sessions | List sessions |
GET | /api/v1/workflows/{id}/sessions/{sid} | Get session details |
POST | /api/v1/workflows/{id}/sessions/{sid}/complete | Complete session |
POST | /api/v1/workflows/{id}/sessions/{sid}/abort | Abort session |
GET | /api/v1/workflows/{id}/sessions/{sid}/trace | Get decision trace |
GET | /api/v1/workflows/{id}/sessions/{sid}/trace/export | Download trace as JSON |
GET | /api/v1/workflows/{id}/sessions/{sid}/delegations | List session delegations |
Delegation Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/delegations | Issue a scoped d_token |
GET | /api/v1/delegations/{id} | Get delegation record |
POST | /api/v1/delegations/{id}/revoke | Revoke delegation immediately |