Agent ↔ Orchestrator Bridge
Paladin agents and Battalion workflows interact bidirectionally:
- An agent can trigger orchestration — schedule a job, enqueue an item, fire an event, or send a notification — through a narrow, policy-guarded port.
- A workflow can invoke an agent — run a single Paladin or a whole Battalion as a step and feed its output back into the workflow.
This guide covers both directions, how to configure the bridge safely, and four end-to-end recipes. It builds on the Orchestration and Content Processing guides.
Every example targets the current v0.5.0 workspace. The substantive examples are real, compiled code pulled from the
paladin-doc-examplescrate via mdBook{{#include}}(one illustrative fragment isrust,ignore). API forms are verified againstcrates/paladin-ports/src/output/orchestrator_port.rs,paladin_executor_port.rs,battalion_port.rs, and the concreteOrchestratorBridgeAdapterinsrc/application/services/orchestration/.
Table of Contents
- Agents Triggering Orchestration
- Orchestration Invoking Agents
- Configuring the Bridge
- Use-Case Recipes
- See Also
Agents Triggering Orchestration
The seam is OrchestratorPort (crates/paladin-ports/src/output/orchestrator_port.rs). It
exposes exactly four actions, mirrored by the BridgeAction enum:
BridgeAction | OrchestratorPort method | Request type | Returns |
|---|---|---|---|
ScheduleJob | schedule_job | ScheduleJobRequest | Uuid |
QueueItem | queue_item | QueueItemRequest | Uuid |
FireEvent | fire_event | FireEventRequest | EventDispatchResult |
SendNotification | send_notification | SendNotificationRequest | Uuid |
The concrete adapter, OrchestratorBridgeAdapter, wraps an Arc<Orchestrator> and a
BridgePolicy. It enforces the policy before performing any underlying call, so an agent can
never exceed the actions or per-execution caps it was granted.
sequenceDiagram
participant Agent as Paladin agent (tool call)
participant Bridge as OrchestratorBridgeAdapter
participant Policy as BridgePolicy
participant Orch as Orchestrator
Agent->>Bridge: fire_event(FireEventRequest)
Bridge->>Policy: is_allowed(FireEvent)?
Policy-->>Bridge: true
Bridge->>Policy: cap_for(FireEvent)
Policy-->>Bridge: 3
Bridge->>Orch: dispatch event (within cap)
Orch-->>Bridge: EventDispatchResult
Bridge-->>Agent: Ok(EventDispatchResult)
Tool-based invocation from an agent loop
Expose the bridge to a Paladin as a tool. When the agent decides to act, the tool implementation
calls the relevant OrchestratorPort method. The agent never touches the Orchestrator
directly — only the policy-guarded port.
#![allow(unused)] fn main() { use paladin_ports::output::orchestrator_port::{ BridgeAction, BridgePolicy, FireEventRequest, OrchestratorBridgeError, OrchestratorPort, }; /// An agent fires a domain event through the policy-guarded bridge. pub async fn agent_triggers_orchestration() -> Result<(), Box<dyn std::error::Error>> { // Grant ONLY the actions this agent should perform, with explicit caps. let mut allowed = HashSet::new(); allowed.insert(BridgeAction::FireEvent); let policy = BridgePolicy::new(allowed, 0, 0, 5, 0); // up to 5 events, nothing else // In production this is an `OrchestratorBridgeAdapter`; here a mock stands in. let bridge: Arc<dyn OrchestratorPort> = mock_orchestrator(); let _ = &policy; // the real adapter is constructed as `::new(orchestrator, policy)` match bridge .fire_event(FireEventRequest { event_type: "critical_finding".to_string(), payload: serde_json::json!({ "severity": "high" }), source: "security-agent".to_string(), }) .await { Ok(result) => println!("fired; {} trigger(s) matched", result.triggered_count), Err(OrchestratorBridgeError::ActionNotAllowed(_)) => { eprintln!("policy forbids this action") } Err(OrchestratorBridgeError::QuotaExceeded { .. }) => { eprintln!("per-execution cap reached") } Err(e) => return Err(e.into()), } Ok(()) } }
OrchestratorBridgeError distinguishes ActionNotAllowed (the policy doesn't grant the action)
from QuotaExceeded (the per-execution cap is reached), so an agent can react sensibly instead
of failing opaquely.
Orchestration Invoking Agents
The reverse direction uses the executor ports:
PaladinExecutorPort(paladin_executor_port.rs) — run a single Paladin:async fn execute(&self, paladin: &Paladin, input: &str) -> Result<PaladinResult, PaladinError>.BattalionPort(battalion_port.rs) — run/monitor a whole Battalion by id:execute(battalion_id) -> BattalionResult, plusstatusandcancel.
A workflow step builds the input string (passing context from earlier steps), calls the executor, and reads the result back out.
sequenceDiagram
participant WF as Workflow step
participant Exec as PaladinExecutorPort
participant Paladin as Paladin agent
WF->>Exec: execute(&paladin, input_with_context)
Exec->>Paladin: run agent loop
Paladin-->>Exec: PaladinResult { output, token_count, ... }
Exec-->>WF: Ok(PaladinResult)
Note over WF: feed result.output into the next step
#![allow(unused)] fn main() { use paladin_core::platform::container::paladin::Paladin; use paladin_ports::output::paladin_executor_port::PaladinExecutorPort; /// A workflow step runs a single Paladin, passing context via the input string. pub async fn orchestration_invokes_agent( analyst: &Paladin, ) -> Result<(), Box<dyn std::error::Error>> { let executor: Arc<dyn PaladinExecutorPort> = mock_executor(); let upstream = "Q3 revenue rose 12% QoQ; churn fell to 2.1%."; let input = format!("Summarize the key risks given this context:\n{upstream}"); let result = executor.execute(analyst, &input).await?; println!("agent said: {}", result.output); println!( "tokens: {}, stop reason: {:?}", result.token_count, result.stop_reason ); Ok(()) } }
PaladinResult carries output, token_count, execution_time_ms, loop_count, and
stop_reason — everything the workflow needs to decide what to do next. To invoke a whole
Battalion instead of a single agent, use BattalionPort::execute(battalion_id) and read the
BattalionResult (see Orchestration → Configuration Reference).
Configuring the Bridge
Bridge behavior is configured programmatically through BridgePolicy — there is no dedicated
config.yml bridge section in v0.5.0. A policy is two things: the set of allowed actions, and a
per-execution cap for each action.
#![allow(unused)] fn main() { /// Build least-privilege and default bridge policies. pub fn configure_bridge() { use paladin_ports::output::orchestrator_port::{BridgeAction, BridgePolicy}; // Explicit, least-privilege: allow scheduling + notifications only, // with caps of (jobs=2, queue=0, events=0, notifications=5). let mut allowed = HashSet::new(); allowed.insert(BridgeAction::ScheduleJob); allowed.insert(BridgeAction::SendNotification); let policy = BridgePolicy::new(allowed, 2, 0, 0, 5); // Builder-style: start from caps and add actions. let policy = BridgePolicy::new(HashSet::new(), 1, 1, 1, 1) .allow(BridgeAction::FireEvent) .allow(BridgeAction::QueueItem); // Conservative-but-usable default: all four actions, cap 3 each. let policy = BridgePolicy::default(); } }
The three forms shown are: an explicit least-privilege policy, the builder-style .allow(..),
and the conservative-but-usable Default (all four actions, cap 3 each). Prefer an explicit
least-privilege policy for agents you don't fully trust.
Tip: because the adapter enforces the policy before every call, tightening a policy is a safe, local change — you don't have to audit the agent's prompt to constrain what it can do.
Use-Case Recipes
1. News monitoring pipeline with AI analysis
NewsApiFetcher → AI summarization (LlmContentAnalyzer) → notification via the bridge.
#![allow(unused)] fn main() { use paladin_ports::output::orchestrator_port::SendNotificationRequest; /// Recipe: notify the result of an AI summary through the bridge. pub async fn recipe_news_notification( bridge: &Arc<dyn OrchestratorPort>, summary: &str, ) -> Result<(), Box<dyn std::error::Error>> { bridge .send_notification(SendNotificationRequest { channel: "email".to_string(), recipient: "ops@example.com".to_string(), subject: "Daily news digest".to_string(), body: summary.to_string(), }) .await?; Ok(()) } }
See Content Processing for the ingestion/analysis half and Orchestration → Job Scheduling to run this on a cron.
2. Research workflow
A web/HTTP tool gathers sources, a Paladin synthesizes them, and a Formation assembles the final report.
// 1. Agent gathers sources via an HTTP tool (Arsenal), producing notes.
// 2. Synthesis Paladin run as a workflow step:
let synthesis = executor.execute(&synthesizer, &collected_notes).await?;
// 3. Formation assembles intro → body → conclusion from the synthesis.
let report = formation_service.execute(&report_formation, &synthesis.output).await?;
3. Scheduled batch enrichment (job queue)
A recurring job enqueues items; a worker drains the queue and runs each through a Paladin.
#![allow(unused)] fn main() { use paladin_core::platform::container::schedule::Schedule; use paladin_ports::output::orchestrator_port::{QueueItemRequest, ScheduleJobRequest}; /// Recipe: schedule a recurring batch job and enqueue an item. pub async fn recipe_scheduled_batch( bridge: &Arc<dyn OrchestratorPort>, content_id: &str, ) -> Result<(), Box<dyn std::error::Error>> { bridge .schedule_job(ScheduleJobRequest { name: "nightly-enrichment".to_string(), description: "Enrich the day's content with AI tags".to_string(), schedule: Schedule::Daily(2, 0), // 02:00 daily }) .await?; bridge .queue_item(QueueItemRequest { queue_name: "enrichment".to_string(), payload: serde_json::json!({ "content_id": content_id }), }) .await?; Ok(()) } }
4. Trigger-initiated agent run
An agent fires a domain event; a registered Trigger matches it and initiates a Paladin run — fully event-driven, no polling.
#![allow(unused)] fn main() { /// Recipe: an agent fires an event that a Trigger turns into a Paladin run. pub async fn recipe_trigger_initiated( bridge: &Arc<dyn OrchestratorPort>, ) -> Result<(), Box<dyn std::error::Error>> { let dispatch = bridge .fire_event(FireEventRequest { event_type: "anomaly_detected".to_string(), payload: serde_json::json!({ "metric": "latency_p99", "value": 920 }), source: "monitor-agent".to_string(), }) .await?; println!("{} trigger(s) initiated", dispatch.triggered_count); Ok(()) } }
See Also
- Orchestration — the Battalion patterns, job scheduler, and trigger system the bridge drives.
- Content Processing — the ingestion/analysis pipeline used in recipes 1 and 3.
- Paladin Agents — building the agents on both sides of the bridge.
- Crate Map — where
OrchestratorPortand the executor ports live.