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

Orchestrators

Orchestrators are AiFlow's multi-model, tool-using task-delegation layer: a non-realtime entity that a native Agent can hand a task to mid-conversation and get back a real, model-generated answer. This page is the full CRUD reference for the Orchestrator resource: its fields, its lifecycle, and every endpoint that creates, reads, updates, or disables one. For the concepts behind these fields, local versus remote execution, the provider/model string format, and why a delegation loop can't form, see Orchestrators. This page assumes you've read it.

Gated behind a deployment setting

Every endpoint on this page, and on Built-in templates, Orchestrator tools, Orchestrator links, Orchestrator MCP servers, Agent-to-Orchestrator delegation, and the Console, only exists on a deployment where an admin has turned Orchestrators on. When the setting is off, none of these routes are mounted: a request to any of them returns a plain 404, not a "feature disabled" error. If you're not sure whether it's on, check GET /api/v1/features first, described in Agent-to-Orchestrator delegation.

Admin session token required

Every endpoint on this page authenticates with an admin session token, not an API key; see Authentication. Each endpoint below states the minimum admin role it requires: Viewer and above means any logged-in admin, Editor and above means Editor, Admin, or Owner, and Admin and above means Admin or Owner. A request from a role below that minimum gets a 403 with {"detail": "Insufficient role for this action"}.

The Orchestrator object

Field Type Notes
id integer Response only. Assigned on creation.
slug string Response only after creation (see Creating an Orchestrator for its create-time rules). A short, URL-safe identifier, e.g. order-lookup. Immutable: it can be set once, on create, and is not a field PATCH accepts.
name string Required on create, 1 to 120 characters. A human-readable display name.
description string or null Optional, up to 500 characters. Defaults to null.
status string enum One of draft, active, disabled. Defaults to active on create.
execution_mode string enum One of local, remote_a2a. Defaults to local. See Local or remote execution.
instruction string or null The Orchestrator's system prompt. Optional, defaults to null. No length limit on PATCH; uploading it as a file instead (see Setting the instruction from a file) caps it at 200,000 bytes.
model string Defaults to the deployment's GEMINI_DEFAULT_MODEL setting (gemini-3.5-flash-lite out of the box). A bare Gemini model name, or a provider/model LiteLLM string (e.g. anthropic/claude-sonnet-5). See the model-string table in Orchestrators: multi-model, not just Gemini.
temperature number Defaults to 0.7. Must be between 0.0 and 2.0 inclusive.
max_iterations integer Defaults to 8. Must be between 1 and 50 inclusive. The step budget for one delegated run, counted in model turns: a tool call and the answer after it are two steps, and the tool's own result costs nothing. A run that spends the budget returns whatever text it has, or a "completed with no final answer" message if it never got that far.
remote_a2a_url string or null Optional, up to 500 characters, defaults to null. The A2A endpoint URL, only meaningful when execution_mode is remote_a2a.
remote_auth_header_value string or null Write-only. Accepted on create and update, never returned in any response. Encrypted at rest. See Remote auth headers are write-only below.
has_remote_auth_header boolean Response only. true if a remote_auth_header_value is currently stored for this Orchestrator, false otherwise. This is how you check whether one is set without ever seeing the value itself.
include_global_context_docs boolean Defaults to false.

Remote auth headers are write-only

remote_auth_header_value follows the same masking convention as an Orchestrator's MCP connections (see Orchestrator MCP servers): AiFlow stores only an encrypted copy, and no endpoint ever returns the raw value in a response body. has_remote_auth_header is the only signal that one is set.

On PATCH, this field has three distinct states, because null and "clear it" aren't the same thing:

  • Omit the field entirely: the stored value, if any, stays unchanged.
  • Send an empty string (""): this clears the stored value, and has_remote_auth_header becomes false.
  • Send a non-empty string: this replaces the stored value.

Listing Orchestrators

GET /api/v1/orchestrators

Requires: Viewer and above.

Returns every Orchestrator in the deployment, active and disabled alike, ordered by ascending id.

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

response = httpx.get(
    "https://api.your-domain.com/api/v1/orchestrators",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
orchestrators = response.json()
const response = await fetch("https://api.your-domain.com/api/v1/orchestrators", {
  headers: { Authorization: `Bearer ${adminToken}` },
});
const orchestrators = await response.json();
[
  {
    "id": 1,
    "slug": "order-lookup",
    "name": "Order Lookup",
    "description": null,
    "status": "active",
    "execution_mode": "local",
    "instruction": "You look up order status and shipping details. Be terse.",
    "model": "gemini-3.5-flash-lite",
    "temperature": 0.7,
    "max_iterations": 8,
    "remote_a2a_url": null,
    "has_remote_auth_header": false,
    "include_global_context_docs": false
  }
]

Creating an Orchestrator

Capped by your licence

This deployment's licence sets an orchestrators limit. Creating one more past that cap returns 402 rather than succeeding; see Licence limits.

POST /api/v1/orchestrators

Requires: Admin and above.

Field Type Required Default
slug string Yes none
name string Yes none
description string or null No null
status string enum (draft, active, disabled) No active
execution_mode string enum (local, remote_a2a) No local
instruction string or null No null
model string No GEMINI_DEFAULT_MODEL
temperature number, 0.0-2.0 No 0.7
max_iterations integer, 1-50 No 8
remote_a2a_url string or null No null
remote_auth_header_value string or null No null
include_global_context_docs boolean No false

slug must be 1 to 80 characters and match ^[a-z0-9]+(-[a-z0-9]+)*$: lowercase letters and digits, grouped into segments joined by single hyphens, like order-lookup or tier2-support. No uppercase letters, and no leading, trailing, or doubled hyphens. A slug already used by another Orchestrator returns a 409.

curl -X POST https://api.your-domain.com/api/v1/orchestrators \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
        "slug": "order-lookup",
        "name": "Order Lookup",
        "instruction": "You look up order status and shipping details. Be terse.",
        "model": "gemini-3.5-flash-lite"
      }'
import httpx

response = httpx.post(
    "https://api.your-domain.com/api/v1/orchestrators",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "slug": "order-lookup",
        "name": "Order Lookup",
        "instruction": "You look up order status and shipping details. Be terse.",
        "model": "gemini-3.5-flash-lite",
    },
)
response.raise_for_status()
orchestrator = response.json()
const response = await fetch("https://api.your-domain.com/api/v1/orchestrators", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${adminToken}`,
  },
  body: JSON.stringify({
    slug: "order-lookup",
    name: "Order Lookup",
    instruction: "You look up order status and shipping details. Be terse.",
    model: "gemini-3.5-flash-lite",
  }),
});
const orchestrator = await response.json();

Returns 201 Created with the full Orchestrator object shown in Listing Orchestrators above.

Getting one Orchestrator

GET /api/v1/orchestrators/{orchestrator_id}

Requires: Viewer and above.

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

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

A nonexistent orchestrator_id gets a 404 with {"detail": "Orchestrator 1 not found"}.

Updating an Orchestrator

PATCH /api/v1/orchestrators/{orchestrator_id}

Requires: Editor and above.

Every field is optional; only the fields present in the request body are changed. slug isn't accepted here at all, since it's immutable after creation (see the field table above). A request that includes it gets a 422 schema-validation error, because slug isn't a recognized field on this endpoint's body.

curl -X PATCH https://api.your-domain.com/api/v1/orchestrators/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"temperature": 0.3, "max_iterations": 12}'
import httpx

response = httpx.patch(
    "https://api.your-domain.com/api/v1/orchestrators/1",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={"temperature": 0.3, "max_iterations": 12},
)
response.raise_for_status()
orchestrator = response.json()
const response = await fetch("https://api.your-domain.com/api/v1/orchestrators/1", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${adminToken}`,
  },
  body: JSON.stringify({ temperature: 0.3, max_iterations: 12 }),
});
const orchestrator = await response.json();

Returns 200 OK with the full, updated Orchestrator object. A nonexistent orchestrator_id gets a 404.

Switching to remote execution

execution_mode and remote_a2a_url are ordinary fields on this same endpoint; there's no separate "switch to remote" action:

curl -X PATCH https://api.your-domain.com/api/v1/orchestrators/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
        "execution_mode": "remote_a2a",
        "remote_a2a_url": "https://your-agent.example.run.app",
        "remote_auth_header_value": "Bearer eyJhbGciOi..."
      }'
import httpx

response = httpx.patch(
    "https://api.your-domain.com/api/v1/orchestrators/1",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "execution_mode": "remote_a2a",
        "remote_a2a_url": "https://your-agent.example.run.app",
        "remote_auth_header_value": "Bearer eyJhbGciOi...",
    },
)
response.raise_for_status()
const response = await fetch("https://api.your-domain.com/api/v1/orchestrators/1", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${adminToken}`,
  },
  body: JSON.stringify({
    execution_mode: "remote_a2a",
    remote_a2a_url: "https://your-agent.example.run.app",
    remote_auth_header_value: "Bearer eyJhbGciOi...",
  }),
});

AiFlow never deploys anything on your behalf here. You, or your infrastructure team, deploy and host the remote Orchestrator yourselves, then paste the resulting endpoint URL into remote_a2a_url, along with an optional auth header value to send with every call. The response never echoes back remote_auth_header_value; check has_remote_auth_header to confirm it was stored.

Setting the instruction from a file

POST /api/v1/orchestrators/{orchestrator_id}/instruction-file

Requires: Editor and above.

An alternative to pasting text directly into instruction on PATCH: upload a .md, .markdown, or .txt file, and its raw text becomes the instruction field, replacing whatever was there before. AiFlow doesn't keep the file itself or a history of previous uploads; it decodes the bytes once and writes them into the same instruction column a PATCH would set. Uploading again just replaces it again, the same as PATCH-ing different text would.

This is a multipart/form-data request with a single field named file.

Constraint Detail
Accepted extensions .md, .markdown, or .txt (case-insensitive). A file with any other extension gets a 422. If the upload carries no filename at all, this check is skipped.
Max size 200,000 bytes. Larger gets a 422 with {"detail": "File is too large (max 200,000 bytes)."}.
Encoding Must be valid UTF-8. A file that fails to decode as UTF-8 gets a 422 with {"detail": "File must be UTF-8 encoded text."}.
curl -X POST https://api.your-domain.com/api/v1/orchestrators/1/instruction-file \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -F "file=@order-lookup-instructions.md;type=text/markdown"
import httpx

with open("order-lookup-instructions.md", "rb") as f:
    response = httpx.post(
        "https://api.your-domain.com/api/v1/orchestrators/1/instruction-file",
        headers={"Authorization": f"Bearer {admin_token}"},
        files={"file": ("order-lookup-instructions.md", f, "text/markdown")},
    )
response.raise_for_status()
orchestrator = response.json()
const form = new FormData();
form.append("file", fileInput.files[0], "order-lookup-instructions.md");

const response = await fetch(
  "https://api.your-domain.com/api/v1/orchestrators/1/instruction-file",
  {
    method: "POST",
    headers: { Authorization: `Bearer ${adminToken}` },
    body: form,
  },
);
const orchestrator = await response.json();

Don't set Content-Type manually

Leave the Content-Type header off entirely in Python and TypeScript. Both httpx and fetch set the correct multipart/form-data; boundary=... value themselves once you pass a files or FormData body. Setting the header yourself, without the boundary, breaks the upload.

Returns 200 OK with the full, updated Orchestrator object, instruction now holding the uploaded file's decoded text.

Disabling an Orchestrator

DELETE /api/v1/orchestrators/{orchestrator_id}

Requires: Admin and above.

This is a soft disable, not a hard delete. It sets status to disabled and leaves the row, and everything attached to it, its tool configuration, its sub-Orchestrator links, its MCP connections, in place. That preserves its configuration history and lets you re-enable it later with a PATCH that sets status back to active. Any Agent-to-Orchestrator link pointing at it also stays intact; a delegated call to a disabled Orchestrator simply fails at run time instead of succeeding.

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

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

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

Where to go next

  • Orchestrator tools: enabling built-in tools (send_email, send_whatsapp, web search, code execution, and more).
  • Orchestrator links: building a graph of sub-Orchestrators underneath this one.
  • Orchestrator MCP servers: connecting this Orchestrator (or every Orchestrator, via the shared pool) to external MCP servers.
  • Agent-to-Orchestrator delegation: granting a native Agent permission to hand a task to this Orchestrator.
  • Console: chatting with an Orchestrator directly, for debugging and exploration.
  • Evals: grading a candidate instruction against a regression suite before promoting it, without touching this Orchestrator's live configuration.