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, andhas_remote_auth_headerbecomesfalse. - Send a non-empty string: this replaces the stored value.
Listing Orchestrators¶
Requires: Viewer and above.
Returns every Orchestrator in the deployment, active and disabled alike,
ordered by ascending id.
[
{
"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.
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¶
Requires: Viewer and above.
A nonexistent orchestrator_id gets a 404 with
{"detail": "Orchestrator 1 not found"}.
Updating an Orchestrator¶
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.
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:
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¶
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."}. |
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¶
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.
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
instructionagainst a regression suite before promoting it, without touching this Orchestrator's live configuration.