> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# MeetStream Guide: Custom Function Tools for MIA Agents

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](/guides/mia/create-an-agent). 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

| Feature | Custom Function | MCP Server |
|---------|----------------|------------|
| Registers a single endpoint | Yes | No — a whole server with many tools |
| JSON-schema arguments | Yes | Yes (via the MCP tool spec) |
| Authentication | Your bearer token / API key in `headers` | Per the MCP server's own auth |
| Response-variable extraction into `{{var}}` | Yes | No |
| Silent-return / filler-utterance control | Yes | No |
| Good for | One-off integrations, CRUD webhooks | Multi-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.

```bash
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[]`](MeetStream_Agent_Config_API_Reference.md#nested-agentcustom_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:

```json
{
  "name": "lookup_order",
  "bot": {
    "bot_id": "83acdd72-3257-4b06-a2e9-0d879dc571ff",
    "dynamic_vars": {}
  },
  "args": {
    "order_id": "ORD-1234"
  }
}
```

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`:

```json
{ "order_id": "ORD-1234" }
```

### Request headers

| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` when a body is sent |
| `X-Bot-ID` | ID of the bot that invoked the function |
| `X-Agent-ID` | Optional 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:
```json
{
  "data": {
    "order": {
      "id": "ORD-1234",
      "status": "shipped",
      "eta": "2026-04-26"
    }
  }
}
```

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

```jsonc
{
  "headers": {
    "Authorization": "Bearer <CUSTOM_FUNCTION_TOKEN>",
    "X-Tenant": "acme"
  }
}
```

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)

```python
import os
from flask import Flask, request, jsonify, abort

app = Flask(__name__)
EXPECTED = f"Bearer {os.environ['CUSTOM_FUNCTION_TOKEN']}"


@app.post("/orders/lookup")
def lookup_order():
    if request.headers.get("Authorization") != EXPECTED:
        abort(401, "Invalid token")

    payload = request.get_json()
    order_id = payload["args"]["order_id"]
    return jsonify({
        "data": {
            "order": {
                "id": order_id,
                "status": "shipped",
                "eta": "2026-04-26",
            }
        }
    })
```

#### Node.js (Express)

```javascript
const express = require("express");

const app = express();
app.use(express.json());

const EXPECTED = `Bearer ${process.env.CUSTOM_FUNCTION_TOKEN}`;

app.post("/orders/lookup", (req, res) => {
  if (req.get("Authorization") !== EXPECTED) {
    return res.status(401).send("Invalid token");
  }

  const orderId = req.body.args.order_id;

  res.json({
    data: {
      order: { id: orderId, status: "shipped", eta: "2026-04-26" },
    },
  });
});

app.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):
```json
{
  "agent": {
    "custom_functions": [
      {
        "name": "lookup_order",
        "url": "https://api.example.com/orders/lookup",
        "...": "...",
        "response_variables": [
          { "name": "latest_order_status", "path": "data.order.status" },
          { "name": "latest_order_eta",    "path": "data.order.eta"    }
        ]
      }
    ]
  }
}
```

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`](/guides/mia/mia-api-guide#mid-call-updates-update_mia). For example, the saved prompt can contain:

```text
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:

```bash
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.

```json
{
  "speak_during_execution": true,
  "speak_during_prompt": "One moment while I pull that up."
}
```

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:

```json
{ "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

- [Agent Config API Reference — `agent.custom_functions[]`](MeetStream_Agent_Config_API_Reference.md#nested-agentcustom_functions)
- [MIA Reference — Custom Function Tools capability](/guides/mia/what-is-mia#custom-function-tools)
- [Create Agent Guide — Step 6c](/guides/mia/create-an-agent#6c-optional-add-a-custom-function)
- [Mid-Call Updates (`update_mia`)](/guides/mia/mia-api-guide#mid-call-updates-update_mia)