Install
$ agentstack add skill-patterai-skills-integrate-openclaw ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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/"
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,
},
},
},
},
}
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/completions to 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.
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/"→ one explicit, named agent. **Always pin
this.** (Alias forms openclaw: / agent: 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/"). - Carry
call_idas the OpenClaw session key so a multi-turn call maps to
one OpenClaw session (continuity), and forward caller for 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 "}, # 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 " }, // 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 --verbose shows 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
};
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.
3. Per-tool timeout
- TypeScript:
ToolDefinition.timeoutMs(default 10 000 ms, clamped to
300 000 ms). Raise it to 60_000 for slow tools — shown above. A ti
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: PatterAI
- Source: PatterAI/skills
- License: MIT
- Homepage: https://www.skills.sh/patterai/skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.