Skip to content

Outbound Calls (call me back)

Sometimes the work outlasts the conversation - a long lookup, a background job, or a result that isn’t ready before the caller hangs up. The agent can place an outbound Teams call back to deliver the result when it’s ready.

  1. During a call, the model calls the call_me_back tool (or kicks off a background job with hermes_agent_task) with the message to deliver.
  2. The plugin asks the StandIn media bridge to place an outbound 1:1 Teams call to the caller, via a small loopback HTTP endpoint StandIn exposes.
  3. When the callee answers (recording active), the agent speaks the pending result and says goodbye - greet/deliver on answer, not while ringing.

The inbound call that requested the callback and the outbound leg that delivers it are different WebSocket connections, so the pending result is correlated by callId in a process-global registry with a TTL (see below).

KeyEnv varDefaultMeaning
worker_base_urlTEAMS_VOICE_WORKER_BASE_URLhttp://127.0.0.1:9440The loopback HTTP endpoint StandIn exposes for place-call.
allow_remote_workerTEAMS_VOICE_ALLOW_REMOTE_WORKERfalsePermit a non-loopback worker_base_url. Off by default.
tenant_idTEAMS_VOICE_TENANT_ID (falls back to TEAMS_TENANT_ID)""Default Azure AD tenant for the outbound call.

The place-call request is authenticated with the same shared secret and header names as the WebSocket upgrade, but the signed payload differs - it signs the callee’s identity, not a callId:

POST {worker_base_url}/api/calls
X-StandIn-Timestamp: {ts}
X-StandIn-Signature: HMAC-SHA256(shared_secret, "{ts}.{userObjectId}") # lowercase hex
Content-Type: application/json
{ "userObjectId": "<callee AAD object id>", "tenantId": "<tenant>" }

On success StandIn returns {"callId": ..., "scenarioId": ...}.

The request carries the HMAC-signed shared secret, so the plugin refuses a non-loopback worker_base_url unless allow_remote_worker is explicitly set - otherwise a misconfigured host would receive the signed request. Both userObjectId and tenantId are required, and a missing shared secret is rejected before any request is made.

Because the delivery leg is a separate connection, the plugin stores the pending spoken result keyed by callId in a process-global registry. Entries carry a TTL of 600 s (10 minutes) and are pruned on access, so a never-answered call-back can’t leak its result string indefinitely. When the outbound leg’s session.start arrives with direction: "outbound", the plugin pops the pending result and the agent delivers it on answer.

Outbound “call me back” additionally needs the Microsoft Graph Calls.InitiateGroupCall.All application permission on your bot app (admin- consented). Skip it if you don’t use outbound calls. Pairing and tenant setup are in the StandIn dashboard - standin.komaa.com, docs.komaa.com.