---
title: "Consult your agent"
description: "Let an in-call Patter agent escalate to your own back-office agent over HTTP — the dispatch + consult pattern."
---

Patter conducts the call (speech-to-text, the voice agent, text-to-speech, and
the carrier). The **consult** tool gives that in-call agent an on-demand bridge
back to **your own agent** — reachable over HTTP — for deeper reasoning, fresh
information, or an action beyond the call.

This is the *dispatch + consult* pattern: your orchestrator (or any HTTP
endpoint you host) stays **off the per-turn path**. It is consulted only when
the in-call agent decides it needs help, so ordinary turns keep their low
latency while hard turns can reach your full reasoning stack.

## When to use it

- The caller asks something the in-call agent can't answer from its prompt
  (order status, account details, a policy lookup).
- You already run an agent (e.g. an internal assistant) and want phone calls to
  tap into it without putting it on every conversational turn.

If instead you want **your LLM to drive every turn**, that is a different
("brain-on-the-line") mode — consult is the lighter, lower-latency default.

## Quickstart

```ts
import { Patter, type ConsultConfig } from 'getpatter';

const phone = new Patter({ carrier: ... });

const agent = phone.agent({
  systemPrompt:
    "You are front-desk support. If you can't answer directly, consult your back-office agent.",
  stt: ...,            // any Patter STT (pipeline) — or use a Realtime engine
  tts: ...,
  consult: {
    url: 'https://my-orchestrator.example.com/consult',
    headers: { Authorization: `Bearer ${process.env.ORCHESTRATOR_TOKEN}` },
    timeoutMs: 30_000,
  },
});

await phone.serve(agent);
```

When `consult` is set, Patter auto-injects a `consult_agent` tool into the
agent (Realtime and Pipeline modes). The model calls it with a single
`request` string when it needs help.

## The HTTP contract

Patter POSTs JSON to your `url`:

```json
{
  "request": "When does order 4815 ship?",
  "call_id": "CA0000000000000000000000000000a001",
  "caller": "+15555550100",
  "callee": "+15555550199"
}
```

Your endpoint returns JSON with a `reply` (or `response` / `text` / `result` /
`answer` / `message`) string, which the agent speaks:

```json
{ "reply": "Order 4815 ships Tuesday by end of day." }
```

Plain text and other JSON shapes are accepted too (the raw body is spoken /
serialized as a fallback).

## `ConsultConfig`

| Field | Default | Notes |
|---|---|---|
| `url` | `undefined` | Generic webhook endpoint. SSRF-validated at call start. **Mutually exclusive** with `openaiCompatible` — set exactly one. |
| `openaiCompatible` | `undefined` | Native OpenAI-compatible target (e.g. an OpenClaw agent) — no hand-written adapter. Use `openclawConsult(...)`. See [OpenClaw](#openclaw-native-target-no-adapter). |
| `headers` | `undefined` | Sent with every POST (e.g. an `Authorization` bearer). Never logged. |
| `timeoutMs` | `30000` | Per-consult timeout — higher than the generic webhook-tool default (10 000 ms) because a consult may run deeper reasoning. |
| `toolName` | `"consult_agent"` | The tool name the model sees. |
| `description` | (sensible default) | Tune to steer *when* the agent escalates. |
| `reassurance` | `undefined` | Filler the agent speaks while the consult runs (Realtime mode only). The `openclawConsult` preset sets a sensible default. |
| `allowLoopback` | `false` | Opt-in: permit a loopback / private / link-local `url` (e.g. a local back-office agent on `127.0.0.1` or an RFC1918 host). See [Pointing consult at a local agent](#pointing-consult-at-a-local-agent). |

## OpenClaw (native target — no adapter)

To consult an [OpenClaw](/integrations/openclaw) agent, skip the hand-written
adapter entirely: `openclawConsult(...)` speaks OpenClaw's OpenAI-compatible
`POST /v1/chat/completions` gateway directly.

```ts
import { openclawConsult } from 'getpatter';

const agent = phone.agent({
  engine: ...,                        // Realtime recommended — reassurance works there
  systemPrompt: 'You are the after-hours receptionist...',
  consult: openclawConsult('receptionist'),
});
```

That one line targets `model="openclaw/receptionist"` on the local gateway,
sends the call id as both the OpenAI `user` field and the `x-openclaw-session-key`
header (one OpenClaw session per call), reads the operator-grade bearer from
`OPENCLAW_API_KEY` (never logged), auto-enables `allowLoopback` for the
co-located gateway, and attaches a "let me check" reassurance filler. Overridable
options (`openclawConsult(agent, opts)`):

| Option | Default | Notes |
|---|---|---|
| `agent` | — (required) | Agent id → `model="openclaw/<agent>"`. An already-namespaced target (`openclaw/x`, `agent:x`) passes through. Pass a **least-privileged** agent — selecting one is routing, not a security boundary. |
| `baseUrl` | `http://127.0.0.1:18789/v1` | OpenClaw gateway (OpenAI-compatible base). |
| `apiKey` | env `OPENCLAW_API_KEY` | Operator-grade bearer. Prefer the env var. |
| `timeoutMs` | `30000` | Phone-safe. Don't raise above 30 s on a live call. |
| `reassurance` | "Let me check on that for you, one moment." | Realtime-only filler. |

**Enable the endpoint first.** Set
`gateway.http.endpoints.chatCompletions.enabled = true` in
`~/.openclaw/openclaw.json` and keep the gateway bound to loopback. A 404 from the
endpoint means it is disabled (the consult logs an actionable hint). See the
[OpenClaw integration guide](/integrations/openclaw) for agent scoping and
deployment.

Under the hood this is a generic `OpenAICompatibleConsult` codec (`baseUrl` +
`model` + optional `apiKey` / `apiKeyEnv` / `sessionHeader`), so the same
primitive drives any OpenAI-compatible gateway (vLLM, Ollama, Groq). The generic
`{ url }` webhook path below stays the escape hatch for custom request/response
mappings.

## Pointing consult at a local agent

By default the consult URL is SSRF-validated and loopback / private / link-local
targets are rejected — so `http://localhost:8000/consult` will not pass. When your
back-office agent (or a thin adapter) runs on the same machine, set
`allowLoopback: true` to relax that check:

```ts
const agent = phone.agent({
  systemPrompt: '...',
  consult: {
    url: 'http://localhost:8000/consult',   // local agent on loopback
    timeoutMs: 30_000,
    allowLoopback: true,
  },
});
```

What the flag does:

- **Scope.** Relaxes the loopback (`127.0.0.0/8`, `::1`, `localhost`), RFC1918
  private (`10/8`, `172.16/12`, `192.168/16`), and link-local host checks **for the
  consult URL only**. The generic webhook-tool validator path — which can be reached
  by tool / LLM input — stays strict and is unaffected.
- **Always enforced.** Non-HTTP(S) schemes (`file:`, `javascript:`, …) are rejected
  even with the flag on.
- **Why it is safe.** The consult URL is SDK-user configuration, not caller-derived
  input. Cloud-metadata hostnames also become reachable when opted in — only enable
  the flag for a URL you control.

## Behaviour & limits

- **Failure is graceful.** A timeout, non-2xx response, or unreachable endpoint
  does not crash the turn — the agent speaks a short fallback line and carries
  on.
- **Security.** The URL is SSRF-validated; header values are never logged. The
  URL is trusted SDK configuration (not caller-supplied). To point consult at a
  local agent, opt in with `allowLoopback: true` — see
  [Pointing consult at a local agent](#pointing-consult-at-a-local-agent).
- **Mode support.** Injected in **Realtime** and **Pipeline** modes. ElevenLabs
  ConvAI is **not** supported — its tools live on the ElevenLabs-hosted agent,
  so a warning is emitted if you set `consult` with that provider.

See the [Python version](/python-sdk/consult) for the same feature in
`getpatter` for Python.
