MeetStream Guide: Custom Function Tools for MIA Agents

View as Markdown

Custom functions let a MIA agent call any HTTPS endpoint you host as a tool — no MCP server required. This guide walks you end-to-end: registering a function, writing a receiving endpoint, authenticating the call, and wiring response values back into the agent’s prompt.

Applies to: Google Meet, Zoom, Microsoft Teams Support: docs.meetstream.ai · API: api.meetstream.ai

New to MIA? Start with the Create Agent Guide. This guide assumes you already have an agent and just want to add a custom-function tool to it.


1. When to use custom functions vs MCP

FeatureCustom FunctionMCP Server
Registers a single endpointYesNo — a whole server with many tools
JSON-schema argumentsYesYes (via the MCP tool spec)
AuthenticationYour bearer token / API key in headersPer the MCP server’s own auth
Response-variable extraction into {{var}}YesNo
Silent-return / filler-utterance controlYesNo
Good forOne-off integrations, CRUD webhooksMulti-tool integrations you already run as MCP

You can combine both on the same agent — add MCP servers for multi-tool integrations and custom functions for everything else.


2. Quick start — full API example

Register a custom function on an agent via the API. The LLM sees lookup_order in its function-calling schema and can call it when the conversation warrants. The endpoint is authenticated with a webhook-specific bearer token.

$curl -X POST https://api.meetstream.ai/api/v1/mia \
> -H "Authorization: Token <YOUR_API_KEY>" \
> -H "Content-Type: application/json" \
> -d '{
> "agent_name": "Support Agent",
> "mode": "pipeline",
> "model": {
> "provider": "openai",
> "model": "gpt-4.1",
> "system_prompt": "You are a support agent. When the caller asks about an order, call lookup_order.",
> "first_message": "Hi, I am your support assistant. How can I help today?"
> },
> "voice": { "provider": "openai", "voice_id": "nova" },
> "transcriber": { "provider": "deepgram", "model": "nova-3" },
> "agent": {
> "custom_functions": [
> {
> "name": "lookup_order",
> "description": "Look up the status of a customer order by order id.",
> "method": "POST",
> "url": "https://api.example.com/orders/lookup",
> "headers": {
> "Authorization": "Bearer <CUSTOM_FUNCTION_TOKEN>"
> },
> "parameters": {
> "type": "object",
> "required": ["order_id"],
> "properties": {
> "order_id": { "type": "string", "description": "Order id like ORD-1234" }
> }
> },
> "timeout_s": 20,
> "retries": 2,
> "speak_during_execution": true,
> "speak_during_prompt": "One moment while I pull that up.",
> "response_variables": [
> { "name": "latest_order_status", "path": "data.order.status" }
> ]
> }
> ]
> }
> }'

For the full list of fields and constraints, see agent.custom_functions[] in the API reference.


3. Wire format

When the LLM calls your function, MeetStream sends an HTTPS request to the URL you registered.

Request body

By default the body is a JSON envelope so you can distinguish arguments from bot context:

1{
2 "name": "lookup_order",
3 "bot": {
4 "bot_id": "83acdd72-3257-4b06-a2e9-0d879dc571ff",
5 "dynamic_vars": {}
6 },
7 "args": {
8 "order_id": "ORD-1234"
9 }
10}

For POST, PUT, and PATCH, this envelope is the default. GET and DELETE requests have no body by default.

If you prefer a flat body containing only args, set "payload_args_only": true:

1{ "order_id": "ORD-1234" }

Request headers

HeaderValue
Content-Typeapplication/json when a body is sent
X-Bot-IDID of the bot that invoked the function
X-Agent-IDOptional agent configuration ID; do not require this header

MeetStream always adds X-Bot-ID unless your configuration explicitly supplies that exact header name. X-Agent-ID and bot.agent_id are optional and may be absent, so use X-Bot-ID as the reliable session identifier. dynamic_vars contains values extracted from earlier custom-function responses; it does not include initial agent_config_params.

Any other configured headers are forwarded, including {{variable}} substitutions. Environment expansion works only when the entire header value is env:VAR_NAME and that variable has been provisioned in the managed agent runtime; "Bearer env:VAR_NAME" is sent literally.

Response body

Your endpoint should respond with application/json. Anything 2xx is treated as success. Non-2xx responses are handed back to the LLM verbatim (capped at response_cap_chars) so the agent can recover gracefully.

Example success response:

1{
2 "data": {
3 "order": {
4 "id": "ORD-1234",
5 "status": "shipped",
6 "eta": "2026-04-26"
7 }
8 }
9}

With response_variables: [{ "name": "latest_order_status", "path": "data.order.status" }] configured, the value "shipped" becomes available as {{latest_order_status}} for the rest of the session.


4. Authenticating the call

MeetStream forwards the headers you configure and adds the bot context described above. Use a dedicated, narrowly scoped bearer token for each endpoint.

Configure the function

1{
2 "headers": {
3 "Authorization": "Bearer <CUSTOM_FUNCTION_TOKEN>",
4 "X-Tenant": "acme"
5 }
6}

Configured values are sent verbatim after {{variable}} resolution. A whole value such as "Authorization": "env:CUSTOM_FUNCTION_AUTH_HEADER" can reference a variable provisioned by MeetStream in the managed runtime.

Header values are persisted in the agent configuration and returned to callers who can read that configuration. Use a function-specific token with minimal permissions, rotate it regularly, and never reuse a broad account credential.

Verify the token in your endpoint

Python (Flask)

1import os
2from flask import Flask, request, jsonify, abort
3
4app = Flask(__name__)
5EXPECTED = f"Bearer {os.environ['CUSTOM_FUNCTION_TOKEN']}"
6
7
8@app.post("/orders/lookup")
9def lookup_order():
10 if request.headers.get("Authorization") != EXPECTED:
11 abort(401, "Invalid token")
12
13 payload = request.get_json()
14 order_id = payload["args"]["order_id"]
15 return jsonify({
16 "data": {
17 "order": {
18 "id": order_id,
19 "status": "shipped",
20 "eta": "2026-04-26",
21 }
22 }
23 })

Node.js (Express)

1const express = require("express");
2
3const app = express();
4app.use(express.json());
5
6const EXPECTED = `Bearer ${process.env.CUSTOM_FUNCTION_TOKEN}`;
7
8app.post("/orders/lookup", (req, res) => {
9 if (req.get("Authorization") !== EXPECTED) {
10 return res.status(401).send("Invalid token");
11 }
12
13 const orderId = req.body.args.order_id;
14
15 res.json({
16 data: {
17 order: { id: orderId, status: "shipped", eta: "2026-04-26" },
18 },
19 });
20});
21
22app.listen(3000);

Verification checklist

  • Use a constant-time comparison when matching tokens (hmac.compare_digest in Python, crypto.timingSafeEqual in Node) for defence against timing attacks.
  • Rotate tokens periodically — update the receiving service and agent configuration together.
  • Scope tokens narrowly — give each agent its own token so a leak can be contained without affecting other tenants.

5. Response variables — making the agent remember

Setting response_variables on a function extracts values from a successful JSON response into in-memory session variables. They are immediately available to later custom-function URL, header, query, and body templates. They are also available when the stored agent prompt template is rebuilt through update_mia.

Agent config (excerpt):

1{
2 "agent": {
3 "custom_functions": [
4 {
5 "name": "lookup_order",
6 "url": "https://api.example.com/orders/lookup",
7 "...": "...",
8 "response_variables": [
9 { "name": "latest_order_status", "path": "data.order.status" },
10 { "name": "latest_order_eta", "path": "data.order.eta" }
11 ]
12 }
13 ]
14 }
15}

To use captured values in the active prompt, include their placeholders in the saved model.system_prompt, then trigger a template rebuild with update_mia. For example, the saved prompt can contain:

When phase is wrap_up, confirm that the order is {{latest_order_status}}
with ETA {{latest_order_eta}}.

After the function has populated those variables:

$curl -X POST "https://api.meetstream.ai/api/v1/bots/{bot_id}/update_mia" \
> -H "Authorization: Token <YOUR_API_KEY>" \
> -H "Content-Type: application/json" \
> -d '{
> "agent_config_params": {
> "phase": "wrap_up"
> }
> }'

Values persist for the lifetime of the session and override matching AgentConfigParams during a template rebuild. A literal system_prompt supplied to update_mia is not template-resolved, so placeholders inside that field remain unchanged.

Path rules

  • Dot-notation only: data.user.city, meta.total, result.
  • Array access is not supported — stick to object navigation.
  • A missing path is silently skipped (no error).
  • Non-scalar values are JSON-encoded (e.g. {"city": "SF"}{"city":"SF"}).

6. Speak-during / speak-after behavior

Filler utterance while the HTTP call runs

If your endpoint takes more than a couple of hundred milliseconds, the meeting can feel like it’s stalling. Turn on speak_during_execution to have the agent emit a short filler while the HTTP call is in flight.

1{
2 "speak_during_execution": true,
3 "speak_during_prompt": "One moment while I pull that up."
4}

Leave speak_during_prompt empty to use the default filler (“One moment please.”).

Silent return for pure side effects

When your function exists purely to do something (create a ticket, update a CRM), you often don’t want the agent to summarise the result out loud. Set:

1{ "speak_after_execution": false }

The agent’s turn ends silently once the tool returns. The next turn begins when the user speaks again.


7. Errors and retries

  • 5xx / network errors: MeetStream retries up to retries times with exponential backoff (0.5s, 1s, 2s, 4s, capped at 8s).
  • 4xx: not retried. The response body is capped and handed back to the LLM so it can adjust and retry with different arguments, or apologise to the caller.
  • Timeouts (timeout_s): count as failures and are retried up to retries times.
  • Response size: anything beyond response_cap_chars is truncated with a …[truncated at N chars] marker.

Use HTTP status codes meaningfully in your endpoint — a clear 4xx error message (“Order not found”) teaches the LLM to correct itself or explain to the caller.


8. Security & best practices

  • Always require an Authorization header — without it, anyone who learns your URL can hit your endpoint.
  • Idempotency: retries mean the same call can arrive multiple times. Use an idempotency key (e.g. derive one from bot_id + args) for state-mutating endpoints.
  • Don’t leak secrets in descriptions — descriptions are shown to the LLM. Put endpoint credentials only in headers and scope them specifically to the custom function.
  • Keep responses small — the LLM reads the full capped response every turn it’s referenced; small, structured responses cost less and reason better.
  • Use speak_after_execution=false for write ops — saves tokens and keeps the meeting flowing.

9. Reference