Skip to content
MeridFlow AiFlow v8.x • self-hosted

Tools

A tool is a function the model can call mid-conversation: write a summary, hit a webhook, search the knowledge base, hand a call to a human. One exception is follow_up (category automation, covered in the table below), which runs on its own schedule instead and is never something the model calls. AiFlow ships a fixed catalog of built-in tools (this page), plus two ways to add more without writing any AiFlow code: Custom tools (any HTTP endpoint you control) and MCP servers (any external Model Context Protocol server). Enabling delegate_to_agent, covered below, is what actually activates Agent-to-agent delegation on an agent once a delegation link exists.

Admin role required

GET /api/v1/tools and GET /api/v1/agents/{agent_id}/tools need any logged-in admin (Owner, Admin, Editor, or Viewer). PUT /api/v1/agents/{agent_id}/tools/{tool_name}, which enables or reconfigures a tool, needs Owner or Admin. That's a stricter tier than editing the agent's own fields or context documents (both Editor and above, see Agents and Context & knowledge): changing what an agent is capable of doing sits at a higher bar than changing what it knows or says.

The tool registry

GET /api/v1/tools

Returns every tool AiFlow's backend knows how to run, regardless of whether any agent has it enabled. This is what the admin dashboard's Tools tab reads to render a real form per tool (text fields, toggles, a masked field for a secret) instead of a raw JSON textarea. config_schema is a JSON Schema object describing exactly what config a given tool accepts, and it's null for a tool that takes no configuration at all.

curl https://api.your-domain.com/api/v1/tools \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.get(
    "https://api.your-domain.com/api/v1/tools",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
tools = response.json()
const response = await fetch("https://api.your-domain.com/api/v1/tools", {
  headers: { Authorization: `Bearer ${adminToken}` },
});
const tools = await response.json();

Each entry in the returned list has this exact shape:

Field Type Description
name string The tool's stable identifier, e.g. send_webhook. This is what you pass as {tool_name} in the enable endpoint below.
description string What the tool does, in the wording actually sent to the model as its function description.
parameters object The Gemini function-calling parameter schema the model fills in when it calls this tool. Not something you configure, shown for completeness.
config_schema object or null The JSON Schema an admin's config value for this tool must satisfy. null for a tool that needs no configuration.
display_name string or null Human-readable label for the admin UI, e.g. "Send Webhook".
category string One of built_in, custom, knowledge, messaging, orchestration, or automation. Grouping only, has no effect on behavior.

A full entry, send_webhook, looks like this:

{
  "name": "send_webhook",
  "description": "Sends arbitrary structured data to an external system configured for this agent. Use this to notify a CRM, create a ticket, or trigger any other integration outside of AiFlow.",
  "parameters": {
    "type": "OBJECT",
    "properties": {
      "payload": {
        "type": "OBJECT",
        "description": "Arbitrary JSON data to send to the configured external system."
      }
    },
    "required": ["payload"]
  },
  "config_schema": {
    "type": "object",
    "properties": {
      "url": {
        "type": "string",
        "title": "Webhook URL",
        "description": "The external endpoint AiFlow POSTs {agent_id, call_id, payload} to."
      }
    },
    "required": ["url"]
  },
  "display_name": "Send Webhook",
  "category": "custom"
}

delegate_to_orchestrator only appears when Orchestrators are turned on

This entry in the catalog is registered only when your deployment has Orchestrators enabled, an opt-in, deployment-wide setting that's off by default. Every other tool on this page is always present in GET /api/v1/tools regardless of that setting.

An agent's current tool configuration

GET /api/v1/agents/{agent_id}/tools

Returns the same catalog as GET /api/v1/tools, merged with this agent's own enabled and config state: one entry per registered tool, whether or not the agent has ever touched it:

curl https://api.your-domain.com/api/v1/agents/1/tools \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.get(
    "https://api.your-domain.com/api/v1/agents/1/tools",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
agent_tools = response.json()
const response = await fetch("https://api.your-domain.com/api/v1/agents/1/tools", {
  headers: { Authorization: `Bearer ${adminToken}` },
});
const agentTools = await response.json();

Each entry adds two fields on top of the catalog shape above:

Field Type Description
tool_name string Same identifier as name in the catalog.
enabled boolean true if this agent can call the tool right now. false for a tool the agent has never configured, this list always includes every registered tool, not just the ones turned on.
config object or null This agent's stored config for the tool. null if never configured, even for a tool that requires config once enabled.

Enabling or reconfiguring a tool

PUT /api/v1/agents/{agent_id}/tools/{tool_name}
Field Type Required Default Description
enabled boolean No true Whether the agent can call this tool.
config object or null No null Must satisfy that tool's config_schema from the catalog. Omit or send null for a tool that takes no configuration.

This endpoint always upserts. Calling it on a tool the agent has never configured creates the row; calling it again replaces both enabled and config in full. There is no partial-merge behavior the way PATCH endpoints elsewhere on this site have: sending config without a previously-set field means that field is gone, not preserved. A {tool_name} that doesn't exist in the registry returns 404.

The examples below cover the tools most integrations touch first.

record_summary

No config. The agent writes a summary of the conversation onto the call record once it wraps up.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/record_summary \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/record_summary",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/record_summary",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: true }),
  },
);

send_webhook

config.url (string, required by this tool's own config_schema): the endpoint AiFlow POSTs {agent_id, call_id, payload} to whenever the model calls this tool.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/send_webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true, "config": {"url": "https://hooks.example.com/acme"}}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/send_webhook",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True, "config": {"url": "https://hooks.example.com/acme"}},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/send_webhook",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: { url: "https://hooks.example.com/acme" },
    }),
  },
);

search_knowledge_base

No config. Lets the model search this agent's own and every global context document (see Context & knowledge) for relevant passages.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/search_knowledge_base \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/search_knowledge_base",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/search_knowledge_base",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: true }),
  },
);

search_skills

No config. Lets the model describe the current situation and get back the closest-matching skill's full procedure, plus the names and triggers of the agent's other skills, or a clear "no matching skill" message. Passing a skill's exact name loads that one alone, so a wrong first guess is one call away from the right procedure. Distinct from search_knowledge_base: a knowledge-base match is a short passage to quote, a skill match is a whole procedure to follow.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/search_skills \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/search_skills",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/search_skills",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: true }),
  },
);

send_whatsapp

config.content_sid (string, required by this tool's own config_schema): a pre-approved Twilio Content Template SID (Twilio Console → Content Template Builder). WhatsApp and Meta policy requires an approved template for any business-initiated message; there's no free-form text option here.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/send_whatsapp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true, "config": {"content_sid": "HXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/send_whatsapp",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "enabled": True,
        "config": {"content_sid": "HXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
    },
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/send_whatsapp",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: { content_sid: "HXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" },
    }),
  },
);

send_telegram

Sends a plain-text Telegram message. Unlike send_whatsapp there is no template to get approved: a bot may message any chat that has already started a conversation with it, in whatever wording the agent chooses. That makes it the quickest outbound channel to set up.

Both config fields are optional and fall back to the deployment's own TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID. Set them here only when an agent needs its own bot or its own destination chat.

Field Type Required Falls back to Notes
bot_token string No TELEGRAM_BOT_TOKEN From @BotFather. Write-only, never read back.
chat_id string No TELEGRAM_CHAT_ID A user, group, or channel id, e.g. -1001234567890.

A message longer than Telegram's 4096-character limit is sent as several messages rather than truncated.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/send_telegram \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true, "config": {"chat_id": "-1001234567890"}}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/send_telegram",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True, "config": {"chat_id": "-1001234567890"}},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/send_telegram",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: { chat_id: "-1001234567890" },
    }),
  },
);

Telegram cannot place calls

The Telegram Bot API has no method for starting, receiving, or joining a voice or video call, so there is no Telegram equivalent of start_phone_call. sendVoice delivers a recorded audio file, not a conversation. Use the phone channel for live audio.

start_phone_call

No config. Places an outbound phone call to a number the visitor just gave the agent during a text or widget conversation, carries the recent chat transcript forward as context, then continues that same agent's persona over the new phone session. Requires the agent to have its own twilio_phone_number configured; has no effect if the conversation is already a phone call. Limited to one triggered call per chat session (see Call.triggered_by_call_id), so a single conversation can't dial repeatedly. Pair it with transfer_to_human (enabled on the same agent) to finish a chat-to-call-to-human handoff: once the phone leg connects, the agent can warm-transfer it to a real person exactly as it would on any other phone call.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/start_phone_call \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/start_phone_call",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/start_phone_call",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: true }),
  },
);

transfer_to_human

config.transfer_number (string, required by this tool's own config_schema): the E.164 phone number dialed in to take over the call. Only takes effect on phone calls; has no effect in a widget session, which has no phone leg to transfer.

On a deployment licensed for the taskrouter feature, three optional config fields, taskrouter_workspace_sid, taskrouter_workflow_sid, and taskrouter_task_attributes (a JSON string the Workflow routes on, e.g. {"skill": "billing"}), route the handoff through a Twilio TaskRouter Workspace: the transfer dials whichever Worker TaskRouter assigns, and falls back to transfer_number if none is assigned within a few seconds.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/tools/transfer_to_human \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true, "config": {"transfer_number": "+15557654321"}}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/tools/transfer_to_human",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": True, "config": {"transfer_number": "+15557654321"}},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/tools/transfer_to_human",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: { transfer_number: "+15557654321" },
    }),
  },
);

Built-in tool catalog

Most of these are licensed: a tool the licence does not cover is never registered at all, so the model is not aware of it rather than being told it may not use it. Six are always registered on every plan, including the free tier: record_summary, capture_contact_info, end_call, search_skills, verify_staff_member, and verify_caller_identity. search_knowledge_base needs the rag right, which every current plan grants. GET /api/v1/agents/{agent_id}/tools returns exactly what this deployment has, which is more reliable than reading this table.

Tool Category Config required Description
record_summary built_in None Writes a summary of the conversation once it's wrapping up.
capture_contact_info built_in None Records the caller or visitor's name and/or email as it learns them.
verify_staff_member built_in None Confirms a self-reported staff claim against a code from the staff roster. With an empty roster it refuses every claim.
verify_caller_identity built_in verify_service_sid (string, required) Sends a one-time passcode to the caller's number or email and checks it, live proof of control. Needs a Twilio Verify Service.
send_webhook custom url (string, required) Sends arbitrary JSON to an external endpoint, notify a CRM, create a ticket, or trigger any integration outside AiFlow.
search_knowledge_base knowledge None Searches this agent's active, indexed context documents for a relevant passage.
search_skills knowledge None Looks up a step-by-step procedure matching the current situation, distinct from a fact lookup.
send_telegram messaging bot_token, chat_id (both optional) Sends a plain-text Telegram message. No template approval, unlike WhatsApp.
send_whatsapp messaging content_sid (string, required) Sends the caller a WhatsApp message using a pre-approved Content Template.
send_email messaging from_address (string, optional) Sends the caller or visitor an email. The agent writes styled HTML content, rendered inside this deployment's own branded template automatically (Settings → Branding, falling back to AiFlow's own identity if unset), with a plain-text fallback part generated alongside it. from_address overrides the platform default sender for this agent only; omit it to use the deployment default.
reply_to_email messaging None Same automatic HTML and branded formatting as send_email, but replies to an inbound email through this agent's own connected mailbox address rather than send_email's platform-wide sender. See Mailbox connections.
start_phone_call built_in None Places an outbound call to a number given during a chat/widget session, carrying the transcript forward. Once per session; requires twilio_phone_number.
transfer_to_human built_in transfer_number (string, required) Hands a live phone call to a real person, with a brief spoken summary first. Phone calls only. Optional taskrouter_workspace_sid / taskrouter_workflow_sid / taskrouter_task_attributes route the handoff through Twilio TaskRouter (licensed taskrouter feature) instead of the fixed number.
delegate_to_agent orchestration None Hands a narrowly-scoped task to another configured agent and relays its one-shot text answer. Requires a delegation link to the target agent.
delegate_to_orchestrator orchestration None Hands a task to a configured Orchestrator and relays its answer. Only present in the catalog when Orchestrators are enabled for this deployment.
end_call built_in None Ends the conversation once it's genuinely finished.
crm_upsert_contact custom provider (string, required) Creates or updates a contact in the connected CRM. See CRM connectors.
crm_create_ticket custom provider (string, required) Creates a support ticket or case in the connected CRM/helpdesk. See CRM connectors.
crm_log_call_activity custom provider (string, required) Logs this call against the caller's contact in the connected CRM/helpdesk. See CRM connectors.
follow_up automation frequency_days (integer), reason (string), both required Periodically scans the Contacts list and reaches out again by email or call when one still needs a follow-up. See the note below.

follow_up runs on a schedule, not mid-conversation

Every other tool on this page is a function the model calls during a live conversation. follow_up instead runs on its own timer, every frequency_days days: it scans every contact who has talked to this agent, asks the model whether their situation still matches reason (free text you set, e.g. "they showed interest in a product but something seemed to block them from buying"), and for anyone it is, sends an email or places a call, whichever the model judges fits, subject to that contact having an email or phone number on file. Two contacts talking to the same agent at different times become due at different times; a contact is never followed up with again sooner than frequency_days after their last one, tracked on their own Contacts record. This tool is also available on Orchestrators (see Orchestrator tools), where a sweep covers the whole shared Contacts list instead of one agent's own contacts, and only email is offered: an Orchestrator has no outbound-calling capability the way an agent does.

Next

  • Custom tools: back a new tool with any HTTP endpoint you control.
  • MCP servers: connect an agent to an external Model Context Protocol server instead.
  • CRM connectors: the OAuth connection the three crm_* tools above act through.
  • Agent-to-agent delegation: the admin-configured link delegate_to_agent actually depends on.