plugins.entries."msteams-bridge".config in your OpenClaw config. OpenClaw looks
configuration up by plugin id, so the entry key must be exactly msteams-bridge - under any other
key the plugin loads with no configuration at all and every call is refused.
The plugin serves two lanes: calling (the media WebSocket StandIn dials) and messages (the
managed chat relay). Set secret and you configure both. It must match the connection secret shown
in your StandIn dashboard.
Minimal working config (start here)
Everything else on this page is tuning. This is the smallest config that answers a Teams call and talks back, verified end to end against a live call:port 9442 and path /msteams/calling for the calling lane;
messagesPort 9444 and messagesPath /msteams/messages for the managed-bot chat relay.
Teams chat on a bring-your-own Azure bot does not come through this plugin: it reaches OpenClaw’s
own msteams channel at 127.0.0.1:3978, path /api/messages. So the two paths you publish are
/msteams/calling (to 9442) and /api/messages (to 3978) - see
Expose your agent for the mount commands.
Realtime provider options
Everything insiderealtime.providers.<name> is passed to the voice provider. The full known set -
only apiKey is required (plus the three azure* keys on Azure):
Spelling matters: from plugin v0.4.1 the known keys are schema-checked, so a typo like
azureEndpont fails validation instead of surfacing as a runtime mystery.
You do not need the voice-call plugin
voice-call is OpenClaw’s telephony plugin (Twilio, fromNumber, webhook URLs). This one is
separate and self-contained: install msteams-bridge and nothing else.
If you are moving from an older setup, note the Teams settings used to live nested inside it as
plugins.entries."voice-call".config.msteams. Lift that block up to
plugins.entries."msteams-bridge".config and delete the voice-call entry - leaving it behind does no
harm, but it configures a phone system you are not using.
Four things that bite on a first install
Two models, two places
This trips people up because the failure looks like a call bug, not a config one.
The agent model is inherited. You do not configure it here, and in a normal install there is
nothing to set: the plugin reads whatever your OpenClaw agent already uses.
responseModel is an optional override, for when the voice lane should consult a different model
than your default agent. Resolution order:
responseModel, if you set itagents.defaults.model.primary- the normal case- OpenClaw’s built-in default, only if you have configured no agent at all
Vision: the agent sees your screen and camera
Share your screen (or turn your camera on) in the call, and the agent can look at it: ask “what’s on my screen?”, “read this error”, or “what am I holding?”. StandIn’s hosted bridge samples what is shown and delivers frames to your plugin; the agent looks on demand with itslook_at_screen tool.
Ask about something shown earlier - “what did the previous slide say?” - and the agent reviews
its recent scene-change keyframes instead of only the live frame.
When the agent sees. On a published OpenClaw host, frames are delivered with the agent’s next
turn - so it sees your screen whenever it reasons, which covers every question you ask it. Truly
continuous awareness between turns needs a realtime image channel no published host exposes yet;
the plugin uses it automatically if your host gains one. The gateway log says which route it took at
the start of each call.
Debugging: “the agent says it can’t see”
The agent’s own wording tells you which layer to look at. Work down this ladder - each answer points at a different cause:- “I can’t see anything yet - make sure your camera or screen-share is on.” No frame has arrived at the plugin. Start (or restart) the share; it can take a few seconds after sharing starts. If it never arrives, confirm the call actually connected through StandIn (you hear the agent) and that your Agent calling URL points at this plugin instance.
- “I’m not receiving the shared-screen image.” Frames arrived but were not attached to the agent’s look. That is the pre-0.4.1 bug - upgrade the plugin and restart the gateway.
-
“I’ve been looking quite a lot in the last minute…”
The
maxVisionPerMinutebudget. Wait a few seconds or raise the cap. -
The agent never mentions seeing at all, or refuses.
Check
realtime.toolPolicyis not"none". -
In a MEETING, the agent neither hears nor sees until recording starts.
That is
requireRecordingStatus: truedoing its job: a compliance gate that holds all media until Teams reports recording active. Start recording in the meeting, or set the key tofalsewhere your policy allows. 1:1 direct calls are not affected - the gate opens on connect. - Mid-call lookups fail generally (not just vision), e.g. an “Unknown model” error in the gateway log. The agent model, not vision - see Two models, two places.
- The gateway terminal. A healthy look logs nothing dramatic; failures name themselves
(
look failed for <callId> - ...,invalid message from <callId>: ...). - The agent session log -
~/.openclaw/agents/<agentId>/sessions/*.jsonl. Find the newest file and look for the consult turns: a working look shows the user turn carrying image parts; a broken one shows text only. This one check separates “frames are not arriving” from “frames are arriving but not attached”, which sound identical in the agent’s spoken answer.
Restart the gateway after upgrading. OpenClaw loads plugin code at startup; an upgrade on disk
changes nothing until the gateway restarts. If a fix “didn’t work”, check this first.
Voice messages
Someone sends the bot a Teams voice note. By default the agent sees only that an attachment exists - it can name the file back and nothing else, which reads as being ignored. Turn transcription on and the clip is transcribed into the turn, so “listen to this and tell me what you think” is a question the agent can actually answer:Off by default on purpose. Each clip is a speech-to-text call and a voice note can run for
minutes, so this is real per-message cost - something to opt into, not discover on a bill. It also
needs the messages lane configured (the
secret), since voice notes arrive as chat attachments.Mode selection
Setmode to "realtime" or "streaming". If omitted, the runtime auto-selects realtime when a
realtime provider resolves, else streaming. Both modes honor the inbound allowlist, outbound
call-backs, recording gate, and sessionScope agent memory. See Modes.
stt.provider / stt.providers if set, else your
openclaw-configured transcription provider; if none resolves it falls back to VAD-segmented file
transcription. The realtime.* block is ignored except the echo-guard knobs
(suppressInputDuringPlayback, echoSuppressionWindowMs, echoBargeInRms), which apply in both modes.
Outbound call-backs (optional, either mode)
openclaw_agent_task takes deliverVia. The default,
"message", sends the result as a Teams chat message. Set "call" and the agent rings the caller back
and speaks it once the work is finished.
Chat-to-call. Ask in a Teams chat and get the answer as a phone call, via the
call_me_with_the_answer tool. It rings the person it is chatting with - always, and only. There is no
target parameter, deliberately: the agent reads untrusted text all day, and a tool that accepted an
arbitrary user id would turn any of it into “ring this person”. Requires the messages lane too.
workerBaseUrl is StandIn’s outbound API URL from your dashboard - not a server you host.Needs v0.4.0+. Earlier versions accepted
deliverVia: "call" and acknowledged it to the caller,
but never placed a call - the delivery was routed to a tool that does not exist in OpenClaw. Nothing
errored; the caller simply waited for a call that was never made.Key reference
What
inboundPolicy: "pairing" actually does today. The plugin issues no pairing codes,
expirations, or approval prompts for inbound calls: with "pairing", a caller is accepted only
if their AAD object id (or phone number) is already in allowFrom - exactly the same check as
"allowlist". A rejected caller is logged with their caller id and the call is closed. The
openclaw pairing approve msteams ... flow you may know from the chat channel manages chat DM
approvals only; it does not populate this voice allowFrom. Until a real call-pairing flow ships,
prefer "allowlist" and manage allowFrom yourself; "pairing" is accepted so such configs keep
working with identical (allowlist) behavior.