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

Orchestrator tools

An Orchestrator reaches tools from three sources: sub-Orchestrators linked beneath it (see Orchestrator links), external MCP servers (see Orchestrator MCP servers), and a curated catalog of built-in tools. This page covers that last source: listing the catalog, and turning individual tools on, with configuration, per Orchestrator.

You can't attach an arbitrary custom Python function as an Orchestrator tool. If a built-in tool doesn't cover what you need, build or point at an MCP server instead; see Orchestrator MCP servers.

Gated behind a deployment setting, admin session token required

See the two notes at the top of Orchestrators: every endpoint here only exists when your deployment has Orchestrators turned on, and every endpoint here authenticates with an admin session token.

Two different field names for "which tool"

The catalog endpoint and the per-Orchestrator endpoints return slightly different objects. It's easy to mix them up, so here's the distinction:

  • GET /api/v1/orchestrator-tools (the catalog) returns objects whose identifying field is called name.
  • GET /api/v1/orchestrators/{orchestrator_id}/tools and PUT /api/v1/orchestrators/{orchestrator_id}/tools/{tool_name} (the per-Orchestrator endpoints) return objects whose identifying field is called tool_name.

Both refer to the same underlying tool identifier, send_email, google_search, and so on; only the JSON key differs depending on which endpoint returned it.

The catalog: every tool available to enable

GET /api/v1/orchestrator-tools

Requires: Viewer and above.

Returns the full list of tools any Orchestrator in this deployment could enable, whether or not any Orchestrator currently has them on.

Field Type Notes
name string The tool's identifier, used as the {tool_name} path segment when enabling it.
description string A human-readable summary of what the tool does.
config_schema object or null A JSON-Schema-shaped object describing the keys this tool's config accepts, or null if it takes no configuration. Descriptive only, see Config isn't validated server-side below.
display_name string or null A short label for admin UI display.
category string "built_in" (Gemini's own tools), "messaging" (AiFlow's own send_email, send_whatsapp, and send_telegram), or "knowledge" (search_skills).
kind string "tool" for a normal callable tool, or "code_executor" for code_execution, which plugs into the model's code-execution mechanism rather than being called like an ordinary tool. There is no behavioral difference from your side either way, both are enabled the same way, through the same PUT endpoint below.
gemini_only boolean true if this tool only works with a bare Gemini model. See The Gemini-only restriction below.
supports_approval boolean true if this tool can be switched to require human approval before it runs. See Human approval gate below.
curl https://api.your-domain.com/api/v1/orchestrator-tools \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

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

The built-in tool catalog

AiFlow's own tools

name display_name category What it does config fields supports_approval
send_email Send Email messaging Sends an email through the same email provider integration a native Agent's send_email tool uses. from_address (string, optional): overrides the deployment's default sender, e.g. "Acme Support <support@acme.com>". true
send_whatsapp Send WhatsApp messaging Sends a WhatsApp message using a pre-approved Content Template. content_sid (string, required): the Content Template's SID. from_number (string, required): the E.164 WhatsApp-enabled number to send from. An Orchestrator has no phone number of its own the way an Agent does, so the sending number has to be set here explicitly. true
send_telegram Send Telegram messaging Sends a plain-text Telegram message. There is no template to get approved, unlike WhatsApp. bot_token (string, optional): from @BotFather, falling back to the deployment's TELEGRAM_BOT_TOKEN. chat_id (string, optional): the chat to send to when a call does not name one, falling back to TELEGRAM_CHAT_ID. true
search_skills Search Skills knowledge Looks up a step-by-step procedure for the current situation, this Orchestrator's own skills, matched by trigger description rather than a search query. none false
remember_about_user Remember About User knowledge Saves a short note onto the person's contact record, the same profile a native Agent reads, so it is there next time and every Agent sees it. Nothing is saved for a visitor who has not been identified. none false
recall_about_user Recall About User knowledge Reads the person's contact-record profile: everything known about them from every previous conversation, not just this one. none false
follow_up Follow Up automation Runs on its own schedule rather than being callable mid-run: periodically scans the whole shared Contacts list and emails anyone whose situation still matches the reason you set. See Tools: follow_up for the full explanation, shared with the native-Agent version of this tool. frequency_days (integer, required), reason (string, required). false

send_email, send_whatsapp, and send_telegram log to the same outbound-message log and audit trail as a native Agent's sends, with triggered_by_orchestrator_id set on the message row instead of agent_id. They're also, today, the only three tools with supports_approval: true, see Human approval gate below: the tools with a genuine external side effect are exactly the ones worth gating; a read-only lookup like search_skills has nothing to defer.

Gemini's built-in tools

These map to capabilities Gemini models expose natively. Every one of them requires a bare Gemini model (gemini_only: true); see The Gemini-only restriction.

name display_name What it does config fields kind
google_search Google Search Searches Google for current information from the public web. none tool
url_context URL Context Retrieves and reads the content of URLs mentioned in the conversation. none tool
vertex_ai_search Vertex AI Search Searches your own private Vertex AI Search data store or search engine (internal documents, policies, knowledge bases). data_store_id (string, optional) or search_engine_id (string, optional): set one or the other, not both. Both are the fully-qualified Vertex AI Search resource path, e.g. projects/{project}/locations/{location}/collections/{collection}/dataStores/{dataStore}. tool
google_maps_grounding Google Maps Grounding Grounds answers with Google Maps data: places, directions, hours. none tool
enterprise_web_search Enterprise Web Search Web search grounding suitable for enterprise compliance requirements. none tool
code_execution Code Execution Lets the model write and run Python code for calculations or data analysis. none code_executor

google_maps_grounding only works when your deployment runs in Vertex AI enterprise mode (GOOGLE_GENAI_USE_ENTERPRISE=true). On a deployment using a plain Gemini API key instead, enabling it has no effect.

Gemini's own tool-calling layer normally won't let google_search or vertex_ai_search combine with any other tool on the same Orchestrator, a restriction unrelated to the Gemini-only rule above. AiFlow works around this automatically, so enabling either one alongside an MCP connection or a sub-Orchestrator link works exactly as you'd expect, with no extra configuration required.

The Gemini-only restriction

The six tools in the table above require the Orchestrator's model field to be a bare Gemini model name (see Orchestrators: multi-model, not just Gemini), not a provider/model LiteLLM string. This is a hard constraint. Enabling one of these tools on an Orchestrator configured with, say, anthropic/claude-sonnet-5 doesn't fail at enable time (the PUT below still succeeds), but it fails at the Orchestrator's next delegated call, with a clear AiFlow-authored error rather than an opaque one from deeper in the model integration layer. It never fails silently, but since the enable-time call succeeds regardless, check the model yourself before it comes up at run time: there's no validation tying model and enabled tools together at configuration time.

Not exposed

A few of Gemini's other built-in capabilities are not available as Orchestrator tools. Each is left out for a concrete reason:

  • An interactive shell or bash tool. Every command would need a human to approve it, and a headless run has nowhere to ask.
  • A computer-use tool. It needs a real screen to control.
  • Memory tools. They need a memory backend AiFlow does not run.
  • Human-in-the-loop input tools. There is no interface for someone to answer mid-run.

An Orchestrator's current tool configuration

GET /api/v1/orchestrators/{orchestrator_id}/tools

Requires: Viewer and above.

Returns the same catalog as GET /api/v1/orchestrator-tools, merged with this Orchestrator's current enabled and config state for each tool. A tool this Orchestrator has never had a PUT call made for comes back with enabled: false and config: null.

Field Type Notes
tool_name string The tool's identifier. Note the field name: tool_name here, name on the catalog endpoint above.
description string Same as the catalog.
enabled boolean Whether this tool is currently on for this Orchestrator.
config object or null This Orchestrator's stored configuration for the tool, or null if none was ever set.
config_schema object or null Same as the catalog.
display_name string or null Same as the catalog.
category string Same as the catalog.
kind string Same as the catalog.
gemini_only boolean Same as the catalog.
supports_approval boolean Same as the catalog: whether this tool can be gated, not whether it currently is.
requires_approval boolean Whether human approval is currently turned on for this tool on this Orchestrator. false for a tool never PUT for this Orchestrator, same as enabled. See Human approval gate.
curl https://api.your-domain.com/api/v1/orchestrators/1/tools \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.get(
    "https://api.your-domain.com/api/v1/orchestrators/1/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/orchestrators/1/tools", {
  headers: { Authorization: `Bearer ${adminToken}` },
});
const tools = await response.json();

A nonexistent orchestrator_id gets a 404.

Enabling (or configuring) a tool

PUT /api/v1/orchestrators/{orchestrator_id}/tools/{tool_name}

Requires: Admin and above.

A stricter tier than links and MCP connections

This endpoint requires Admin, not Editor, unlike Orchestrator links and Orchestrator MCP servers, which both only require Editor. If a request here fails with 403 for an Editor token that works fine on those other two endpoints, that's why, not a bug.

Field Type Required Default
enabled boolean No true
config object or null No null
requires_approval boolean No false

This is an upsert: the first PUT for a given orchestrator_id and tool_name pair creates the row, and every subsequent one replaces it entirely. There's no partial merge of any field: sending a new config object replaces the old one in full rather than deep-merging with it, and omitting requires_approval resets it to false, the same "full replace, not merge" behavior config already has (see the warning at the bottom of this page). {tool_name} must be one of the name values from the catalog above; anything else gets a 404 with {"detail": "Unknown tool '<tool_name>'"}. A nonexistent orchestrator_id also gets a 404. Setting requires_approval: true on a tool whose catalog entry has supports_approval: false gets a 400 instead, see Human approval gate below.

Config isn't validated server-side

config_schema, shown on both the catalog and per-Orchestrator GET endpoints above, exists to describe the shape of config so an admin UI can build a form from it. The PUT endpoint doesn't validate config against it. You can PUT a send_whatsapp config that's missing content_sid and from_number, and it will be accepted and stored as-is. The consequence shows up at run time instead: the tool call returns an explicit error string to the model, for send_whatsapp specifically, "Error: this tool isn't fully configured (missing content_sid/from_number).", rather than actually sending anything. Populate every field marked required in the tables above if you want the tool to work; the API itself won't stop you from skipping one.

Example: enabling send_email

curl -X PUT https://api.your-domain.com/api/v1/orchestrators/1/tools/send_email \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": true, "config": {"from_address": "Acme Support <support@acme.com>"}}'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/orchestrators/1/tools/send_email",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "enabled": True,
        "config": {"from_address": "Acme Support <support@acme.com>"},
    },
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/orchestrators/1/tools/send_email",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: { from_address: "Acme Support <support@acme.com>" },
    }),
  },
);

Example: enabling send_whatsapp

content_sid and from_number are both required for this tool to actually work; see Config isn't validated server-side above.

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

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

Set either data_store_id or search_engine_id, never both. This Orchestrator's model must be a bare Gemini model name for this tool to work; see The Gemini-only restriction above.

curl -X PUT https://api.your-domain.com/api/v1/orchestrators/1/tools/vertex_ai_search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
        "enabled": true,
        "config": {
          "data_store_id": "projects/acme-prod/locations/global/collections/default_collection/dataStores/support-kb"
        }
      }'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/orchestrators/1/tools/vertex_ai_search",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "enabled": True,
        "config": {
            "data_store_id": (
                "projects/acme-prod/locations/global/collections/"
                "default_collection/dataStores/support-kb"
            )
        },
    },
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/orchestrators/1/tools/vertex_ai_search",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      enabled: true,
      config: {
        data_store_id:
          "projects/acme-prod/locations/global/collections/default_collection/dataStores/support-kb",
      },
    }),
  },
);

Disabling a tool

Send {"enabled": false} to the same endpoint. The stored config is left as-is, so re-enabling the tool later doesn't require re-submitting its configuration:

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

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

Omitting config on this request clears it

config defaults to null on this schema. If you send {"enabled": false} with no config key, that's equivalent to sending {"enabled": false, "config": null}, which does clear the stored configuration: this endpoint replaces the row's fields with whatever the request body contains, it doesn't merge them. If you want to disable a tool while keeping its configuration for later, include the same config object you originally set, with enabled changed to false.

Human approval gate

Set requires_approval: true on a tool whose catalog entry has supports_approval: true (today send_email, send_whatsapp, and send_telegram) and a call to it stops running on the spot. Instead, it creates a PendingApproval row and the model gets back a string telling it the action is queued, not done, so it doesn't tell the caller something happened before it actually has.

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

This is a deferred queue, not a paused run: nothing here suspends the Orchestrator's own bounded step budget and wall-clock timeout waiting on a human decision that might take hours. The run finishes normally with the gated call resolved to "queued." See Approvals for the full mechanics of what happens between queuing and a decision, and the /api/v1/pending-approvals endpoints (list, approve, reject) that review it, requiring Admin and above, the same tier as this endpoint.

That page also covers being told a request is waiting: a queued request can be emailed, sent over Telegram or WhatsApp, or phoned through, so it is not left until somebody opens the dashboard.

Setting requires_approval: true on a tool with supports_approval: false (any of Gemini's own built-ins today) fails with a 400:

{ "detail": "'google_search' doesn't support approval gating." }

This only ever covers a tool from the catalog above, never a tool from a connected MCP server. A connection is handed to Google ADK as one whole toolset; ADK calls that server's individual tools directly, so there's no point where this gate could intercept one of those calls the way it does a registered built-in. If a sensitive action only exists as an MCP tool, the MCP server itself is the only place that can hold off performing it pending confirmation today; there's no way to defer or replay an MCP tool call from this side.