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

Agent-to-agent delegation

With more than one Agent in a deployment, one can hand a narrowly-scoped task to another and get back a one-shot text answer, without ending its own session. A caller talking to a sales agent can have a billing question quietly routed to the billing agent, get a real answer drawn from that agent's own persona and knowledge, and hear it spoken back in the sales agent's own voice, all inside one call.

Not the same thing as Orchestrator delegation

AiFlow has a second, more capable delegation path: an Agent handing a task to an Orchestrator instead of another Agent, covered in a separate guide cluster on Orchestrators (see Orchestrators for the conceptual difference). This page is Agent-to-agent only: single-hop, text-only, and restricted to links an admin has explicitly created. An Orchestrator can run a full multi-step, multi-model, tool-using task. Agent-to-agent delegation is simpler than that.

The three constraints

  • Explicit, admin-configured links only. An agent can delegate only to an agent it has an enabled AgentDelegationLink to, created through the endpoints below. There is no "any agent can ask any other agent anything" mode.
  • Single-hop only. The delegated-to agent's own answer can't itself trigger a further delegation. Its system prompt, built fresh for that one task, never includes any tool declarations at all, so there's no mechanism for it to delegate onward even if a link existed. Depth is 1 by construction.
  • Text-only. No voice or video passes between agents, even on a phone or widget call where the caller's own side is voice. The delegated task and the answer that comes back are both plain text; the calling agent speaks that text back in its own voice.

The delegated-to agent has 20 seconds to produce an answer. A slower response is treated as a timeout, and the calling agent's tool call returns an error instead of hanging the live conversation.

Admin role required

GET /api/v1/agents/{agent_id}/delegation-links needs any logged-in admin (Owner, Admin, Editor, or Viewer). POST, PATCH, and DELETE all need Owner or Admin.

Field reference

Field Type Required on create Default Description
to_agent_id integer Yes None The numeric id of the agent being delegated to. Cannot equal the agent_id in the URL, an agent can't delegate to itself.
enabled boolean No true Whether this link is currently usable. Checked at the moment the delegate_to_agent tool is called, so disabling a link takes effect immediately, no need to also disable the tool on the calling agent.

Response shape

{
  "id": 3,
  "from_agent_id": 1,
  "to_agent_id": 2,
  "to_agent_slug": "billing-bot",
  "to_agent_name": "Billing Bot",
  "enabled": true
}

to_agent_slug and to_agent_name are denormalized onto every response so a list of links is readable without a separate lookup per row. Both always reflect the target agent's current slug and name at read time.

POST /api/v1/agents/{agent_id}/delegation-links

This creates a link letting agent 1 delegate to agent 2:

curl -X POST https://api.your-domain.com/api/v1/agents/1/delegation-links \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"to_agent_id": 2}'
import httpx

response = httpx.post(
    "https://api.your-domain.com/api/v1/agents/1/delegation-links",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"to_agent_id": 2},
)
response.raise_for_status()
link = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/delegation-links",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ to_agent_id: 2 }),
  },
);
const link = await response.json();

Returns 201 Created with the shape shown in Response shape above.

Failure Status When
Self-delegation 422 to_agent_id equals the agent_id in the URL.
Target not found 404 No agent exists with that id.
Duplicate link 409 A link from this agent to that target already exists (enabled or not); PATCH the existing one instead of creating a second.
GET /api/v1/agents/{agent_id}/delegation-links
curl https://api.your-domain.com/api/v1/agents/1/delegation-links \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

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

Only links where this agent is the source (from_agent_id) are returned. A link letting another agent delegate to this one shows up on that other agent's own list, not here.

PATCH /api/v1/agents/{agent_id}/delegation-links/{link_id}

The only field this endpoint accepts is enabled (boolean, optional). to_agent_id is immutable after creation; delete and recreate the link if the target needs to change.

curl -X PATCH https://api.your-domain.com/api/v1/agents/1/delegation-links/3 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"enabled": false}'
import httpx

response = httpx.patch(
    "https://api.your-domain.com/api/v1/agents/1/delegation-links/3",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"enabled": False},
)
response.raise_for_status()
link = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/delegation-links/3",
  {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: false }),
  },
);
const link = await response.json();
DELETE /api/v1/agents/{agent_id}/delegation-links/{link_id}
curl -X DELETE https://api.your-domain.com/api/v1/agents/1/delegation-links/3 \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.delete(
    "https://api.your-domain.com/api/v1/agents/1/delegation-links/3",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/delegation-links/3",
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${adminToken}` },
  },
);

Returns 204 No Content.

Turning the tool on

A delegation link by itself doesn't let an agent delegate: it only makes the target eligible. The calling agent also needs the delegate_to_agent tool enabled. No config is required; the model supplies the target's slug and task instruction as call arguments. See Tools for the general PUT /api/v1/agents/{agent_id}/tools/{tool_name} shape. The call for this specific tool is:

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

With both the link and the tool in place, agent 1's model decides on its own, mid-conversation, when a caller's question is clearly outside its own scope. It calls delegate_to_agent with agent 2's slug and a plain-language task description, then relays whatever text answer comes back.

Next

  • Tools: the full built-in tool catalog, including delegate_to_agent.
  • Agents: the resource a delegation link connects two of.
  • Orchestrators: the more capable, multi-model delegation path this page's constraints do not reach.