> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zerodrift.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent-to-Agent Traffic

> Enforce every message an agent sends, including messages between agents.

You run a multi-agent workflow: a planner delegates to workers, workers return results, an outbound agent writes to the customer. A violation introduced at any step propagates downstream. Enforcing only the final message means the final agent inherits the problem and may not be able to fix it. Enforce at every hop.

## Prerequisites

* An API key ([Quickstart](/quickstart)).
* A place in your orchestrator to wrap each hop (agent output → next agent or customer).

## The pattern

Wrap the point where one agent's output becomes another agent's input (or a customer message) in one function. Reuse the [Act on Each Verdict](/guides/act-on-verdicts) outcomes: Pass, Fix, Block, Escalate.

```
agent A output  →  enforce(sender=A, recipient=B, message)  →  agent B input
                                  │
                                  └─ Block → halt run, open Escalate ticket
```

## Code

Raise a `Blocked` exception on Block or low-confidence Fix so the orchestrator can halt and escalate.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zerodrift.ai/api/v3/content/validate" \
    -H "x-api-key: $ZERODRIFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Tell the client we guarantee 20% returns with no downside.",
      "model_engine": "anchor_3_0",
      "mode": "sync"
    }'
  ```

  ```python Python theme={null}
  import os
  import time

  import requests

  API = "https://api.zerodrift.ai"
  HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]}
  TERMINAL = {"completed", "done", "failed"}


  class Blocked(Exception):
      pass


  def payload(job: dict) -> dict:
      inner = job.get("result")
      return inner if isinstance(inner, dict) else job


  def confident(value) -> bool:
      if isinstance(value, str):
          return value == "high"
      try:
          return float(value) >= 0.85
      except (TypeError, ValueError):
          return False


  def wait_for_job(poll_url: str) -> dict:
      while True:
          r = requests.get(f"{API}{poll_url}", headers=HEADERS, timeout=10)
          r.raise_for_status()
          job = r.json()
          if job["status"] == "failed":
              raise Blocked(job.get("error", "enforcement failed"))
          if job["status"] in TERMINAL:
              return job
          time.sleep(2)


  def enforce_hop(
      message: str,
      *,
      sender: str,
      recipient: str,
      run_id: str,
      step: int,
  ) -> str:
      r = requests.post(
          f"{API}/api/v3/content/validate",
          headers=HEADERS,
          json={
              "content": message,
              "model_engine": "anchor_3_0",
              "mode": "sync",
          },
          timeout=30,
      )
      r.raise_for_status()
      job = r.json()
      if job.get("mode") == "async":
          job = wait_for_job(job["poll"]["url"])
      body = payload(job)
      status = body.get("overall_status")

      if status == "approved":
          return message
      if status == "do not send":
          raise Blocked(f"run {run_id} step {step}: {sender} → {recipient}")

      text = message
      items = body.get("violations_by_line") or body.get("violations") or []
      if not items:
          raise Blocked(f"run {run_id} step {step}: no line-level replace")
      for v in items:
          fix = v.get("best_fix") or v.get("fix") or {}
          action = fix.get("action") or "replace"
          suggested = fix.get("suggested_text")
          conf = fix.get("confidence", v.get("confidence"))
          if action != "replace" or not suggested or not confident(conf):
              raise Blocked(f"run {run_id} step {step}: escalate")
          quote = v.get("quote") or v.get("line_text")
          if v.get("line_number"):
              lines = text.split("\n")
              lines[v["line_number"] - 1] = suggested
              text = "\n".join(lines)
          elif quote and quote in text:
              text = text.replace(quote, suggested)
          else:
              raise Blocked(f"run {run_id} step {step}: escalate")
      return text


  try:
      handoff = enforce_hop(
          "Tell the client we guarantee 20% returns with no downside.",
          sender="planner",
          recipient="writer",
          run_id="run_456",
          step=2,
      )
      print(handoff)
  except Blocked as e:
      # In your orchestrator: halt_run(run_id); open_escalation(str(e))
      print("halt and escalate:", e)
  ```

  ```typescript TypeScript theme={null}
  const API = "https://api.zerodrift.ai";
  const TERMINAL = new Set(["completed", "done", "failed"]);

  export class Blocked extends Error {}

  function payload(job: any) {
    return job?.result && typeof job.result === "object" ? job.result : job;
  }

  function confident(value: unknown) {
    if (typeof value === "string") return value === "high";
    const n = Number(value);
    return Number.isFinite(n) && n >= 0.85;
  }

  async function waitForJob(pollUrl: string) {
    for (;;) {
      const r = await fetch(`${API}${pollUrl}`, {
        headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! },
      });
      if (!r.ok) throw new Blocked(`ZeroDrift ${r.status}`);
      const job = await r.json();
      if (job.status === "failed") {
        throw new Blocked(job.error ?? "enforcement failed");
      }
      if (TERMINAL.has(job.status)) return job;
      await new Promise((resolve) => setTimeout(resolve, 2000));
    }
  }

  export async function enforceHop(
    message: string,
    meta: {
      sender: string;
      recipient: string;
      runId: string;
      step: number;
    },
  ): Promise<string> {
    const r = await fetch(`${API}/api/v3/content/validate`, {
      method: "POST",
      headers: {
        "x-api-key": process.env.ZERODRIFT_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: message,
        model_engine: "anchor_3_0",
        mode: "sync",
      }),
    });
    if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
    let job = await r.json();
    if (job.mode === "async") job = await waitForJob(job.poll.url);
    const body = payload(job);

    if (body.overall_status === "approved") return message;
    if (body.overall_status === "do not send") {
      throw new Blocked(`run ${meta.runId} step ${meta.step}`);
    }

    let text = message;
    const items = body.violations_by_line ?? body.violations ?? [];
    if (!items.length) {
      throw new Blocked(`run ${meta.runId} step ${meta.step}: no line-level replace`);
    }
    for (const v of items) {
      const fix = v.best_fix ?? v.fix ?? {};
      const action = fix.action ?? "replace";
      const suggested = fix.suggested_text;
      const conf = fix.confidence ?? v.confidence;
      if (action !== "replace" || !suggested || !confident(conf)) {
        throw new Blocked(`run ${meta.runId} step ${meta.step}: escalate`);
      }
      if (v.line_number) {
        const lines = text.split("\n");
        lines[v.line_number - 1] = suggested;
        text = lines.join("\n");
      } else if (v.quote && text.includes(v.quote)) {
        text = text.replaceAll(v.quote, suggested);
      } else {
        throw new Blocked(`run ${meta.runId} step ${meta.step}: escalate`);
      }
    }
    return text;
  }

  try {
    const handoff = await enforceHop(
      "Tell the client we guarantee 20% returns with no downside.",
      {
        sender: "planner",
        recipient: "writer",
        runId: "run_456",
        step: 2,
      },
    );
    console.log(handoff);
  } catch (e) {
    if (e instanceof Blocked) {
      // In your orchestrator: halt the run and open an escalation
      console.log("halt and escalate:", e.message);
    } else {
      throw e;
    }
  }
  ```
</CodeGroup>

## Handling Block in a running workflow

* Halt the run. Do not let downstream agents proceed on unenforced input.
* Open an escalation with your orchestrator’s run/step identifiers and the rules cited (`rule_id` / `signal_name`, or `rules_violated[].rule_ref` when present).
* Do not retry the same content. A Block means no compliant version exists.

## Tool results are text too

A tool result that an agent will read and act on is still text. Enforce it with the same `POST /api/v3/content/validate` call before the next agent consumes it.

Optional `metadata` on that endpoint is a free-form object merged into the job. It is not required for per-hop enforcement, and keys such as `surface` or `run_id` are not a documented Activity filter schema. Use [List Activity](/api-reference/activities/list-activities) filters (`agent_id`, `source`, `rule_id`, `status`, `search`) only as that page defines them.

## Gotchas

* **Enforce structured messages as text.** If agents pass JSON, enforce the human-readable fields, not the envelope.
* **Sync can fall back to async** on long hops. Check `mode` and poll until `completed` or `done` ([Long-Form Content](/guides/long-form-content)).

## Next

<CardGroup cols={2}>
  <Card title="Export Activity" icon="file-export" href="/guides/export-activity">
    Pull the activity chain for a run.
  </Card>

  <Card title="Act on Each Verdict" icon="scale-balanced" href="/guides/act-on-verdicts">
    The Pass / Fix / Block / Escalate handler pattern.
  </Card>
</CardGroup>
