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

Agent-to-Orchestrator delegation

This page covers how a native, realtime Agent gets permission to hand a task off to an Orchestrator mid-conversation, and how to confirm your deployment has this turned on before you rely on any of it. It's a separate mechanism from Agent-to-Agent delegation (delegate_to_agent) and from the sub-Orchestrator links covered on the previous page. This page covers specifically the native-Agent side of that boundary; see Why delegation can't form a loop for why the boundary exists and can't be crossed the other way.

Getting an Agent delegating into an Orchestrator takes two steps:

  1. Create an AgentOrchestratorLink (this page): an explicit allowlist entry saying "Agent X may delegate into Orchestrator Y."
  2. Enable the delegate_to_orchestrator tool on that Agent (cross-referenced below, with full mechanics on the tools guide): the tool the Agent actually calls at conversation time to use that permission.

Checking whether Orchestrators are enabled

GET /api/v1/features

Requires: Viewer and above.

Orchestrators are gated behind a deployment-wide setting, off by default (see Orchestrators: whether your deployment has this turned on). This endpoint lets a client check that setting programmatically instead of assuming it either way.

When it is off, none of those routes are mounted at all. That covers Orchestrators, Orchestrator tools, Orchestrator links, Orchestrator MCP servers, the Console, and the rest of this page. A request to any of them returns a plain 404, and the body says nothing that separates a disabled feature from a resource that never existed.

So if you are seeing unexplained 404s on an Orchestrator endpoint, check this setting first.

The response has exactly one field:

Field Type Notes
orchestrators_enabled boolean Whether this deployment has the Orchestrators feature turned on.
curl https://api.your-domain.com/api/v1/features \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

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

This endpoint itself is always mounted, regardless of the setting's value, so a client always has a way to discover the flag, even when it's off.

Field Type Notes
id integer Response only.
from_agent_id integer Response only. The Agent in the URL path.
to_orchestrator_id integer The target Orchestrator's id. Required on create.
to_orchestrator_slug string Response only. The target Orchestrator's slug, denormalized onto the link.
to_orchestrator_name string Response only. The target Orchestrator's name, denormalized onto the link.
enabled boolean Defaults to true on create.

Notice what's not here compared to an Orchestrator link: there's no invocation_style. An Agent always reaches an Orchestrator the same way, by explicitly calling the delegate_to_orchestrator tool and getting one answer back. There's no sub_agent-style hand-off of control from the native-Agent layer; that style only exists between Orchestrators themselves.

GET /api/v1/agents/{agent_id}/orchestrator-links

Requires: Viewer and above.

Returns every Orchestrator this Agent is currently allowed to delegate into, ordered by ascending id.

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

response = httpx.get(
    "https://api.your-domain.com/api/v1/agents/1/orchestrator-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/orchestrator-links",
  { headers: { Authorization: `Bearer ${adminToken}` } },
);
const links = await response.json();
[
  {
    "id": 1,
    "from_agent_id": 1,
    "to_orchestrator_id": 1,
    "to_orchestrator_slug": "order-lookup",
    "to_orchestrator_name": "Order Lookup",
    "enabled": true
  }
]

A nonexistent agent_id gets a 404 with {"detail": "Agent 1 not found"}.

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

Requires: Admin and above.

A stricter tier than sub-Orchestrator links

This endpoint requires Admin, not Editor. Orchestrator links, which connect one Orchestrator to another, only require Editor; granting a native Agent delegation permission requires Admin. If a request here fails with 403 for an Editor token, that's why.

Field Type Required Default
to_orchestrator_id integer Yes none
enabled boolean No true
curl -X POST https://api.your-domain.com/api/v1/agents/1/orchestrator-links \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"to_orchestrator_id": 1}'
import httpx

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

Returns 201 Created with the full link object. You get a 404 if agent_id doesn't exist, a 404 with {"detail": "Orchestrator 1 not found"} if to_orchestrator_id doesn't exist, and a 409 with {"detail": "A link to 'order-lookup' already exists"} if this Agent already has a link to that Orchestrator. There's no cycle check here, unlike Orchestrator links: an Agent can never be a delegation target, only a source, so a link from an Agent to an Orchestrator can't participate in a graph cycle, no matter how many sub-Orchestrator links exist beneath that Orchestrator.

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

Requires: Admin and above.

The only field this endpoint accepts is enabled (boolean, optional). There's no way to repoint an existing link at a different to_orchestrator_id; delete it and create a new one instead.

curl -X PATCH https://api.your-domain.com/api/v1/agents/1/orchestrator-links/1 \
  -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/orchestrator-links/1",
    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/orchestrator-links/1",
  {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({ enabled: false }),
  },
);
const link = await response.json();

Returns 200 OK with the full, updated link object. A nonexistent link_id (or one that doesn't belong to agent_id) gets a 404.

DELETE /api/v1/agents/{agent_id}/orchestrator-links/{link_id}

Requires: Admin and above.

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

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

Returns 204 No Content. A nonexistent link_id gets a 404.

Enabling the delegation tool

A link on its own only makes delegation possible. The Agent also needs its delegate_to_orchestrator built-in tool turned on before it will actually use it, through a PUT to the same per-Agent tools endpoint every other built-in tool uses:

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

See Tools for the full mechanics of this endpoint, its exact request and response shape, and every other built-in tool an Agent can enable. It isn't repeated here since it isn't specific to Orchestrator delegation.

What happens at conversation time

Once both steps above are done, the Agent decides for itself, mid- conversation, when a request is out of its own scope. It calls delegate_to_orchestrator with the target Orchestrator's slug and a plain-language task instruction. That call runs the target Orchestrator's full agentic loop, its own model, its own tools, its own sub-Orchestrators if any, and the Agent speaks or types the resulting answer back in its own voice. From the user's perspective it's the same experience as delegating to another Agent, just backed by a fundamentally more capable delegate on the other end. See What an Orchestrator is for what makes that delegate's capability different from a second Agent.

If you've subscribed to webhook notifications, an orchestrator_task.completed or orchestrator_task.failed event fires once the delegated run finishes. That gives your own systems a deterministic "it happened" signal, separate from the answer already spoken back into the conversation.