> For a complete documentation index, fetch https://docs.voximplant.ai/llms.txt

# Example: Function calling

> This example answers an inbound Voximplant call, connects it to Inworld Realtime, and handles function calls inside VoxEngine.

<blockquote>
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

This example answers an inbound Voximplant call, connects it to Inworld Realtime, and handles function calls inside VoxEngine.

**Jump to the [Full VoxEngine scenario](#full-voxengine-scenario).**

## Prerequisites

* Set up an inbound entrypoint for the caller:
  * [Phone number](/getting-started/network-options/phone-numbers-pstn)
  * [WhatsApp](/getting-started/network-options/whatsapp)
  * [SIP user or SIP registration](/getting-started/network-options/sip)
  * [App user](/getting-started/network-options/web-mobile)
* Create a [routing rule](/platform/voxengine/routing-rules) that points the destination to this scenario.
* Store your Inworld API key in Voximplant [Secrets](/platform/voxengine/secrets) under `INWORLD_API_KEY`.

## Tool setup

Function tools are included in the Inworld `session.update` payload.
The example exposes two client-side tools:

* `get_weather`: returns demo weather data to the model.
* `hang_up`: returns a tool result, lets Inworld speak a brief goodbye when audio is produced, and hangs up after the response completes.

```js title="Tool definition"
tools: [
  {
    type: "function",
    name: "get_weather",
    description: "Get current weather for a location.",
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
    },
  },
  {
    type: "function",
    name: "hang_up",
    description: "Hang up the current call.",
    parameters: {
      type: "object",
      properties: {},
      required: [],
    },
  },
],
tool_choice: "auto",
```

The rest of the session config stays focused on the tool flow: model, prompt, audio input/output models, and concise TTS delivery settings.
The system prompt tells the model not to speak before a tool result is available, so the caller does not hear a filler phrase such as "let me check" before the function result is ready.
Unlike the richer inbound demo, this example does not force `segmenter_strategy: "full_turn"`; that keeps the tool-result answer more responsive once Inworld starts generating the final spoken response.
For a broader walkthrough of Inworld voice behavior tuning, see [Answering an incoming call](inbound).

## Handle tool calls

Inworld emits the function name and `call_id` on the function-call output item, then emits the final JSON arguments with `ResponseFunctionCallArgumentsDone`.
The example stores the function-call metadata by item id, reads the matching arguments, executes the local function, sends a `function_call_output` conversation item, and creates the follow-up response:

```js title="Track the function call"
functionCallsByItemId[item.id] = {
  name: item.name,
  call_id: item.call_id,
};
```

```js title="Read the function arguments"
const { item_id: itemId, arguments: rawArgs } = payload;
const functionCall = functionCallsByItemId[itemId];
```

```js title="Respond to a tool call"
voiceAIClient.conversationItemCreate({
  item: {
    type: "function_call_output",
    call_id: toolCallId,
    output: JSON.stringify(result),
  },
});
voiceAIClient.responseCreate({
  response: {
    output_modalities: ["audio", "text"],
  },
});
```

## Connect call audio

The call setup is otherwise the same as the inbound example.
After `SessionCreated`, send the session config. After `SessionUpdated`, bridge media and seed an initial greeting with `conversationItemCreate` followed by `responseCreate`.
The caller's spoken turns go through the bridged audio stream; Inworld STT, semantic VAD, and `create_response: true` handle turn completion and response creation.

## Barge-in

The function-calling example keeps the same interruption behavior as the other Inworld examples:

```js title="Barge-in"
voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
  voiceAIClient.outputAudioBufferClear({});
});
```

## Notes

* `sessionKey` can be any unique string to maintain context for the Inworld session.
* Keep tool handlers fast in VoxEngine. For slow work, return an acknowledgement and move the long-running workflow outside the call path.
* Tool responses must use the same `call_id` that Inworld provided.
* Empty tool argument strings are treated as `{}`; tools without parameters do not need placeholder JSON.
* Tool-call turns are silent until the function output is sent back and the follow-up response is created.
* The `hang_up` tool waits for `ResponseOutputAudioDone` when Inworld produces audio, with a completed `ResponseDone` fallback for final responses that complete without audio output.
* For the full provider event schema, see the [Inworld module reference](/api-reference/voxengine/inworld/realtime-api-events).

## Full VoxEngine scenario

```javascript title={"voxeengine-inworld-function-calling.js"} maxLines={0}
/**
 * Voximplant + Inworld Realtime API demo
 * Scenario: answer an incoming call and handle Inworld function calls.
 *
 * Configure this in the Voximplant application:
 * - Secret `INWORLD_API_KEY` (Voximplant Secrets)
 */

require(Modules.Inworld);

const WEATHER_TOOL = "get_weather";
const HANGUP_TOOL = "hang_up";

const SYSTEM_PROMPT = `
You are Voxi, a Voximplant developer advocate on a live phone call.
Voximplant is pronounced VOX-im-plant.
Keep answers short and natural.

If the caller asks about weather, call get_weather.
If the caller wants to end the call, say a brief goodbye and call hang_up.
When you call a tool, do not speak before the tool result is available.

Voice style:
- Sound like an expressive product expert, not a flat IVR.
- Use short, human turns.
- Use at most one TTS-2 non-verbal tag per turn, and often none: [laugh], [breathe], [sigh], [clear throat].
- Use at most one [speak ...] steering tag per turn. If used, it must be first.
`;

const SESSION_CONFIG = {
    session: {
        type: "realtime",
        model: "claude-sonnet-4-6",
        instructions: SYSTEM_PROMPT,
        output_modalities: ["audio", "text"],
        audio: {
            input: {
                transcription: {
                    model: "inworld/inworld-stt-1",
                    prompt: "Important terms: Voximplant, VoxEngine, Inworld, weather, San Francisco.",
                },
                turn_detection: {
                    type: "semantic_vad",
                    eagerness: "high",
                    create_response: true,
                    interrupt_response: true,
                },
            },
            output: {
                voice: "Ashley",
                model: "inworld-tts-2",
            },
        },
        providerData: {
            tts: {
                delivery_mode: "BALANCED",
            },
        },
        tools: [
            {
                type: "function",
                name: WEATHER_TOOL,
                description: "Get current weather for a location.",
                parameters: {
                    type: "object",
                    properties: {
                        location: {
                            type: "string",
                            description: "City name, for example San Francisco.",
                        },
                    },
                    required: ["location"],
                },
            },
            {
                type: "function",
                name: HANGUP_TOOL,
                description: "Hang up the current call.",
                parameters: {
                    type: "object",
                    properties: {},
                    required: [],
                },
            },
        ],
        tool_choice: "auto",
    },
};

VoxEngine.addEventListener(AppEvents.CallAlerting, async ({call}) => {
    let voiceAIClient;
    let pendingHangup = false;
    const functionCallsByItemId = {};

    // Helper to clean-up the call when done
    const terminate = (event) => {
        if (event) Logger.write(JSON.stringify(event));
        voiceAIClient?.close();
        VoxEngine.terminate();
    };

    // Termination handlers.
    call.addEventListener(CallEvents.Disconnected, terminate);
    call.addEventListener(CallEvents.Failed, terminate);

    try {
        call.answer();

        voiceAIClient = await Inworld.createRealtimeAPIClient({
            apiKey:  VoxEngine.getSecretValue("INWORLD_API_KEY"),
            sessionKey: `inworld-tools-${Date.now()}`,
            onWebSocketClose: terminate,
        });

        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionCreated, () => {
            Logger.write("===Inworld.SessionCreated===");
            voiceAIClient.sessionUpdate(SESSION_CONFIG);
        });

        // Once the session is configured, bridge call media and trigger the greeting.
        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.SessionUpdated, () => {
            Logger.write("===Inworld.SessionUpdated===");
            // Bridge media between the call and Inworld Realtime.
            VoxEngine.sendMediaBetween(call, voiceAIClient);
            voiceAIClient.conversationItemCreate({
                item: {
                    type: "message",
                    role: "user",
                    content: [
                        {
                            type: "input_text",
                            text: "The phone call just connected. Say only: Hi, this is Voxi. I can check the weather.",
                        },
                    ],
                },
            });
            voiceAIClient.responseCreate({
                response: {
                    output_modalities: ["audio", "text"],
                },
            });
        });

        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => {
            Logger.write("===BARGE-IN: Inworld.InputAudioBufferSpeechStarted===");
            voiceAIClient.outputAudioBufferClear({});
        });

        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.ResponseOutputItemAdded, (event) => {
            const payload = event?.data?.payload || event?.data || {};
            const item = payload.item;
            if (item?.type !== "function_call") return;

            functionCallsByItemId[item.id] = {
                name: item.name,
                call_id: item.call_id,
            };
        });

        voiceAIClient.addEventListener(
            Inworld.RealtimeAPIEvents.ResponseFunctionCallArgumentsDone,
            (event) => {
                const payload = event?.data?.payload || event?.data || {};
                const {item_id: itemId, arguments: rawArgs} = payload;
                const functionCall = functionCallsByItemId[itemId];
                const toolName = functionCall?.name;
                const toolCallId = functionCall?.call_id;

                if (!toolName || !toolCallId) {
                    Logger.write("===TOOL_CALL_MISSING_FIELDS===");
                    Logger.write(JSON.stringify({payload, functionCall}));
                    return;
                }

                let args = {};
                if (typeof rawArgs === "string") {
                    if (rawArgs.trim()) {
                        try {
                            args = JSON.parse(rawArgs);
                        } catch (error) {
                            Logger.write("===TOOL_ARGS_PARSE_ERROR===");
                            Logger.write(rawArgs);
                            Logger.write(error);
                        }
                    }
                } else if (rawArgs && typeof rawArgs === "object") {
                    args = rawArgs;
                }

                Logger.write("===TOOL_CALL_RECEIVED===");
                Logger.write(JSON.stringify({toolName, args}));

                if (toolName === WEATHER_TOOL) {
                    const location = args.location || "San Francisco";
                    const result = {
                        location,
                        temperature_f: 72,
                        condition: "sunny",
                    };
                    voiceAIClient.conversationItemCreate({
                        item: {
                            type: "function_call_output",
                            call_id: toolCallId,
                            output: JSON.stringify(result),
                        },
                    });
                    Logger.write("===TOOL_RESPONSE_SENT===");
                    Logger.write(JSON.stringify(result));
                    voiceAIClient.responseCreate({
                        response: {
                            output_modalities: ["audio", "text"],
                        },
                    });
                    return;
                }

                if (toolName === HANGUP_TOOL) {
                    pendingHangup = true;
                    const result = {status: "hangup_pending"};
                    voiceAIClient.conversationItemCreate({
                        item: {
                            type: "function_call_output",
                            call_id: toolCallId,
                            output: JSON.stringify(result),
                        },
                    });
                    Logger.write("===TOOL_RESPONSE_SENT===");
                    Logger.write(JSON.stringify(result));
                    voiceAIClient.responseCreate({
                        response: {
                            output_modalities: ["audio", "text"],
                        },
                    });
                    return;
                }

                const result = {error: `Unhandled tool: ${toolName}`};
                voiceAIClient.conversationItemCreate({
                    item: {
                        type: "function_call_output",
                        call_id: toolCallId,
                        output: JSON.stringify(result),
                    },
                });
                Logger.write("===TOOL_RESPONSE_SENT===");
                Logger.write(JSON.stringify(result));
                voiceAIClient.responseCreate({
                    response: {
                        output_modalities: ["audio", "text"],
                    },
                });
            },
        );

        // Let the agent finish speaking the goodbye before hanging up when possible.
        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.ResponseOutputAudioDone, () => {
            if (!pendingHangup) return;
            Logger.write("===HANGUP_AFTER_AGENT_AUDIO===");
            pendingHangup = false;
            call.hangup();
        });

        // Fallback handler in case the response is missing audio and not caught above
        voiceAIClient.addEventListener(Inworld.RealtimeAPIEvents.ResponseDone, (event) => {
            const payload = event?.data?.payload || event?.data || {};
            if (pendingHangup && payload?.response?.status === "completed") {
                Logger.write("===HANGUP_AFTER_RESPONSE_DONE===");
                pendingHangup = false;
                call.hangup();
            }
            Logger.write(`===${event.name}===`);
            if (event?.data) Logger.write(JSON.stringify(event.data));
        });


        // Consolidated log-only handlers for lifecycle, audio, and error debugging.
        [
            Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionDelta,
            Inworld.RealtimeAPIEvents.ConversationItemInputAudioTranscriptionCompleted,
            Inworld.RealtimeAPIEvents.ResponseCreated,
            Inworld.RealtimeAPIEvents.ResponseFunctionCallArgumentsDelta,
            Inworld.RealtimeAPIEvents.ResponseOutputAudioDone,
            Inworld.RealtimeAPIEvents.ResponseOutputAudioTranscriptDone,
            Inworld.RealtimeAPIEvents.InputAudioBufferSpeechStopped,
            Inworld.RealtimeAPIEvents.InputAudioBufferCommitted,
            Inworld.RealtimeAPIEvents.InputAudioBufferCleared,
            Inworld.RealtimeAPIEvents.OutputAudioBufferStarted,
            Inworld.RealtimeAPIEvents.OutputAudioBufferStopped,
            Inworld.RealtimeAPIEvents.OutputAudioBufferCleared,
            Inworld.RealtimeAPIEvents.ConnectorInformation,
            Inworld.RealtimeAPIEvents.HTTPResponse,
            Inworld.RealtimeAPIEvents.Error,
            Inworld.RealtimeAPIEvents.WebSocketError,
            Inworld.RealtimeAPIEvents.Unknown,
            Inworld.Events.WebSocketMediaStarted,
            Inworld.Events.WebSocketMediaEnded,
        ].forEach((eventName) => {
            voiceAIClient.addEventListener(eventName, (event) => {
                Logger.write(`===${event.name}===`);
                if (event?.data) Logger.write(JSON.stringify(event.data));
            });
        });

    } catch (error) {
        Logger.write("===UNHANDLED_ERROR===");
        terminate(error instanceof Error ? {message: error.message, stack: error.stack} : {error: String(error)});
    }
});

```