Google Meet Plugin: Join Meet URLs via Chrome or Twilio
Learn how the google-meet plugin joins explicit Meet URLs for OpenClaw agents, including modes for agent talk-back, bidi, and observe-only. For developers integrating teleconference capabilities.
Read this when
- You want an OpenClaw agent to join a Google Meet call
- You want an OpenClaw agent to create a new Google Meet call
- You are configuring Chrome, Chrome node, or Twilio as a Google Meet transport
The google-meet plugin handles joining explicit Meet URLs on behalf of an OpenClaw agent. Its scope is intentionally limited:
- Only
https://meet.google.com/...URLs are joined; phone numbers discovered on its own are never dialed. - A fresh Meet URL can be generated by
googlemeet createvia the Google Meet API, with a browser fallback available, and then joined as the default action. - Chrome-based participation relies on a signed-in Chrome profile, which may live on a paired node. Twilio participation reaches a phone number plus PIN/DTMF through the Voice call plugin; direct dialing of a Meet URL is not supported.
- In
mode: "agent"mode, which is the default, a realtime provider transcribes participant speech, sends it to the configured OpenClaw agent, and the answer is spoken via standard OpenClaw TTS.mode: "bidi"allows a realtime voice model to respond directly.mode: "transcribe"joins in observe-only fashion, with no talk-back capability. - No automatic consent announcement occurs when the plugin enters a call.
- The CLI command is
googlemeet;meetis set aside for broader agent teleconference workflows.
Quick start
Start by installing the plugin along with the native audio dependencies for the Chrome host, then configure a realtime provider key. OpenAI serves as the default transcription provider for agent mode; Google Gemini Live can act as the voice provider for bidi mode. On macOS:
openclaw plugins install npm:@openclaw/google-meet
brew install blackhole-2ch sox
export OPENAI_API_KEY=sk-...
# only needed when realtime.voiceProvider is "google" for bidi mode
export GEMINI_API_KEY=...
The BlackHole 2ch virtual audio device that Chrome routes through is installed by blackhole-2ch. A reboot is mandatory after Homebrew's installer finishes before macOS will expose the device:
sudo reboot
After the reboot, confirm both components:
system_profiler SPAudioDataType | grep -i BlackHole
command -v sox
For a Linux desktop running PipeWire-Pulse:
sudo apt install pipewire-audio pulseaudio-utils # Debian/Ubuntu
systemctl --user --now enable pipewire pipewire-pulse wireplumber
pactl info
command -v pactl pacat parec
Within that desktop user's audio session, OpenClaw sets up an OpenClaw Meeting Audio null sink and a matching source. The Gateway or paired node must run under the same user account that launches Chrome.
Once installed, the plugin is active by default. Only add an entry when customization is needed:
{
plugins: {
entries: {
"google-meet": {
config: {},
},
},
},
}
To disable the plugin, run openclaw plugins disable google-meet.
Verify the setup, then join:
openclaw googlemeet setup
openclaw googlemeet join https://meet.google.com/abc-defg-hij
Output from setup is designed for agent consumption and reflects both mode and transport: it indicates the Chrome profile, node pinning, and, for realtime Chrome joins, the native virtual-audio backend and the delayed-intro check. Observe-only joins bypass realtime prerequisites:
openclaw googlemeet setup --transport chrome-node --mode transcribe
With Twilio delegation configured, setup also indicates whether voice-call, Twilio credentials, and public webhook exposure are all in place. Before an agent joins, treat any ok: false check as a blocker for that transport or mode. Machine-readable output comes from --json, while --transport chrome|chrome-node|twilio lets you preflight a specific transport in advance:
openclaw googlemeet setup --transport twilio
Alternatively, an agent can join through the google_meet tool:
{
"action": "join",
"url": "https://meet.google.com/abc-defg-hij",
"transport": "chrome-node",
"mode": "agent"
}
Talk-back on local Chrome is supported on macOS with BlackHole 2ch and SoX, or on Linux with PipeWire-Pulse and pactl/pacat/parec. For other operating systems, turn to mode: "transcribe", Twilio dial-in, or a supported macOS/Linux chrome-node host.
Create a meeting
openclaw googlemeet create --transport chrome-node --mode agent
openclaw googlemeet create --no-join
Two distinct paths exist for create, and the result's source field reports which one was taken:
api: employed when Google Meet OAuth credentials are configured. This path is deterministic and independent of browser UI state.browser: used when OAuth credentials are absent. OpenClaw launcheshttps://meet.google.com/newon the pinned Chrome node and waits for Google to redirect to a genuine meeting-code URL; the OpenClaw Chrome profile on that node must already be signed into Google. Both join and create reuse an existing Meet tab, or an in-progress.../newor Google account prompt tab, before opening a new one; tab matching ignores benign query strings such asauthuser.
By default, create joins and returns joined: true along with the join session. To mint only the URL, pass --no-join on the CLI or "join": false to the tool.
For rooms created through the API, set an explicit access policy rather than relying on the Google account default:
openclaw googlemeet create --access-type OPEN --transport chrome-node --mode agent
--access-type | Who can join without knocking |
|---|---|
OPEN | Anyone with the Meet URL |
TRUSTED | Host org's trusted users, invited external users, and dial-in users |
RESTRICTED | Invitees only |
This policy applies solely to API-created rooms, so OAuth configuration is a prerequisite. If you authenticated before this option was introduced, rerun openclaw googlemeet auth login --json after adding the meetings.space.settings scope to your OAuth consent screen.
When the browser fallback encounters a Google sign-in or Meet permission blocker, the tool returns manualAction: { reason, message } together with browser.nodeId/browser.targetId/browserUrl. Pass that message along and hold off on opening additional Meet tabs until the operator has completed the browser step.
Observe-only join
Configure "mode": "transcribe" to bypass the duplex realtime bridge, which removes the virtual-audio requirement and the talk-back feature. Chrome joins in transcribe mode also skip OpenClaw's microphone and camera permission grant as well as the Meet Use microphone flow; if Meet presents the audio-choice interstitial, automation first attempts Continue without microphone. Managed Chrome transports install a best-effort Meet caption observer across all modes, so durable notes remain available without altering the live agent-consult path. googlemeet status --json and googlemeet doctor report captioning, captionsEnabledAttempted, transcriptLines, lastCaptionAt, lastCaptionSpeaker, lastCaptionText, and a recentTranscript tail.
For the bounded session transcript, read the exact tracked Meet tab:
openclaw googlemeet transcript <session-id>
openclaw googlemeet transcript <session-id> --since <next-index> --json
The observer retains at most 2,000 completed caption lines within the Meet page. Visible progressive text remains in the status health tail until the caption row completes, meaning saving nextIndex cannot bypass a later text expansion; leaving finalizes visible rows before the snapshot. droppedLines reports lines lost from the head when the cap is exceeded. The bounded googlemeet transcript tail still keeps only the four most recently ended sessions and resets with the Gateway. Separately, OpenClaw appends completed caption rows to the shared state database throughout the meeting and writes a derived summary on leave. Use openclaw transcripts to inspect or export those durable notes.
Automatic notes are enabled by default. Set transcripts.enabled: false to
disable durable notes globally; explicit transcribe mode still exposes only
its bounded live tail. Twilio joins do not have the browser caption stream and
are not captured by this path.
For a yes/no listen probe:
openclaw googlemeet test-listen <meet-url> --transport chrome-node
It joins in transcribe mode, waits for fresh caption/transcript movement, and returns listenVerified, listenTimedOut, manual-action fields, and current caption health.
Realtime session health
During talk-back sessions, google_meet status reports Chrome/audio bridge health: inCall, manualAction, providerConnected, realtimeReady, audioInputActive, audioOutputActive, last input/output timestamps, byte counters, and bridge-closed state. Managed Chrome sessions only speak the intro/test phrase after health reports inCall: true; otherwise speechReady: false and the speech attempt is blocked rather than silently no-opping.
Local Chrome joins through the signed-in OpenClaw browser profile and routes its microphone and speaker through the native backend selected by chrome.audioBackend. The default shared loopback device is enough for a first smoke test but can echo; use separate virtual devices or a Loopback-style graph for clean duplex audio.
Local Gateway + Parallels Chrome
A full Gateway or model API key is not required inside a macOS VM just to give it Chrome. Run the Gateway and agent locally; run a node host in the VM.
| Runs where | What |
|---|---|
| Gateway host | OpenClaw Gateway, agent workspace, model/API keys, realtime provider, Google Meet plugin config |
| Parallels macOS VM | OpenClaw CLI/node host, Chrome, SoX, BlackHole 2ch, a Chrome profile signed in to Google |
| Not needed in the VM | Gateway service, agent config, model provider setup |
Install VM dependencies, reboot, verify:
brew install blackhole-2ch sox
sudo reboot
system_profiler SPAudioDataType | grep -i BlackHole
command -v sox
Install the plugin in the VM, where it is enabled by default, and start the node host:
openclaw plugins install npm:@openclaw/google-meet
openclaw node run --host <gateway-host> --port 18789 --display-name parallels-macos
If <gateway-host> is a LAN IP without TLS, opt in for that trusted private network:
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \
openclaw node run --host <gateway-lan-ip> --port 18789 --display-name parallels-macos
Use the same flag when installing as a LaunchAgent (it is process environment, stored in the LaunchAgent environment when present on the install command, not an openclaw.json setting):
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \
openclaw node install --host <gateway-lan-ip> --port 18789 --display-name parallels-macos --force
openclaw node restart
Approve the node from the Gateway host, then confirm it advertises both googlemeet.chrome and browser capability/browser.proxy:
openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status
Route Meet through that node:
{
gateway: {
nodes: {
commands: { allow: ["googlemeet.chrome", "browser.proxy"] },
},
},
plugins: {
entries: {
"google-meet": {
enabled: true,
config: {
defaultTransport: "chrome-node",
chrome: {
guestName: "OpenClaw Agent",
autoJoin: true,
reuseExistingTab: true,
},
chromeNode: {
node: "parallels-macos",
},
},
},
},
},
}
Now join normally from the Gateway host:
openclaw googlemeet join https://meet.google.com/abc-defg-hij
For a one-command smoke test that creates or reuses a session, speaks a known phrase, and prints session health:
openclaw googlemeet test-speech https://meet.google.com/abc-defg-hij
During realtime join, browser automation fills the guest name, clicks Join/Ask to join, and accepts Meet's first-run "Use microphone" prompt when it appears (or "Continue without microphone" during observe-only join and browser-only meeting creation). If the profile is signed out, Meet is waiting for host admission, Chrome needs mic/camera permission, or Meet is stuck on an unresolved prompt, the result includes manualAction: { reason, message }. Stop retrying, report that message plus browserUrl/browserTitle, and retry only after the manual action completes.
If chromeNode.node is omitted, OpenClaw auto-selects only when exactly one connected node advertises both googlemeet.chrome and browser control; pin chromeNode.node (node id, display name, or remote IP) when several capable nodes are connected.
Common failure checks
| Symptom | Fix |
|---|---|
Configured Google Meet node ... is not usable: offline | The pinned node is recognized but unreachable. Report the setup blocker; do not quietly switch to another transport unless requested. |
No connected Google Meet-capable node | Set up npm:@openclaw/google-meet inside the VM, execute openclaw plugins enable browser, launch openclaw node run, and confirm the pairing. If Google Meet was turned off, turn it back on. Make sure gateway.nodes.commands.allow contains both googlemeet.chrome and browser.proxy. |
BlackHole 2ch audio device not found | On macOS, put blackhole-2ch on the host under inspection and restart it. |
PipeWire-Pulse is unavailable | On Linux, begin the desktop user's pipewire-pulse service and add pulseaudio-utils; avoid running the node as root or outside the Chrome user's audio session. |
| Chrome opens but cannot join | Log into the browser profile inside the VM, or leave chrome.guestName enabled. Guest auto-join relies on OpenClaw browser automation via the node browser proxy; aim the node's browser.defaultProfile (or a named existing-session profile) at the desired profile. |
| Duplicate Meet tabs | Keep chrome.reuseExistingTab: true as is. OpenClaw brings up an existing tab for the same URL, and creation reuses an ongoing .../new or Google account prompt tab, before spawning another. |
| No audio | Send Meet mic/speaker through the virtual audio path OpenClaw uses; employ separate virtual devices or Loopback-style routing for clean duplex audio. |
Install notes
The Chrome talk-back default depends on host audio tools that OpenClaw does not include or distribute:
sox: command-line audio utility. The plugin issues explicit CoreAudio device commands for the default 24 kHz PCM16 audio bridge.blackhole-2ch: macOS virtual audio driver providing theBlackHole 2chdevice Chrome/Meet route through.pactl,pacat, andparec: Linux PulseAudio utilities used against PipeWire-Pulse to provision and stream throughOpenClaw Meeting Audio.
SoX is licensed LGPL-2.0-only AND GPL-2.0-only; BlackHole is GPL-3.0. If you build an installer or appliance that bundles BlackHole with OpenClaw, review BlackHole's upstream licensing or get a separate license from Existential Audio.
Transports
| Transport | Use when |
|---|---|
chrome | Chrome/audio live on the Gateway host |
chrome-node | Chrome/audio live on a paired node (for example a Parallels macOS VM) |
twilio | Phone dial-in fallback via the Voice Call plugin, when Chrome participation is not available |
Chrome
Opens the Meet URL through OpenClaw browser control and joins as the signed-in OpenClaw browser profile. Before launch, the plugin checks or provisions the host's native virtual-audio backend and then runs any configured audio bridge health/startup command. For local Chrome, pick the profile with browser.defaultProfile; chrome.browserProfile is passed to chrome-node hosts instead.
openclaw googlemeet join https://meet.google.com/abc-defg-hij --transport chrome
openclaw googlemeet join https://meet.google.com/abc-defg-hij --transport chrome-node
Chrome mic/speaker audio routes through the local OpenClaw audio bridge. If the native backend is unavailable, the join fails with a setup error instead of joining without an audio path.
Twilio
A strict dial plan delegated to the Voice call plugin. It does not parse Meet pages for phone numbers; Google Meet must expose a phone dial-in number and PIN for the meeting.
Enable Voice Call on the Gateway host, not the Chrome node:
{
plugins: {
allow: ["google-meet", "voice-call", "google"],
entries: {
"google-meet": {
enabled: true,
config: {
defaultTransport: "chrome-node",
// or set "twilio" if Twilio should be the default
},
},
"voice-call": {
enabled: true,
config: {
provider: "twilio",
inboundPolicy: "allowlist",
realtime: {
enabled: true,
provider: "google",
instructions: "Join this Google Meet as an OpenClaw agent. Be brief.",
toolPolicy: "safe-read-only",
providers: {
google: {
silenceDurationMs: 500,
startSensitivity: "high",
},
},
},
},
},
google: {
enabled: true,
},
},
},
}
Provide Twilio credentials through environment to keep secrets out of openclaw.json:
export TWILIO_ACCOUNT_SID=AC...
export TWILIO_AUTH_TOKEN=...
export TWILIO_FROM_NUMBER=+15550001234
export GEMINI_API_KEY=...
Use realtime.provider: "openai" with OPENAI_API_KEY instead if OpenAI is the realtime voice provider.
Restart or reload the Gateway after enabling voice-call; plugin config changes do not take effect until reload. Verify:
openclaw config validate
openclaw plugins list | grep -E 'google-meet|voice-call'
openclaw googlemeet setup
When Twilio delegation is wired, googlemeet setup includes twilio-voice-call-plugin, twilio-voice-call-credentials, and twilio-voice-call-webhook checks.
openclaw googlemeet join https://meet.google.com/abc-defg-hij \
--transport twilio \
--dial-in-number +15551234567 \
--pin 123456
Use --dtmf-sequence for a custom sequence, with leading w or commas for a pause before the PIN:
openclaw googlemeet join https://meet.google.com/abc-defg-hij \
--transport twilio \
--dial-in-number +15551234567 \
--dtmf-sequence ww123456#
OAuth and preflight
OAuth is optional for creating a Meet link, because googlemeet create can fall back to browser automation. Configure OAuth for official API create, space resolution, or Meet Media API preflight. Chrome/Chrome-node joins never depend on OAuth; they use a signed-in Chrome profile, the host's native virtual-audio backend, and (for chrome-node) a connected node either way.
Create Google credentials
In Google Cloud Console:
Create or select a project
Enable the Google Meet REST API
Configure the OAuth consent screen
Internal is simplest for a Google Workspace organization. External works for personal/test setups; while the app is in Testing, add each Google account that will authorize it as a test user.
Add the requested scopes
https://www.googleapis.com/auth/meetings.space.createdhttps://www.googleapis.com/auth/meetings.space.readonlyhttps://www.googleapis.com/auth/meetings.space.settingshttps://www.googleapis.com/auth/meetings.conference.media.readonlyhttps://www.googleapis.com/auth/calendar.events.readonly(Calendar lookup)https://www.googleapis.com/auth/drive.meet.readonly(transcript/smart-note document body export)
Create an OAuth client ID
Application type Web application. Authorized redirect URI:
http://localhost:8085/oauth2callback
Copy the client ID and client secret
meetings.space.created is a dependency of spaces.create. To turn Meet URLs or codes into spaces, meetings.space.readonly handles that resolution. Through meetings.space.settings, OpenClaw can forward SpaceConfig options like accessType when creating rooms via the API. For preflight checks and media operations with the Meet Media API, meetings.conference.media.readonly is used, and Developer Preview enrollment might be needed by Google for actual Media API usage. Only when performing --today/--event calendar lookups is calendar.events.readonly necessary. drive.meet.readonly applies solely to --include-doc-bodies export scenarios. If your only requirement is browser-based Chrome joins, you can skip OAuth completely.
Mint the refresh token
Set up oauth.clientId and, if desired, oauth.clientSecret (alternatively, supply them through environment variables), then execute:
openclaw googlemeet auth login --json
A PKCE flow runs with a localhost callback on http://localhost:8085/oauth2callback, producing an oauth config block that contains a refresh token. When the browser cannot reach the local callback, add --manual to enable a copy/paste flow:
OPENCLAW_GOOGLE_MEET_CLIENT_ID="your-client-id" \
OPENCLAW_GOOGLE_MEET_CLIENT_SECRET="your-client-secret" \
openclaw googlemeet auth login --json --manual
JSON output:
{
"oauth": {
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"refreshToken": "refresh-token",
"accessToken": "access-token",
"expiresAt": 1770000000000
},
"scope": "..."
}
Place the oauth object into the plugin config as shown:
{
plugins: {
entries: {
"google-meet": {
enabled: true,
config: {
oauth: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
refreshToken: "refresh-token",
},
},
},
},
},
}
If keeping the refresh token out of config is preferred, environment variables are the better choice; resolution checks config first, then falls back to environment. Should you have authenticated before meeting creation, calendar lookup, or document-body export support were introduced, rerun openclaw googlemeet auth login --json so the refresh token matches the current scope set.
Verify OAuth with doctor
openclaw googlemeet doctor --oauth --json
This verifies that OAuth config is present and that the refresh token can generate an access token, all without loading the Chrome runtime or needing a connected node. Only status fields appear in the report (ok, configured, tokenSource, expiresAt, check messages), and neither the access token, refresh token, nor client secret is ever displayed.
| Check | Meaning |
|---|---|
oauth-config | oauth.clientId combined with oauth.refreshToken, or a cached access token, exists |
oauth-token | The cached access token remains valid, or the refresh token produced a new one |
meet-spaces-get | An existing Meet space was resolved by the optional --meeting check |
meet-spaces-create | A new Meet space was created by the optional --create-space check |
To demonstrate that the Meet API is enabled and the spaces.create scope is granted, run the side-effecting create check:
openclaw googlemeet doctor --oauth --create-space --json
To prove read access to an existing space:
openclaw googlemeet doctor --oauth --meeting https://meet.google.com/abc-defg-hij --json
openclaw googlemeet resolve-space --meeting https://meet.google.com/abc-defg-hij
When these checks return a 403, the usual causes are a disabled Meet REST API, a refresh token lacking the required scope, or the Google account being unable to access that space. If a refresh-token error appears, rerun openclaw googlemeet auth login --json and save the new oauth block.
The browser fallback needs no OAuth; authentication for Google comes from the signed-in Chrome profile on the chosen node, not from OpenClaw configuration.
These environment variables serve as fallbacks:
OPENCLAW_GOOGLE_MEET_CLIENT_IDorGOOGLE_MEET_CLIENT_IDOPENCLAW_GOOGLE_MEET_CLIENT_SECRETorGOOGLE_MEET_CLIENT_SECRETOPENCLAW_GOOGLE_MEET_REFRESH_TOKENorGOOGLE_MEET_REFRESH_TOKENOPENCLAW_GOOGLE_MEET_ACCESS_TOKENorGOOGLE_MEET_ACCESS_TOKENOPENCLAW_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_ATorGOOGLE_MEET_ACCESS_TOKEN_EXPIRES_ATOPENCLAW_GOOGLE_MEET_DEFAULT_MEETINGorGOOGLE_MEET_DEFAULT_MEETINGOPENCLAW_GOOGLE_MEET_PREVIEW_ACKorGOOGLE_MEET_PREVIEW_ACK
Resolve, preflight, and read artifacts
openclaw googlemeet resolve-space --meeting https://meet.google.com/abc-defg-hij
openclaw googlemeet preflight --meeting https://meet.google.com/abc-defg-hij
Once Meet has generated conference records:
openclaw googlemeet artifacts --meeting https://meet.google.com/abc-defg-hij
openclaw googlemeet attendance --meeting https://meet.google.com/abc-defg-hij
openclaw googlemeet export --meeting https://meet.google.com/abc-defg-hij --output ./meet-export
When --meeting, artifacts, and attendance are used, the most recent conference record is selected by default; supply --all-conference-records to apply this to every stored record.
Before reading artifacts, calendar lookup pulls the meeting URL from Google Calendar, which needs a refresh token with the Calendar events readonly scope:
openclaw googlemeet latest --today
openclaw googlemeet calendar-events --today --json
openclaw googlemeet artifacts --event "Weekly sync"
openclaw googlemeet attendance --today --format csv --output attendance.csv
--today scans today's primary calendar for an event containing a Meet link; --event <query> looks for matching event text; --calendar <id> points to a non-primary calendar. calendar-events shows matching events and indicates which one latest/artifacts/attendance/export will pick.
To reference a conference record directly when its id is known:
openclaw googlemeet latest --meeting https://meet.google.com/abc-defg-hij
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --json
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
To close an API-created space:
openclaw googlemeet end-active-conference https://meet.google.com/abc-defg-hij
This invokes spaces.endActiveConference and demands OAuth with the meetings.space.created scope for a space the authorized account controls. It takes a Meet URL, meeting code, or spaces/{id} and first maps it to the API space resource. It differs from googlemeet leave: leave halts OpenClaw's local or session involvement; end-active-conference requests that Google Meet terminate the active conference for the space.
Generate a readable report:
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 \
--format markdown --output meet-artifacts.md
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 \
--format csv --output meet-attendance.csv
openclaw googlemeet export --conference-record conferenceRecords/abc123 \
--include-doc-bodies --zip --output meet-export
openclaw googlemeet export --conference-record conferenceRecords/abc123 \
--include-doc-bodies --dry-run
artifacts delivers conference record metadata along with participant, recording, transcript, structured transcript-entry, and smart-note resource metadata when Google provides it. For large meetings, --no-transcript-entries skips entry lookup. attendance expands participants into participant-session rows that include first and last seen times, total session duration, late or early-leave flags, and duplicate participant resources merged by signed-in user or display name; --no-merge-duplicates keeps raw resources separate, while --late-after-minutes/--early-before-minutes adjust the thresholds.
export creates a folder containing summary.md, attendance.csv, transcript.md, artifacts.json, attendance.json, and manifest.json. manifest.json captures the selected input, export options, conference records, output files, counts, token source, any Calendar event used, and partial-retrieval warnings. --zip additionally writes a portable archive next to the folder. --include-doc-bodies exports linked transcript or smart-note Google Docs text via Drive files.export, which requires the Drive Meet readonly scope; without it, exports contain only Meet metadata and structured transcript entries. A partial artifact failure, such as a smart-note listing, transcript-entry, or document-body error, logs the warning in the summary or manifest rather than aborting the entire export. --dry-run retrieves the same data and outputs the manifest JSON without generating the folder or ZIP.
Agents rely on the same set of actions through the google_meet tool (export, create paired with accessType, end_active_conference, test_listen); refer to Tool for details.
Live smoke test
OPENCLAW_LIVE_TEST=1 \
OPENCLAW_GOOGLE_MEET_LIVE_MEETING=https://meet.google.com/abc-defg-hij \
pnpm test:live -- extensions/google-meet/google-meet.live.test.ts
openclaw googlemeet setup --transport chrome-node --mode transcribe
openclaw googlemeet test-listen https://meet.google.com/abc-defg-hij --transport chrome-node --timeout-ms 30000
| Variable | Purpose |
|---|---|
OPENCLAW_LIVE_TEST=1 | Turns on guarded live tests |
OPENCLAW_GOOGLE_MEET_LIVE_MEETING | Stores the Meet URL, code, or spaces/{id} that was kept |
OPENCLAW_GOOGLE_MEET_CLIENT_ID / GOOGLE_MEET_CLIENT_ID | OAuth client identifier |
OPENCLAW_GOOGLE_MEET_REFRESH_TOKEN / GOOGLE_MEET_REFRESH_TOKEN | Refresh token |
OPENCLAW_GOOGLE_MEET_CLIENT_SECRET, OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN, OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT | Not required; the same fallback names minus the OPENCLAW_ prefix also work |
For the base artifact/attendance smoke test, meetings.space.readonly and meetings.conference.media.readonly are required. To look up the calendar, calendar.events.readonly is needed. Exporting the Drive document body requires drive.meet.readonly.
Create examples
openclaw googlemeet create
Outputs the new meeting URI, its source, and the join session. With OAuth in place, the Meet API is used; otherwise, the signed-in profile of the pinned Chrome node takes over. Browser fallback JSON:
{
"source": "browser",
"meetingUri": "https://meet.google.com/abc-defg-hij",
"joined": true,
"browser": {
"nodeId": "ba0f4e4bc...",
"targetId": "tab-1"
},
"join": {
"session": {
"id": "meet_...",
"url": "https://meet.google.com/abc-defg-hij"
}
}
}
When the browser fallback first encounters a Google login prompt or a Meet permission blocker, google_meet provides structured details rather than a plain string:
{
"source": "browser",
"error": "google-login-required: Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
"manualAction": {
"reason": "google-login-required",
"message": "Sign in to Google in the OpenClaw browser profile, then retry meeting creation."
},
"browser": {
"nodeId": "ba0f4e4bc...",
"targetId": "tab-1",
"browserUrl": "https://accounts.google.com/signin",
"browserTitle": "Sign in - Google Accounts"
}
}
API create JSON:
{
"source": "api",
"meetingUri": "https://meet.google.com/abc-defg-hij",
"joined": true,
"space": {
"name": "spaces/abc-defg-hij",
"meetingCode": "abc-defg-hij",
"meetingUri": "https://meet.google.com/abc-defg-hij"
},
"join": {
"session": {
"id": "meet_...",
"url": "https://meet.google.com/abc-defg-hij"
}
}
}
Creation joins by default, but Chrome and Chrome-node still demand a signed-in Google profile for browser-based joining; if signed out, OpenClaw responds with manualAction or a browser fallback error and instructs the operator to complete Google login before trying again.
Set preview.enrollmentAcknowledged: true only once you have verified that your Cloud project, OAuth principal, and meeting participants are all part of the Google Workspace Developer Preview Program for Meet media APIs.
Config
The standard Chrome agent path requires just the plugin enabled, BlackHole, SoX, a realtime provider key, and a configured OpenClaw TTS provider:
{
plugins: {
entries: {
"google-meet": {
enabled: true,
config: {},
},
},
},
}
Defaults
| Key | Default | Notes |
|---|---|---|
defaultTransport | "chrome" | |
defaultMode | "agent" | "realtime" remains valid as an older name for "agent"; use "agent" in new code |
chromeNode.node | unset | Node id, name, or IP for chrome-node; mandatory when more than one capable node could be attached |
chrome.launch | true | Starts Chrome to join; only set false when you are reusing a session that is already open |
chrome.audioBackend | "auto" | Picks blackhole-2ch on macOS or pipewire-pulse on Linux; specify a backend explicitly when a paired Chrome node runs a different OS than the Gateway |
chrome.guestName | "OpenClaw Agent" | Displayed on the Meet guest screen when signed out |
chrome.autoJoin | true | Attempts to prefill the guest name and press Join Now on chrome-node |
chrome.reuseExistingTab | true | Reuses an existing Meet tab rather than spawning new ones |
chrome.waitForInCallMs | 20000 | Waits for the Meet tab to signal in-call status before the talk-back intro plays |
chrome.audioFormat | "pcm16-24khz" | Audio format for command pairs; "g711-ulaw-8khz" is meant only for legacy or custom pairs that output telephony audio |
chrome.audioBufferBytes | 4096 | Processing buffer for calculating generated command latency; values are capped at a 17-byte minimum |
chrome.audioInputCommand | generated native command | SoX or CoreAudio on macOS; parec from the OpenClaw PipeWire-Pulse source on Linux |
chrome.audioOutputCommand | generated native command | SoX or CoreAudio on macOS; pacat into the OpenClaw PipeWire-Pulse sink on Linux |
chrome.bargeInInputCommand | unset | Optional local mic command that writes signed 16-bit little-endian mono PCM for detecting human interruptions during assistant playback; relevant to the Gateway-hosted command-pair bridge |
chrome.bargeInRmsThreshold | 650 | RMS level treated as a human interruption |
chrome.bargeInPeakThreshold | 2500 | Peak level treated as a human interruption |
chrome.bargeInCooldownMs | 900 | Minimum gap between successive interruption clears |
mode (per-request) | "agent" | Talk-back mode; consult the Agent and bidi modes table |
realtime.provider | "openai" | Compatibility fallback applied when the scoped fields below are not set |
realtime.transcriptionProvider | "openai" | Provider id that agent mode uses for realtime transcription |
realtime.voiceProvider | unset | Provider id that bidi mode uses for direct realtime voice; assign "google" for Gemini Live while agent-mode transcription stays on OpenAI. Combine with realtime.model to choose the exact Gemini Live model. |
realtime.toolPolicy | "safe-read-only" | Refer to Agent and bidi modes |
realtime.instructions | brief spoken-reply instructions | Directs the model to keep replies short and rely on openclaw_agent_consult for detailed responses |
realtime.introMessage | "Say exactly: I'm here and listening." | Emitted a single time when the realtime bridge establishes; choose "" to enter without sound |
realtime.agentId | "main" | OpenClaw agent identifier applied to openclaw_agent_consult |
voiceCall.enabled | true | Hands off the Twilio PSTN call, DTMF handling, and introductory greeting to the Voice Call plugin |
voiceCall.dtmfDelayMs | 12000 | Initial pause before transmitting a PIN-derived DTMF sequence over Twilio |
voiceCall.postDtmfSpeechDelayMs | 5000 | Wait time prior to requesting the realtime intro greeting once Voice Call starts the Twilio leg |
With chrome.audioBridgeCommand and chrome.audioBridgeHealthCommand, an external bridge takes over the complete local audio path rather than relying on chrome.audioInputCommand/chrome.audioOutputCommand; check Notes for which mode permits their use.
A migration for the older realtime.provider: "google" format exists as an openclaw doctor --fix: it relocates that intent to realtime.voiceProvider: "google" together with realtime.transcriptionProvider: "openai" when neither field is already populated.
Optional overrides
{
defaults: {
meeting: "https://meet.google.com/abc-defg-hij",
},
browser: {
defaultProfile: "openclaw",
},
chrome: {
guestName: "OpenClaw Agent",
waitForInCallMs: 30000,
bargeInInputCommand: [
"sox",
"-q",
"-t",
"coreaudio",
"External Microphone",
"-r",
"24000",
"-c",
"1",
"-b",
"16",
"-e",
"signed-integer",
"-t",
"raw",
"-",
],
},
chromeNode: {
node: "parallels-macos",
},
defaultMode: "agent",
realtime: {
provider: "openai",
transcriptionProvider: "openai",
voiceProvider: "google",
model: "gemini-3.1-flash-live-preview",
agentId: "jay",
toolPolicy: "owner",
introMessage: "Say exactly: I'm here.",
providers: {
google: {
speakerVoice: "Kore",
},
},
},
}
ElevenLabs serves both agent-mode listening and speaking:
{
tts: {
provider: "elevenlabs",
providers: {
elevenlabs: {
modelId: "eleven_v3",
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
},
},
},
plugins: {
entries: {
"google-meet": {
config: {
realtime: {
transcriptionProvider: "elevenlabs",
providers: {
elevenlabs: {
modelId: "scribe_v2_realtime",
audioFormat: "ulaw_8000",
sampleRate: 8000,
commitStrategy: "vad",
},
},
},
},
},
},
},
}
The ongoing Meet audio originates from tts.providers.elevenlabs.speakerVoiceId. Agent responses may alternatively honor per-reply [[tts:speakerVoiceId=... model=eleven_v3]] directives if TTS model overrides are active, though configuration remains the fixed default for meetings. Upon joining, logs record transcriptionProvider=elevenlabs, and every spoken reply logs provider=elevenlabs model=eleven_v3 speakerVoiceId=<voiceId>.
Settings exclusive to Twilio:
{
defaultTransport: "twilio",
twilio: {
defaultDialInNumber: "+15551234567",
defaultPin: "123456",
},
voiceCall: {
gatewayUrl: "ws://127.0.0.1:18789",
},
}
When voiceCall.enabled: true is active (its default) and Twilio transport is used, Voice Call sends the DTMF sequence prior to opening the realtime media stream, then employs the stored intro text as the opening realtime greeting. If voice-call remains disabled, Google Meet can still verify and log the dial plan but cannot initiate the Twilio call.
Keep voiceCall.gatewayUrl empty to rely on the local trusted Gateway runtime, which maintains the
invoking agent throughout the entire call. A set Gateway URL acts as an explicit WebSocket destination and
cannot verify plugin origin; non-default agent joins fail closed instead of quietly
adopting a different agent. When per-agent routing matters, run Google Meet and Voice Call within the same Gateway process.
Tool
Agents rely on the google_meet tool:
{
"action": "join",
"url": "https://meet.google.com/abc-defg-hij",
"transport": "chrome-node",
"mode": "agent"
}
action | Purpose |
|---|---|
join | Connect to a specific Meet URL |
create | Establish a space (joining by default); works with accessType/entryPointAccess |
status | Show running sessions, or drill into one using sessionId |
setup_status | Execute the identical verification steps as googlemeet setup |
resolve_space | Turn a URL, code, or spaces/{id} into a meeting via spaces.get |
preflight | Check that OAuth and meeting resolution are ready |
latest | Retrieve the newest conference entry tied to a meeting |
calendar_events | Look ahead at Calendar items that carry Meet links |
artifacts | Enumerate conference entries along with participant, recording, transcript, and smart-note details |
attendance | Enumerate participants and their individual sessions |
export | Save the artifacts, attendance, transcript, and manifest bundle; use "dryRun": true for manifest-only output |
recover_current_tab | Target and examine an already-open Meet tab without launching another |
transcript | Pull the bounded caption transcript; sinceIndex picks up from the prior nextIndex |
leave | Terminate a session (Chrome presses Leave; only its own tabs close; Twilio disconnects) |
end_active_conference | Stop the ongoing Google Meet conference for an API-managed space |
speak | Force the realtime agent to talk right away, supplied with sessionId and message |
test_speech | Spin up or reuse a session, fire a known phrase, and report Chrome status |
test_listen | Spin up or reuse an observe-only session, then wait for caption or transcript activity |
test_speech invariably enforces mode: "agent" or "bidi" and errors out when mode: "transcribe" is requested, since observe-only sessions have no speech capability. speechOutputVerified demands both fresh realtime output bytes and fresh non-silent audio arriving on the bridge's microphone capture path during that output. Output from a reused session that is older, or a loopback signal, does not qualify, and sink-byte growth alone no longer signals verified speech.
With Chrome transports, leave leaves a reused user-owned tab open after Meet's Leave button is clicked. Tabs that OpenClaw created are shut once departure happens.
Choose transport: "chrome" when Chrome lives on the Gateway host, and transport: "chrome-node" when it runs on a paired node. Either way, the model providers and openclaw_agent_consult stay on the Gateway host, so model credentials never leave it. Agent-mode logs show the resolved transcription provider and model at bridge startup, and the TTS provider, model, voice, output format, and sample rate after every synthesized reply. Raw mode: "realtime" remains accepted as a legacy alias for mode: "agent", but it is no longer listed in the tool's mode enum.
create combined with an API-backed room and an explicit access policy:
{
"action": "create",
"transport": "chrome-node",
"mode": "agent",
"accessType": "OPEN"
}
Closing the active conference for a known room:
{
"action": "end_active_conference",
"meeting": "https://meet.google.com/abc-defg-hij"
}
A listen-first check before claiming a meeting often helps:
{
"action": "test_listen",
"url": "https://meet.google.com/abc-defg-hij",
"transport": "chrome-node",
"timeoutMs": 30000
}
Speaking on demand:
{
"action": "speak",
"sessionId": "meet_...",
"message": "Say exactly: I'm here and listening."
}
status includes Chrome health whenever that data is available:
| Field | Meaning |
|---|---|
inCall | Chrome is detected as being within the Meet session |
micMuted | Microphone status for Meet, captured on a best-effort basis |
manualAction.reason / manualAction.message | Manual login, host approval, permissions, or browser-control fixes are required for the browser profile before speech works |
speechReady / speechBlockedReason / speechBlockedMessage | Current allowance for managed Chrome speech; speechReady: false signals that OpenClaw skipped the intro/test phrase |
providerConnected / realtimeReady | Current state of the realtime voice bridge |
lastInputAt / lastOutputAt | Most recent audio received from or transmitted to the bridge |
audioInputRouted / audioInputDeviceLabel | Confirmation that Meet's microphone is the validated native virtual-audio input |
audioOutputRouted / audioOutputDeviceLabel | Active routing of the Meet tab's media output to the native virtual-audio backend |
lastOutputLoopbackAt / outputLoopbackSignalBytes | New output whose waveform fingerprint matched on the virtual microphone capture path |
lastOutputLoopbackCorrelation | Correlation score linking the captured signal to the current assistant-output generation |
outputGeneration / verifiedOutputGeneration | Monotonic identifiers; equality indicates the present output, not an earlier utterance, passed loopback verification |
lastOutputLoopbackRms / lastOutputLoopbackPeak | Audio-energy diagnostics for the most recent verified loopback capture chunk |
lastSuppressedInputAt / suppressedInputBytes | Loopback input is disregarded while assistant playback is ongoing |
Agent and bidi modes
| Mode | Who decides the answer | Speech output path | Use when |
|---|---|---|---|
agent | The configured OpenClaw agent | Normal OpenClaw TTS runtime | You want "my agent is in the meeting" behavior |
bidi | The realtime voice model | Realtime voice provider audio response | You want the lowest-latency conversational voice loop |
agent mode: the realtime transcription provider picks up meeting audio, final participant transcripts route through the configured OpenClaw agent, and the answer is spoken via regular OpenClaw TTS. Nearby final-transcript fragments are merged before the consult so one spoken turn does not yield several stale partial answers; realtime input is paused while queued assistant audio is still playing, and recent assistant-like transcript echoes are filtered out before the consult so BlackHole loopback does not cause the agent to answer its own speech.
bidi mode: the realtime voice model responds directly and can invoke openclaw_agent_consult for deeper reasoning, current information, or normal OpenClaw tools. The consult tool runs the regular OpenClaw agent behind the scenes with recent meeting transcript context and returns a concise spoken answer; in agent mode OpenClaw sends that answer directly to TTS, in bidi mode the realtime voice model can speak it back. It relies on the same shared consult machinery as Voice Call.
By default consults run against the main agent; set realtime.agentId to point a Meet lane at a dedicated agent workspace, model defaults, tool policy, memory, and session history. Agent-mode consults use a per-meeting agent:<id>:subagent:google-meet:<session> session key so follow-up questions retain meeting context while inheriting normal agent policy. When an agent calls google_meet in agent mode, the consultant session forks the caller's current transcript before answering participant speech; the Meet session stays separate so meeting follow-ups do not mutate the caller transcript directly.
realtime.toolPolicy controls the consult run:
| Policy | Behavior |
|---|---|
safe-read-only | Expose the consult tool; limit the regular agent to read, web_search, web_fetch, x_search, memory_search, memory_get |
owner | Expose the consult tool; let the regular agent use its normal tool policy |
none | Do not expose the consult tool to the realtime voice model |
The consult session key is scoped per Meet session, so follow-up consult calls reuse prior consult context during the same meeting.
Force a spoken readiness check after Chrome has fully joined:
openclaw googlemeet speak meet_... "Say exactly: I'm here and listening."
Full join-and-speak smoke:
openclaw googlemeet test-speech https://meet.google.com/abc-defg-hij \
--transport chrome-node \
--message "Say exactly: I'm here and listening."
Live test checklist
Before handing a meeting to an unattended agent:
openclaw googlemeet setup
openclaw nodes status
openclaw googlemeet test-speech https://meet.google.com/abc-defg-hij \
--transport chrome-node \
--message "Say exactly: Google Meet speech test complete."
Expected Chrome-node state:
googlemeet setupis all green, and includeschrome-node-connectedwhen Chrome-node is the default transport or a node is pinned.nodes statusshows the selected node connected, advertising bothgooglemeet.chromeandbrowser.proxy.- The Meet tab joins, and
test-speechreturns Chrome health withinCall: true.
For a remote Chrome host such as a Parallels macOS VM, the shortest safe check after updating the Gateway or the VM:
openclaw googlemeet setup
openclaw nodes status --connected
openclaw nodes invoke \
--node parallels-macos \
--command googlemeet.chrome \
--params '{"action":"setup"}'
That proves the Gateway plugin is loaded, the VM node is connected with the current token, and the Meet audio bridge is available before an agent opens a real meeting tab.
For a Twilio smoke, use a meeting that exposes phone dial-in details:
openclaw googlemeet setup
openclaw googlemeet join https://meet.google.com/abc-defg-hij \
--transport twilio \
--dial-in-number +15551234567 \
--pin 123456
Expected Twilio state:
googlemeet setupshows greentwilio-voice-call-plugin,twilio-voice-call-credentials, andtwilio-voice-call-webhookindicators.- After a Gateway reload,
voicecallbecomes accessible through the CLI. - The session that comes back includes
transport: "twilio"and atwilio.voiceCallId. openclaw logs --followdisplays DTMF TwiML delivered ahead of realtime TwiML, followed by a realtime bridge with the initial greeting queued.- The delegated voice call gets terminated by
googlemeet leave <sessionId>.
Troubleshooting
Agent cannot see the Google Meet tool
Make sure the plugin is active and reload the Gateway; the live agent sees only those plugin tools that the current Gateway process has registered:
openclaw plugins list | grep google-meet
openclaw googlemeet setup
For local Chrome talk-back on Linux, the Chrome desktop user's session needs PipeWire-Pulse plus pactl, pacat, and parec. On systems that aren't supported, go with mode: "transcribe", Twilio dial-in, or a supported macOS/Linux chrome-node host.
No connected Google Meet-capable node
On the node host:
openclaw plugins install npm:@openclaw/google-meet
openclaw plugins enable browser
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \
openclaw node run --host <gateway-lan-ip> --port 18789 --display-name parallels-macos
On the Gateway host:
openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status
The node has to be connected and show googlemeet.chrome along with browser.proxy; the Gateway config must permit both:
{
gateway: {
nodes: {
commands: { allow: ["browser.proxy", "googlemeet.chrome"] },
},
},
}
When googlemeet setup fails chrome-node-connected, or the Gateway log mentions gateway token mismatch, reinstall or restart the node using the current Gateway token:
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 \
openclaw node install \
--host <gateway-lan-ip> \
--port 18789 \
--display-name parallels-macos \
--force
After that, reload the node service and execute again:
openclaw googlemeet setup
openclaw nodes status --connected
Browser opens but agent cannot join
Use googlemeet test-listen for observe-only joins or googlemeet test-speech for realtime joins, then check the Chrome health that comes back. If either one contains manualAction, present manualAction.message to the operator and hold off on retrying until the browser action wraps up.
Typical manual steps: log into the Chrome profile; let the guest in from the Meet host account; approve Chrome microphone/camera access when the native prompt pops up; fix or close a stuck Meet permission dialog.
Don't flag "not signed in" just because Meet asks "Do you want people to hear you in the meeting?"; that's Meet's audio-choice interstitial. OpenClaw clicks Use microphone via browser automation when it can and continues waiting for the actual meeting state; for create-only browser fallback it may click Continue without microphone instead, since generating the URL doesn't rely on the realtime audio path.
Meeting creation fails
googlemeet create relies on the Meet API spaces.create when OAuth is set up, otherwise it uses the pinned Chrome node browser. Verify:
- API creation:
oauth.clientIdandoauth.refreshToken(or matchingOPENCLAW_GOOGLE_MEET_*env vars) are in place, and the refresh token was created after create support was added; older tokens may not havemeetings.space.created, so runopenclaw googlemeet auth login --jsonagain. - Browser fallback:
defaultTransport: "chrome-node"andchromeNode.nodetarget a connected node withbrowser.proxyandgooglemeet.chrome; the OpenClaw Chrome profile on that node is logged in and can openhttps://meet.google.com/new. - Browser fallback retries: reuse an existing
.../newor Google account prompt tab before opening a fresh one; retry the tool call instead of manually opening another tab. - Manual action: if the tool returns
manualAction, usebrowser.nodeId,browser.targetId,browserUrl, andmanualAction.messageto direct the operator; avoid looping retries. - Audio-choice interstitial: if Meet shows "Do you want people to hear you in the meeting?", keep the tab open. OpenClaw should click Use microphone or (create-only) Continue without microphone and keep waiting for the generated URL; if it can't, the error should reference
meet-audio-choice-required, notgoogle-login-required.
Agent joins but does not talk
openclaw googlemeet setup
openclaw googlemeet doctor
Use mode: "agent" when routing speech through the STT, OpenClaw agent, then TTS pipeline, while mode: "bidi" serves as the direct realtime voice alternative. mode: "transcribe" deliberately omits any talk-back bridge. For observe-only debugging, execute openclaw googlemeet status --json <session-id> once participants have spoken, then review captioning, transcriptLines, and lastCaptionText. When inCall evaluates to true but transcriptLines remains at 0, possible causes include disabled Meet captions, silence since the observer was installed, a changed Meet interface, or live captions being unsupported for the current meeting language or account.
googlemeet test-speech consistently validates the realtime path, indicating whether bridge output bytes appeared for that specific invocation. If speechOutputVerified comes back false while speechOutputTimedOut is true, the realtime provider might have accepted the utterance, yet OpenClaw saw no fresh output bytes delivered to the Chrome audio bridge.
Additional checks: confirm a realtime provider key (either OPENAI_API_KEY or GEMINI_API_KEY) exists on the Gateway host, ensure the native audio backend is operational on the Chrome host, and verify Meet mic and speaker are directed through the virtual audio path, where doctor should display both input and output routing for local Chrome realtime joins.
googlemeet doctor [session-id] outputs session details, node, in-call status, manual action reason, realtime provider connection, realtimeReady, audio input and output activity, latest audio timestamps, byte counters, and the browser URL. For raw JSON, use googlemeet status [session-id] --json, and employ googlemeet doctor --oauth with --meeting or --create-space added to confirm OAuth refresh while keeping tokens hidden.
When an agent times out and a Meet tab is already present, examine it without launching another:
openclaw googlemeet recover-tab
openclaw googlemeet recover-tab https://meet.google.com/abc-defg-hij
The matching tool action is recover_current_tab, which focuses and examines an existing Meet tab for the chosen transport, using local browser control for chrome or the configured node for chrome-node, without spawning a new tab or session, and returns the current blocker such as login, admission, permissions, or audio-choice state. The CLI command targets the configured Gateway, which must be active; chrome-node additionally demands the node be connected.
Twilio setup checks fail
twilio-voice-call-plugin errors out when voice-call is either disallowed or disabled: add it to plugins.allow, turn on plugins.entries.voice-call, then reload the Gateway.
twilio-voice-call-credentials errors out when the Twilio backend lacks an account SID, auth token, or caller number:
export TWILIO_ACCOUNT_SID=AC...
export TWILIO_AUTH_TOKEN=...
export TWILIO_FROM_NUMBER=+15550001234
twilio-voice-call-webhook errors out when voice-call lacks public webhook accessibility, or when publicUrl targets loopback or private network ranges. Avoid using localhost, 127.0.0.1, 0.0.0.0, 10.x, 172.16.x through 172.31.x, 192.168.x, 169.254.x, fc00::/7, or fd00::/8 as publicUrl, since carrier callbacks cannot reach those destinations. Point plugins.entries.voice-call.config.publicUrl at a public URL, or set up a tunnel or Tailscale exposure:
{
plugins: {
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio",
fromNumber: "+15550001234",
publicUrl: "https://voice.example.com/voice/webhook",
},
},
},
},
}
For local development, prefer a tunnel or Tailscale exposure over a private host URL:
{
plugins: {
entries: {
"voice-call": {
config: {
tunnel: { provider: "ngrok" },
// or
tailscale: { mode: "funnel", path: "/voice/webhook" },
},
},
},
},
}
Restart or reload the Gateway, then proceed with:
openclaw googlemeet setup --transport twilio
openclaw voicecall setup
openclaw voicecall smoke
voicecall smoke defaults to readiness-only mode. To dry-run a specific number:
openclaw voicecall smoke --to "+15555550123"
Add --yes only when you intend to trigger a live outbound call:
openclaw voicecall smoke --to "+15555550123" --yes
Twilio call starts but never enters the meeting
Verify the Meet event includes phone dial-in information, and supply the exact dial-in number along with the PIN or a custom DTMF sequence:
openclaw googlemeet join https://meet.google.com/abc-defg-hij \
--transport twilio \
--dial-in-number +15551234567 \
--dtmf-sequence ww123456#
Insert leading w or commas within --dtmf-sequence to create a pause before entering the PIN.
If the call was created but the dial-in participant never shows up in the Meet roster:
openclaw googlemeet doctor <session-id>: verify the delegated Twilio call ID, whether DTMF was queued, and if the intro greeting was requested.openclaw voicecall status --call-id <id>: verify the call is still active.openclaw voicecall tail: verify that Twilio webhooks are reaching the Gateway.openclaw logs --follow: check for the Twilio Meet sequence: Google Meet delegates the join, Voice Call stores and serves pre-connect DTMF TwiML, Voice Call serves realtime TwiML for the Twilio call, then Google Meet requests intro speech withvoicecall.speak.- Re-run
openclaw googlemeet setup --transport twilio; a green setup check is necessary but does not confirm the meeting PIN sequence is correct. - Confirm the dial-in number matches the same Meet invitation and region as the PIN.
- Increase
voiceCall.dtmfDelayMsfrom the 12-second default if Meet answers slowly or the call transcript still shows the PIN prompt after pre-connect DTMF was sent. - If the participant joins but the greeting is not heard, check
openclaw logs --followfor the post-DTMFvoicecall.speakrequest and either media-stream TTS playback or the Twilio<Say>fallback. If the transcript still shows "enter the meeting PIN", the phone leg has not joined the Meet room yet, so participants will not hear speech.
If webhooks are not arriving, debug the Voice Call plugin first: the provider must reach plugins.entries.voice-call.config.publicUrl or the configured tunnel. See Voice call troubleshooting.
Notes
Google Meet's official media API is receive-oriented, so speaking into a call still needs a participant path. This plugin keeps that boundary visible: Chrome handles browser participation and local audio routing; Twilio handles phone dial-in participation.
Chrome talk-back modes need a supported native virtual-audio backend plus either:
chrome.audioInputCommandpluschrome.audioOutputCommand: OpenClaw owns the bridge and pipes audio inchrome.audioFormatbetween those commands and the selected provider.agentmode uses realtime transcription plus regular TTS;bidimode uses the realtime voice provider. The default path is 24 kHz PCM16 withchrome.audioBufferBytes: 4096; 8 kHz G.711 mu-law remains available for legacy command pairs.chrome.audioBridgeCommand: an external bridge command owns the whole local audio path and must exit after starting or validating its daemon. Valid only forbidi, becauseagentmode needs direct command-pair access for TTS.
With the command-pair Chrome bridge, chrome.bargeInInputCommand can listen to a separate local microphone and clear assistant playback when a human starts talking, keeping human speech ahead of assistant output even while the shared virtual loopback input is temporarily suppressed during assistant playback. Like chrome.audioInputCommand/chrome.audioOutputCommand, it is an operator-configured local command: use an explicit trusted command path or argument list, never a script from an untrusted location.
For clean duplex audio, route Meet output and Meet microphone through separate virtual devices or a Loopback-style virtual device graph; the default shared loopback device can echo other participants back into the call.
googlemeet speak triggers the active talk-back audio bridge for a Chrome session; googlemeet leave stops it (and, for Twilio sessions delegated through Voice Call, hangs up the underlying call). Use googlemeet end-active-conference to also close the active Google Meet conference for an API-managed space.