Skip to main content

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:

  1. JWT signature and expiry -- using the same RS256 key pair as agent tokens
  2. Token type -- must be workflow_session
  3. Participant membership -- the calling agent must be listed in participant_ids
  4. Session status -- the WorkflowSession row in the database must have status active

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:

FieldDescription
session_idUUID of the workflow session
workflow_idUUID of the parent workflow
workflow_nameHuman-readable name
initiated_byAgent ID that started the session
participant_idsList of all registered participant agent IDs
permission_ceilingTools and resources allowed in this session
max_depthMaximum delegation depth permitted
causal_depthCurrent depth in the call chain (from X-Causal-Depth header)
parent_event_idAudit event ID of the parent call (from X-Parent-Event-Id header)
delegation_chainOrdered 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

EndpointAction
POST /api/v1/workflows/{id}/sessionsStart a new session (mints wf_token)
GET /api/v1/workflows/{id}/sessionsList sessions for a workflow
GET /api/v1/workflows/{id}/sessions/{sid}Get session details
POST /api/v1/workflows/{id}/sessions/{sid}/completeMark session as completed
POST /api/v1/workflows/{id}/sessions/{sid}/abortAbort 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:

  1. JWT decode -- RS256 signature and expiry check
  2. Delegatee match -- the delegatee_id claim must match the calling agent's ID
  3. Session match -- the workflow_session_id claim must match the X-Workflow-Session header
  4. Database row -- the Delegation row must exist, have status active, and not be past expires_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:

FieldTypeDescription
workflow_session_idstringThe session this event belongs to
causal_depthintHow deep in the delegation chain (0 = top-level orchestrator)
parent_event_idstring or nullThe audit event ID that caused this event
delegation_chainlist[string]Ordered list of agent IDs from orchestrator to current agent

These values are propagated via HTTP headers on each proxy request:

HeaderMaps To
X-Workflow-Sessionworkflow_session_id (extracted from JWT sub claim)
X-Causal-Depthcausal_depth
X-Parent-Event-Idparent_event_id
X-Delegation-Chaindelegation_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

ConditionDecisionReason
No workflow context (input.workflow_session == null)allowNot a workflow request -- skip
Causal depth exceeds max_depthdenyWorkflow causal depth exceeds max_depth
Tool not in permission_ceiling.tools (non-empty ceiling)escalateTool not permitted by workflow permission ceiling
Depth within limits and tool in ceiling (or ceiling is empty)allowWorkflow governance check passed

Delegation Policy

Package: behavry.delegation.policy File: policies/delegation/delegation_policy.rego

ConditionDecisionReason
No delegation context (input.delegation == null)allowNot a delegation request -- skip
Delegation not verifieddenyDelegation token not verified
Tool not in effective_permissions.tools (non-empty scope)escalateTool not permitted by delegation effective_permissions
Verified and tool in scope (or scope is empty)allowDelegation 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

MethodPathDescription
POST/api/v1/workflowsCreate a workflow
GET/api/v1/workflowsList workflows
GET/api/v1/workflows/{id}Get workflow details
PUT/api/v1/workflows/{id}Update workflow
POST/api/v1/workflows/{id}/suspendSuspend workflow
DELETE/api/v1/workflows/{id}Delete workflow
POST/api/v1/workflows/{id}/sessionsStart session (returns wf_token)
GET/api/v1/workflows/{id}/sessionsList sessions
GET/api/v1/workflows/{id}/sessions/{sid}Get session details
POST/api/v1/workflows/{id}/sessions/{sid}/completeComplete session
POST/api/v1/workflows/{id}/sessions/{sid}/abortAbort session
GET/api/v1/workflows/{id}/sessions/{sid}/traceGet decision trace
GET/api/v1/workflows/{id}/sessions/{sid}/trace/exportDownload trace as JSON
GET/api/v1/workflows/{id}/sessions/{sid}/delegationsList session delegations

Delegation Endpoints

MethodPathDescription
POST/api/v1/delegationsIssue a scoped d_token
GET/api/v1/delegations/{id}Get delegation record
POST/api/v1/delegations/{id}/revokeRevoke delegation immediately