Integrate openclaw
Agent Skills for the Patter SDK — give your AI agent a phone number. Works in Claude Code, Cursor, OpenClaw, Hermes Agent, Codex, Cline, Goose, Amp, Windsurf, and any harness that consumes the Agent Skills standard. One CLI: npx skills add patterai/skills
npx -y skills add PatterAI/skills --skill integrate-openclawAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Wire Patter as the voice layer on top of an OpenClaw brain — the "brain on the line" pattern. Use when the user runs OpenClaw (or any OpenAI-compatible agent gateway) and wants a phone number that answers any caller, talks with low latency, and consults a specific OpenClaw receptionist agent for real data/actions mid-call without the call dropping during a slow (30-60 s) tool. Covers both directions (OpenClaw drives Patter via `patter-mcp`; Patter consults OpenClaw via `ConsultConfig`), long-tool-call survival, speakerphone noise tuning, open inbound, and least-privilege agent scoping. Patter 0.7.0, Python and TypeScript.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
31.3 KB, as published. Nobody here has run it
Integrate Patter with OpenClaw (brain on the line)
OpenClaw is a self-hosted gateway of LLM-backed agents with scoped tool connections (calendars, customer DBs, …). Patter is the voice layer: it owns the carrier leg, talks with low latency, and reaches into OpenClaw only when a turn needs real data or an action. This is the brain-on-the-line architecture — the inverse of wiring OpenClaw in as a "Custom LLM" that sits on every turn (the topology that kills calls on slow tools).
CALLER (any number, no allowlist)
│
▼
Twilio / Telnyx DID ──► PATTER voice agent (OpenAI Realtime, low-latency EN)
│ owns turn-taking + TTS locally
│ speaks "let me check, one moment"
▼
consult ──► local adapter ──► OpenClaw
▲ (loopback) /v1/chat/completions
│ model="openclaw/<receptionistAgentId>"
speaks the reply (ONE scoped agent)
The two directions — and which one to use
| Direction | Who initiates | Transport | Use when |
|---|---|---|---|
| A — OpenClaw drives Patter | OpenClaw agent | patter-mcp MCP server (tools) | OpenClaw should place outbound calls ("call the clinic, wait, tell me what they said"). Blocking until the call ends is the desired semantics here. |
| B — Patter consults OpenClaw | In-call Patter agent | ConsultConfig → HTTP adapter → OpenClaw gateway | Patter answers an inbound call and needs the brain mid-conversation. This is the brain-on-the-line pattern the after-hours-receptionist use case needs. |
They compose, but for an inbound receptionist line Direction B is the one to build first. The rest of this skill is mostly Direction B; Direction A is at the end.
Why not OpenClaw's native voice plugin?
OpenClaw's built-in voice plugin only accepts inbound calls from an
allowlist (inboundPolicy: "allowlist" + allowFrom: [...]); there is no
"accept anyone" mode. That is useless for a public after-hours line where random
people call. Patter owns the DID and answers everyone, then reaches the
receptionist over OpenClaw's operator-side /v1/chat/completions gateway — the
caller is never an OpenClaw "sender" subject to the allowlist. This is the single
clearest reason to put Patter in front.
Direction B — Patter consults OpenClaw mid-call
Step 1 — Enable OpenClaw's chat-completions gateway
OpenClaw exposes an OpenAI-compatible POST /v1/chat/completions. It is
disabled by default. Enable it in ~/.openclaw/openclaw.json (JSON5):
{
gateway: {
http: {
endpoints: {
chatCompletions: {
enabled: true,
},
},
},
},
}
<Warning>
**The gateway credential is operator-grade — the `model` field is NOT a security
boundary.** Selecting an agent in the `model` field picks the *persona*, but with
shared-secret (token/password) auth the gateway credential carries full operator
scope across the whole gateway. Real isolation comes from three things, none of
which is the token:
- A dedicated least-privileged receptionist agent with a tight per-agent
tools.allow/tools.deny(see Step 5). - One gateway per client — bind the gateway to loopback / tailnet only
and never expose
/v1/chat/completionsto the public internet. On a single always-on Mac Mini per client (one OS user = one gateway = one credential set), this isolation is free. - An auth credential on the gateway (mirror OpenClaw's own gateway-auth guidance — do not stand up an unauthenticated, network-reachable endpoint).
Confirm the exact gateway bind/auth keys and the model-routing alias forms
against the live OpenClaw docs (docs.openclaw.ai)
before you ship — they are OpenClaw-side config, not Patter's.
</Warning>
Step 2 — Route the consult to ONE specific receptionist agent
On /v1/chat/completions the model field is an agent target:
"openclaw"/"openclaw/default"→ the default agent — which may be the master / CEO / financial agent and can drift between environments. Never use this from the voice layer."openclaw/<receptionistAgentId>"→ one explicit, named agent. Always pin this. (Alias formsopenclaw:<id>/agent:<id>are commonly accepted — verify against OpenClaw docs.)
Per client, set the agentId explicitly:
| Client | Receptionist agentId |
|---|---|
| Roofing contractor (CA) | openclaw/roofing-ca-receptionist |
| Home-automation contractor (FL) | openclaw/home-fl-receptionist |
Step 3 — Point consult at the OpenClaw agent
Recommended: the native target — no adapter. Patter's consult speaks OpenClaw's
/chat/completions endpoint directly; point it at one scoped agent in a single line:
from getpatter import ConsultConfig, OpenAIRealtime2
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="You are the after-hours receptionist...",
consult=ConsultConfig.openclaw("receptionist"),
)
import { openclawConsult, OpenAIRealtime2 } from "getpatter";
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "You are the after-hours receptionist...",
consult: openclawConsult("receptionist"),
});
It targets model="openclaw/receptionist", sends the call id as the OpenAI user field
plus the x-openclaw-session-key header (one OpenClaw session per call), reads the
operator-grade bearer from OPENCLAW_API_KEY (never logged), auto-enables allow_loopback
for the co-located gateway, and attaches a default "let me check" reassurance filler.
Notify OpenClaw at call end with openclaw_post_call_notifier("receptionist") /
openclawPostCallNotifier("receptionist") on serve(on_call_end=...) to post the call
record (caller, line, duration, transcript) to the same agent and session.
Escape hatch — a hand-written adapter, only when you need a custom request/response
mapping (e.g. a non-OpenClaw back office). Patter POSTs { request, call_id, caller, callee }; the adapter must:
- Target one named agent (
model="openclaw/<receptionistAgentId>"). - Carry
call_idas the OpenClaw session key so a multi-turn call maps to one OpenClaw session (continuity), and forwardcallerfor caller-ID prefetch of the customer record. - Return a concise, spoken-ready string (don't dump a raw API blob back into the voice context).
# adapter.py — forwards Patter consult requests to ONE OpenClaw receptionist agent
import os
import httpx
from fastapi import FastAPI, Request
app = FastAPI()
OPENCLAW_URL = os.environ["OPENCLAW_URL"] # e.g. http://127.0.0.1:18789/v1/chat/completions
OPENCLAW_TOKEN = os.environ["OPENCLAW_GATEWAY_TOKEN"]
RECEPTIONIST_AGENT = os.environ.get("OPENCLAW_AGENT", "openclaw/receptionist")
@app.post("/consult")
async def consult(req: Request):
body = await req.json()
request_text = body.get("request", "")
call_id = body.get("call_id", "")
caller = body.get("caller", "")
payload = {
"model": RECEPTIONIST_AGENT, # ONE scoped agent, never "openclaw"
"user": call_id, # one OpenClaw session per call
"messages": [{
"role": "user",
# caller-id is forwarded so the brain can prefetch the customer record
"content": f"[caller={caller}] {request_text}",
}],
}
headers = {"Authorization": f"Bearer {OPENCLAW_TOKEN}"}
# 75 s is sized to OpenClaw's worst-case tool time (see "timeout ladder").
async with httpx.AsyncClient(timeout=75.0) as client:
resp = await client.post(OPENCLAW_URL, json=payload, headers=headers)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
# Trim to a short, spoken-ready reply before it re-enters the voice context.
return {"reply": content.strip()[:600]}
// adapter.ts — forwards Patter consult requests to ONE OpenClaw receptionist agent
import express from "express";
const app = express();
app.use(express.json());
const OPENCLAW_URL = process.env.OPENCLAW_URL!; // http://127.0.0.1:18789/v1/chat/completions
const OPENCLAW_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN!;
const RECEPTIONIST_AGENT = process.env.OPENCLAW_AGENT ?? "openclaw/receptionist";
app.post("/consult", async (req, res) => {
const { request = "", call_id = "", caller = "" } = req.body ?? {};
const payload = {
model: RECEPTIONIST_AGENT, // ONE scoped agent, never "openclaw"
user: call_id, // one OpenClaw session per call
messages: [{ role: "user", content: `[caller=${caller}] ${request}` }],
};
const resp = await fetch(OPENCLAW_URL, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${OPENCLAW_TOKEN}` },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(75_000), // see "timeout ladder"
});
const data = await resp.json();
const content: string = data.choices[0].message.content;
res.json({ reply: content.trim().slice(0, 600) }); // short, spoken-ready
});
app.listen(8000);
The adapter is also where you can drop slow work onto an OpenClaw sub-agent (e.g. spawn a worker, return a short "checking…" and let a later turn fetch the result). Optional — the in-band reassurance below already covers dead air for a single 60 s lookup.
Step 4 — Point the Patter agent at the adapter
ConsultConfig (Python) / consult (TS) auto-injects a consult_agent tool the
in-call agent can call. On a single Mac Mini the adapter is on loopback, so opt
in with allow_loopback / allowLoopback.
from getpatter import Patter, Twilio, OpenAIRealtime2, ConsultConfig
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(), # low-latency English, strongest tool flow
system_prompt=(
"You are the after-hours receptionist for Acme Roofing. "
"When the caller asks about appointments, availability, or account "
"details, call `consult_agent` with a clear request. "
"Say a brief 'let me check' first, then read back the answer."
),
first_message="Thanks for calling Acme Roofing, how can I help?",
consult=ConsultConfig(
url="http://127.0.0.1:8000/consult",
timeout_s=75.0, # sized to the brain's worst-case tool time
allow_loopback=True, # adapter is co-located on this box
headers={"Authorization": "Bearer <adapter-token>"}, # optional, never logged
# tool_name / description default to "consult_agent" + a sensible prompt;
# override description to steer WHEN the agent escalates.
),
)
phone.serve(agent)
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(), // low-latency English, strongest tool flow
systemPrompt:
"You are the after-hours receptionist for Acme Roofing. " +
"When the caller asks about appointments, availability, or account " +
"details, call consult_agent with a clear request. " +
"Say a brief 'let me check' first, then read back the answer.",
firstMessage: "Thanks for calling Acme Roofing, how can I help?",
consult: {
url: "http://127.0.0.1:8000/consult",
timeoutMs: 75_000, // sized to the brain's worst-case tool time
allowLoopback: true, // adapter is co-located on this box
headers: { Authorization: "Bearer <adapter-token>" }, // optional, never logged
},
});
await phone.serve({ agent });
Defaults to know: consult timeout defaults to 30 s (timeout_s=30.0 /
timeoutMs: 30000), the tool is named consult_agent, and consult is injected
in Realtime and Pipeline only (a warning is emitted if you set it with
ElevenLabs ConvAI).
Long-tool-call survival (the literal blocker)
The prior failure with another platform was: the agent makes a 30-60 s tool call (sometimes browser automation) and the call dies. The fix is three layers, all of which must hold.
1. The timeout ladder — the call dies at the SHORTEST link
| Layer | Where | Set to | Why |
|---|---|---|---|
| Patter consult timeout | ConsultConfig.timeout_s / consult.timeoutMs | 75-90 s | Above the brain's worst-case tool time. Default 30 s is too short for 30-60 s automation. |
| Adapter HTTP timeout | httpx.AsyncClient(timeout=…) / AbortSignal.timeout(…) | = consult timeout | The adapter must outlast the consult, not undercut it. |
OpenClaw gateway chatCompletions timeout | OpenClaw config | raised | The gateway must not give up before the agent's tool returns. |
Direction A: mcp.servers.patter.timeout | OpenClaw config | 600 s | A blocking make_call runs for the whole call. |
Verify the per-server timeout is honoured. OpenClaw's docs don't publish a default for the per-server MCP timeout, and an MCP client can impose its own read-timeout ceiling that silently caps a long blocking call regardless of config. After wiring, probe the server with
openclaw mcp doctor patter --probe(a live connection check).openclaw mcp status --verboseshows the resolved timeout but does NOT open a connection.
2. Reassurance — speak a filler the instant the tool starts
Without a filler the caller hears dead air for the whole lookup and assumes the
line froze. Attach reassurance to the consult tool (or any slow tool) so
the agent speaks immediately and keeps the media stream alive while the brain
works.
The consult_agent tool is auto-built, so to give it reassurance the cleanest
path today is to add your own slow tool that wraps the consult, OR steer the
agent via the system prompt to say "let me check, one moment" before it calls
consult_agent. For a hand-built slow tool, set reassurance on the Tool
(Python) / ToolDefinition (TS):
from getpatter import Tool
# reassurance is a field on the Tool dataclass (string shorthand → after_ms=1500,
# or a dict {"message": str, "after_ms": int}). Realtime mode only today.
check_schedule = Tool(
name="check_schedule",
description="Look up the caller's appointment in the calendar.",
parameters={"type": "object", "properties": {"day": {"type": "string"}}, "required": ["day"]},
handler=my_slow_handler, # may take 30-60 s
reassurance="Let me check the calendar for you, one moment.",
)
// In TS, reassurance and a per-tool timeoutMs are fields on the raw
// ToolDefinition (defineTool does not expose them — build the object directly).
const checkSchedule = {
name: "check_schedule",
description: "Look up the caller's appointment in the calendar.",
parameters: {
type: "object",
properties: { day: { type: "string" } },
required: ["day"],
},
handler: mySlowHandler, // may take 30-60 s
reassurance: "Let me check the calendar for you, one moment.",
timeoutMs: 60_000, // raise off the 10 s default
};
<Warning>
**Reassurance is Realtime-only as of 0.7.0.** In Pipeline mode the field is silently
ignored (the LLM has to generate its own filler); ConvAI doesn't expose it. For
the receptionist line, **use Realtime** — it is also the lowest-latency English
path and has the strongest tool flow.
</Warning>
3. Per-tool timeout
- TypeScript:
ToolDefinition.timeoutMs(default 10 000 ms, clamped to 300 000 ms). Raise it to60_000for slow tools — shown above. A timeout returns{ error, fallback: true }and is not retried. - Python:
timeout_son thetool()factory /Tooldataclass since 0.6.4 (tool(name=..., handler=..., timeout_s=60.0); defaultNonekeeps the 10 s behaviour, clamped to 300 s). Same terminal semantics as TS. The consult path keeps its own independentConsultConfig.timeout_s.
Why this beats "OpenClaw as a Custom LLM"
The failure mode to avoid is wiring OpenClaw in as the per-turn LLM (a
"Custom LLM" / /v1/chat/completions on the critical path of every utterance).
Then the brain's full reasoning + tool latency blocks every response, and a
60 s tool stalls the turn with no acknowledge-then-continue primitive. Patter's
consult is the inverse: the local voice model owns turn-taking and TTS, and
the brain is a consulted specialist invoked only on hard turns. Keep it that way.
Noise & turn-detection for speakerphone
Contractors call from job sites on speakerphone. Tiny noises (a mouse move, the phone shifting) can false-trigger turn detection and cut the agent off mid-sentence. The live levers:
| Lever | Field | Mode | For the noisy line |
|---|---|---|---|
| Barge-in floor | barge_in_threshold_ms / bargeInThresholdMs (default 300) | Realtime + Pipeline | Raise to ~500 so a transient doesn't count as a barge-in. 0 disables barge-in entirely. |
| Far-field noise reduction (0.6.4+) | openai_realtime_noise_reduction="far_field" / openaiRealtimeNoiseReduction | Realtime | Recommended for speakerphone / conference-room callers. |
| Server-VAD tuning (0.6.4+) | realtime_turn_detection=RealtimeTurnDetection(...) / realtimeTurnDetection | Realtime | Raise threshold (~0.7) and silence_duration_ms (~700), or switch to type="semantic_vad" with eagerness="low" so callers can finish a thought. |
| Krisp denoiser (SDK main, post-0.7.0) | denoiser="krisp-viva-tel-v2" (job-site noise) or "krisp-bvc-o-pro-v3" (background voices) | Pipeline | BYO license: getpatter[krisp] + KRISP_VIVA_SDK_LICENSE_KEY + KRISP_MODELS_DIR. |
| High-pass + AGC (0.7.0) | high_pass_hz=100, agc=True / highPassHz, agc | Pipeline | Kills mains hum / handling rumble; levels quiet variable-distance talkers. |
| Silero VAD | Agent(vad=SileroVAD.load(...)) | Pipeline | Raise activation to ~0.8 (filters background noise) and silence to ~0.6-1.0 s (lets callers pause mid-thought). |
| Semantic end-of-turn | turn_detector=SmartTurnDetector.load() (audio) or NamoTurnDetector.load() (transcript; SDK main, post-0.7.0) | Pipeline | Holds the turn while the caller is mid-sentence, bounded by max_semantic_hold_ms (default 1200). |
Realtime receptionist (reassurance works here — preferred for this line):
from getpatter import Patter, Twilio, OpenAIRealtime2, RealtimeTurnDetection
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="You are the after-hours receptionist for Acme Roofing.",
first_message="Thanks for calling Acme Roofing, how can I help?",
barge_in_threshold_ms=500, # noisy speakerphone
openai_realtime_noise_reduction="far_field", # speakerphone preset
realtime_turn_detection=RealtimeTurnDetection(
type="server_vad", threshold=0.7, silence_duration_ms=700,
),
consult=ConsultConfig(url="http://127.0.0.1:8000/consult", timeout_s=75.0, allow_loopback=True),
)
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "You are the after-hours receptionist for Acme Roofing.",
firstMessage: "Thanks for calling Acme Roofing, how can I help?",
bargeInThresholdMs: 500, // noisy speakerphone
openaiRealtimeNoiseReduction: "far_field", // speakerphone preset
realtimeTurnDetection: { type: "server_vad", threshold: 0.7, silenceDurationMs: 700 },
consult: { url: "http://127.0.0.1:8000/consult", timeoutMs: 75_000, allowLoopback: true },
});
Pipeline variant — deeper audio chain when you control the STT/LLM/TTS stack
(see build-voice-agent → references/pipeline-mode.md for the full lever
docs):
from getpatter import Patter, Twilio, NamoTurnDetector
from getpatter.providers.silero_vad import SileroVAD
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
# Pipeline mode (no engine=) — exposes the full inbound audio chain
system_prompt="You are the after-hours receptionist for Acme Roofing.",
first_message="Thanks for calling Acme Roofing, how can I help?",
barge_in_threshold_ms=500,
denoiser="krisp-viva-tel-v2", # BYO Krisp license
high_pass_hz=100, # hum + handling rumble
agc=True, # quiet talkers
vad=SileroVAD.load(activation_threshold=0.8, min_silence_duration=0.7),
turn_detector=NamoTurnDetector.load(), # PATTER_NAMO_MODEL
consult=ConsultConfig(url="http://127.0.0.1:8000/consult", timeout_s=75.0, allow_loopback=True),
)
<Warning>
**Don't reach for `echo_cancellation` on a phone line.** Patter's AEC is
browser/native-audio only — on a PSTN call the carrier round-trip exceeds the
filter window, so it passes audio through unchanged (line echo is the
carrier's job, ITU-T G.168). For speakerphone TTS bleed use the Realtime
`far_field` preset or the Pipeline denoiser instead. Reassurance is still
Realtime-only — prefer **Realtime** for the receptionist line, which since
0.6.4 also has first-class noise levers (`far_field`,
`realtime_turn_detection`).
</Warning>
Inbound: anyone can call
Patter answers any caller on its DID — no allowlist. There are two ways to set the inbound agent:
- In the SDK: the agent you pass to
phone.serve(agent)answers all inbound calls. VerifyX-Twilio-Signature/ Telnyx Ed25519 at the carrier edge (Patter does this for you) — that is the only trust boundary the public touches. - Via
patter-mcp(Direction A): theconfigure_inboundtool sets the default agent that answers all future inbound calls. Fields:systemPrompt,firstMessage(opt.),engineMode(defaultpipeline),sttProvider/llmProvider/ttsProvider(pipeline),voice,language.
Contrast with OpenClaw's native voice plugin, whose inboundPolicy is
allowlist-only (disabled by default) — it structurally cannot front random after-hours callers.
Patter terminates the carrier leg for everyone; OpenClaw stays the brain
behind it.
Least-privilege agent scoping (OpenClaw side)
The voice layer must reach only the receptionist, never the master agent.
Model the receptionist as a top-level agents.list[] entry (not a sub-agent
of the master — sub-agent auth can fall back to the parent's credentials), with:
- its own workspace /
agentDir/ auth profiles, - a tight
tools.allow(only the calendar + customer-DB tools) and explicittools.deny(exec / write / browser / gateway), - per-agent sandbox (
mode: all,scope: agent).
Expose any third-party MCP tools (calendar, customer DB) only to that agent
(per-server agent projection + a narrow toolFilter.include). Per-client
acceptance gate before going live:
- List agents and their bindings; confirm the receptionist is bound to the inbound path and the master is not reachable from it.
- Inspect the receptionist's effective tool list — it CAN reach the calendar / DB tools, and CANNOT reach exec / financial tools.
- Run OpenClaw's security audit; verify the gateway binds loopback (not public).
- Run one full long-tool-call end to end (a 45-60 s lookup) and confirm the call survives and the agent speaks the result.
The exact OpenClaw config keys, CLI verbs, and audit commands are OpenClaw-side — confirm them against docs.openclaw.ai. Patter's contract is only the consult HTTP body and the named-agent
modelvalue.
Direction A — OpenClaw places calls via Patter (patter-mcp)
Use this when OpenClaw should initiate calls. Run the MCP server from a local
clone (it is not on npm) and register it in ~/.openclaw/openclaw.json. Prefer a
long-lived streamable-http server on an always-on box — OpenClaw reaps idle
stdio runtimes, which would re-spawn the embedded Patter server per session.
{
mcp: {
servers: {
patter: {
url: "http://localhost:3000/mcp",
transport: "streamable-http",
timeout: 600, // blocking make_call runs the whole call
toolFilter: {
// Narrow to the phone path — drop configure_inbound / get_metrics here.
include: ["make_call", "call_third_party", "get_transcript", "get_calls", "end_call"],
},
// Scope this server to the receptionist via the agent's per-agent tool
// policy (agents.list[].tools.allow + sandbox alsoAllow of "bundle-mcp").
// NOTE: mcp.servers.<name>.codex.agents projects ONLY to the Codex
// app-server runtime, NOT embedded-OpenClaw agents — don't rely on it here.
},
},
},
tools: {
sandbox: {
tools: {
// MCP tools live in the "bundle-mcp" group; allowlist it (or "patter__*").
alsoAllow: ["bundle-mcp"],
},
},
},
}
The seven patter-mcp tools: make_call, call_third_party, get_calls,
get_transcript, end_call, get_metrics, configure_inbound. With
wait: true, make_call / call_third_party block until the call ends and
return the outcome plus transcript in one response.
Prefer OAuth / mTLS over static env-var secrets committed in the config, and
verify reachability after wiring (openclaw mcp doctor patter --probe).
Gotchas
- Never set
model: "openclaw"/"openclaw/default"from the voice layer. It resolves to the default agent (possibly the master) and drifts between environments. Always pin"openclaw/<receptionistAgentId>". - The gateway credential is operator-grade. The
modelfield selects a persona, not a permission scope. Isolation = least-privileged agent + loopback binding + one-gateway-per-client, not the token. - Consult timeout defaults to 30 s — too short for 30-60 s automation. Set
timeout_s/timeoutMsto 75-90 s AND raise the OpenClaw gateway timeout. The call dies at the shortest link. - Reassurance is Realtime-only. Pipeline ignores it. Prefer Realtime for the receptionist line so the "let me check" filler actually plays.
- Raise the per-tool timeout on slow tools — both SDKs default to 10 s.
Since 0.6.4: Python
tool(..., timeout_s=60.0), TSToolDefinition.timeoutMs: 60_000(both clamped to 300 s; a timeout is terminal, not retried). allow_loopbackis the intended shape here, not a hack — on a co-located Mac Mini the adapter and gateway are on loopback. It relaxes only the consult URL's host check; non-HTTP(S) schemes are still rejected.- The adapter must forward
call_id/caller. The default doc adapter drops them; withoutuser=call_idevery consult starts a fresh OpenClaw session (no continuity) and caller-ID prefetch is impossible. echo_cancellationis a no-op on PSTN calls — Patter's AEC only applies to browser/native audio; the carrier handles line echo. The real noise levers areopenai_realtime_noise_reduction="far_field"+realtime_turn_detection(Realtime, 0.6.4+) anddenoiser/high_pass_hz/agc/ Silero VAD /turn_detector(Pipeline).patter-mcpis not on npm. Run it from a local clone ofPatterAI/patter-mcp.
Common errors
| Symptom | Fix |
|---|---|
| Call drops ~30 s into a tool/lookup | Consult/adapter timeout still at the 30 s default, or the OpenClaw gateway/MCP timeout is lower. Raise them to cover the work and probe with openclaw mcp doctor --probe to confirm the per-server timeout is honoured. |
| Caller hears dead air during a lookup | No reassurance firing. Use Realtime mode and either set reassurance on a slow tool or prompt the agent to say "let me check" before calling consult_agent. |
| Consult reaches the wrong / a privileged agent | Adapter sends model: "openclaw". Pin "openclaw/<receptionistAgentId>" and lock the agent's tools.allow/deny. |
ValueError: ConsultConfig url must be http(s) / host rejected | Loopback/private host with the SSRF guard on. Set allow_loopback=True / allowLoopback: true for your local adapter URL. |
| Agent cut off mid-sentence on a noisy line | Raise barge_in_threshold_ms to ~500. In Realtime add openai_realtime_noise_reduction="far_field" and raise the realtime_turn_detection threshold; in Pipeline set a denoiser and raise the Silero activation_threshold. |
| Multi-turn call has no memory of earlier turns | Adapter not sending user=call_id — every consult opens a new OpenClaw session. |
| OpenClaw agent can't see Patter's MCP tools (Direction A) | bundle-mcp not allowlisted in tools.sandbox.tools.alsoAllow, or timeout too low for a blocking call. |
| Inbound calls from unknown numbers rejected | You're using OpenClaw's native voice plugin (allowlist-only). Put Patter on the DID instead; it answers everyone. |
Related skills
setup-patter— install the SDK and provision provider/carrier keys first.build-voice-agent— define the in-call agent that does the consulting.add-tools-and-handoffs— custom tools, reassurance, guardrails,transfer_call/end_call.configure-telephony— wire the Twilio / Telnyx inbound webhook.inspect-calls-and-metrics— confirm consult latency and call outcomes.
References
- Patter OpenClaw integration: https://docs.getpatter.com/integrations/openclaw
- Patter consult feature: https://docs.getpatter.com/python-sdk/consult · https://docs.getpatter.com/typescript-sdk/consult
- Patter tools (reassurance, per-tool timeout): https://docs.getpatter.com/python-sdk/tools · https://docs.getpatter.com/typescript-sdk/tools
- Patter MCP server: https://github.com/PatterAI/patter-mcp
- OpenClaw docs (gateway, agents, MCP): https://docs.openclaw.ai