Skip to main content

Orchestrator

The Orchestrator module enables your application to run a complete AI workflow. Unlike an Agent, which interacts with a single AI, an Orchestrator manages multiple steps, such as invoking multiple agents, and following branching logic, all configured in the ART Orchestrator Builder.

Prerequisites

Before using the Orchestrator module, ensure the following:

  • The ADK is installed, authenticated, and connected. If you haven't completed this setup yet, see the Installation guide.
  • An Orchestrator has been created and deployed using the ART Orchestrator Builder.
  • You have the orchestratorId of the deployed Orchestrator.

Workflow

Your application sends a request to an Orchestrator, which executes the workflow and streams events in real time as each step progresses. Each Orchestrator communicates through its own dedicated channel.

Unlike the Agent module, which provides typed events and Run objects, the Orchestrator module uses a flexible, event-based communication model. You send named events and receive raw objects {event, content}, giving you full control over the data exchanged during workflow execution.

To simplify working with workflows, the ADK provides two core classes that you'll use throughout the Orchestrator module:

ClassDescription
OrchestratorRepresents a single orchestrator identified by its orchestratorId. Reuse the same Orchestrator instance for multiple workflow executions. The ADK automatically manages the underlying communication channel.
OrchestratorThreadRepresents a single workflow execution. Each thread has a unique threadId, which is automatically included with every event to associate it with the correct workflow execution.

Quick start

The following example demonstrates the basic workflow: obtaining an orchestrator, creating a thread, registering an event listener, and sending a user_input event to start the workflow:

// 1. Get the orchestrator.
Orchestrator *orchestrator = [self.adk orchestrator:@"your-orchestrator-id"];

// 2. Start a thread.
OrchestratorThread *thread = [orchestrator thread];

// 3. Listen for the events the workflow sends back.
[thread listen:^(NSDictionary *data) {
NSDictionary *content = data[@"content"];

if ([content[@"type"] isEqualToString:@"agent_general_response"]) {
NSLog(@"Answer: %@", content[@"message"]);
}
}];

// 4. Send the user's request.
[thread push:@"user_input"
data:@{
@"user_input": @"Plan a 3-day trip to Goa"
}
options:nil];

The following sections explain each step in more detail, including how to handle workflow events and respond to human_input_request events.

1. Connect to an orchestrator

Get an adk.orchestrator(...) instance from the connected Adk instance. The channel subscription is established lazily — it is established automatically the first time you create an OrchestratorThread by calling orchestrator.thread:

Orchestrator *orchestrator = [self.adk orchestrator:@"your-orchestrator-id"];

2. Start a thread

An OrchestratorThread represents a single workflow execution. Its threadId is automatically attached to every outbound message, ensuring that responses are routed to the correct thread:

[orchestrator thread:^(OrchestratorThread *thread, NSError *error) {
if (error || !thread) {
NSLog(@"%@", error);
return;
}
NSLog(@"Thread: %@", thread.threadId);
}];

thread() is asynchronous because it establishes the underlying channel subscription the first time it is called. To resume a previous workflow execution, pass the existing threadId:

To reconnect to a specific, already-known thread instead of generating a new one, pass its id to threadWithId:completion::

[orchestrator threadWithId:@"thread_1699999999999_ab12cd34"
completion:^(OrchestratorThread *thread, NSError *error) {

}];

3. Listen to the thread

thread listen: registers a callback that receives every event emitted for the thread. Each event is delivered as a raw NSDictionary containing two fields: event and content. Determine the event type from content['type']:

[thread listen:^(NSDictionary<NSString *, id> *message) {
NSString *event = message[@"event"] ?: @"";
NSDictionary *content = message[@"content"] ?: @{};

if ([event isEqualToString:@"agent_general_response"]) {
NSLog(@"%@", content[@"message"]);
} else if ([event isEqualToString:@"agent_error_response"]) {
NSLog(@"%@", content[@"message"]);
} else if ([event isEqualToString:@"human_input_request"]) {
NSLog(@"%@", content[@"prompt"]);
} else if ([event isEqualToString:@"agent_wait_response"]) {
NSLog(@"%@", content[@"waiting_for_agent_id"]);
} else if ([event isEqualToString:@"planner_correction_request"]) {
NSLog(@"%@", content[@"reason"]);
} else {
NSLog(@"%@", message);
}
}];

The callback argument

Each invocation of the listener receives a raw event dictionary with the following keys:

KeyTypeDescription
data[@"event"]NSString *The wire-level event name.
data[@"content"]idThe event payload. This is typically an NSDictionary<NSString *, id> *, whose values you access directly (for example, content[@"message"] or content[@"prompt"]).

Listening to a single event

To listen for one named event only, use bind:callback:. Its callback receives just that event's content, not the full {event, content} frame, and buffered payloads for that event are replayed before new ones arrive:

[thread bind:@"status_update"
callback:^(id content) {
NSLog(@"Status: %@", content);
}];

[thread remove:@"status_update"]; // Stop listening for this event.

bind: differs from listen: in two ways:

  • It invokes the callback only for the specified event.
  • It passes the event's content directly, rather than the raw event dictionary containing the event and content keys.

Use bind: when you only care about a specific workflow event. Use listen: when you need to observe the complete event stream.

4. Send the user's request

Send the user's request as a user_input event. push:data:completion: automatically stamps the thread's thread_id on the frame — you only provide the payload:

[thread push:@"user_input"
data:@{@"user_input" : @"Plan a 3-day trip to Goa"}
completion:^(NSError *error) {
if (error) {
NSLog(@"%@", error);
}
}];

The contents of the data NSDictionary are defined by your workflow. Include whatever fields your workflow expects:

[thread push:@"user_input"
data:@{
@"user_input" : @"Plan a trip",
@"preferences" : @{@"budget" : @50000, @"days" : @3}
}
completion:^(NSError *error) {
// ...
}];

Parameters

ParameterTypeDescription
eventNSString *The event name, for example user_input or user_reply.
dataNSDictionary<NSString *, id> *The event payload. Its structure is defined by your workflow.
optionsPushConfig * (nullable)Optional delivery configuration. The OrchestratorThread automatically attaches its threadId to every outbound message.

5. Human-in-the-Loop (HITL)

Like the Agent, the Orchestrator supports Human-in-the-Loop (HITL) interactions. A workflow can pause at any point to request additional input from the user. When this happens, the ADK injects a reply function into the event's content. Call this function with the user's response to resume the workflow execution:

```objectivec
[thread listen:^(NSDictionary<NSString *, id> *data) {
NSDictionary<NSString *, id> *content = data[@"content"];

id reply = content[@"reply"];
if ([content isKindOfClass:[NSDictionary class]] &&
[reply isKindOfClass:NSClassFromString(@"NSBlock")]) {

NSLog(@"Workflow asks: %@", content[@"prompt"]);

// Collect the answer from your UI, then call the reply callback.
void (^replyBlock)(NSDictionary<NSString *, id> *) = reply;
replyBlock(@{
@"user_input": @"Budget 50k, December 20-25"
});

return;
}

// …Handle other events…
}];

The reply function is injected only into human-input requests — that is, when the event is human_input_request or content['type'] is human_input_request.

6. Clean up

When a workflow execution is complete, dispose of the OrchestratorThread. This unregisters the thread from the underlying channel and removes all associated listeners:

[thread dispose];

dispose is idempotent. It does not close the underlying channel subscription or the WebSocket connection, so other OrchestratorThread instances continue to operate normally. After a thread has been disposed, calls to push:data:options:, listen:, bind:callback:, or other thread operations complete with a StateError. Use the isDisposed property to determine whether a thread is still active.

Diagnostics with trace

While a workflow is executing, the orchestrator and its agents may emit trace events containing diagnostic and telemetry information, such as heartbeats, checkpoints, and execution progress. Use these events for debugging, logging, and displaying workflow progress in your application:

[thread listenTrace:^(id frame) {
NSLog(@"%@", frame);
}];

Complete example

- (void)runWorkflow:(Adk *)adk {
Orchestrator *orchestrator = [adk orchestrator:@"your-orchestrator-id"];

[orchestrator thread:^(OrchestratorThread *thread, NSError *error) {
if (error || !thread) {
NSLog(@"%@", error);
return;
}

[thread listen:^(NSDictionary<NSString *, id> *message) {
NSDictionary *content = message[@"content"];
void (^reply)(id) = content[@"reply"];
if (reply) {
reply(@{@"message" : @"Budget 50k, 20\u201325 December"});
return;
}
NSLog(@"%@", message);
}];

[thread push:@"user_input"
data:@{@"user_input" : @"Plan a 3-day trip to Goa"}
completion:^(NSError *pushError) {
if (pushError) {
NSLog(@"%@", pushError);
}
}];
}];
}

API reference

MemberSignatureDescription
threadIdNSString *The thread identifier automatically attached to every outbound message.
push:data:options:- (void)push:(NSString *)event data:(NSDictionary<NSString *, id> *)data options:(PushConfig * _Nullable)options completion:(void (^)(NSError * _Nullable error))completion;Sends an event to the workflow, automatically scoped to this thread.
listen:- (void)listen:(void (^)(NSDictionary<NSString *, id> *data))callback;Registers a listener that receives every event as a raw event dictionary containing the event and content keys.
bind:callback:- (void)bind:(NSString *)event callback:(void (^)(id content))callback;Registers a listener for a single event. The callback receives the event's content directly.
listenTrace:- (void)listenTrace:(void (^)(id data))callback;Registers a listener for trace diagnostic events.
remove:- (void)remove:(NSString *)event;Removes all listeners registered for the specified event.
dispose- (void)dispose;Disposes the thread by unregistering it and removing all associated listeners. Safe to call multiple times.
isDisposedBOOLIndicates whether the thread has been disposed.

Agent vs. Orchestrator

AgentOrchestrator
Best forCommunicating with a single AI agentExecuting multi-step workflows
Create a threadthread (synchronous)thread: (completion-based)
Send inputrun:Runpush:data:options:
Receive resultsdone: (single final response)listen: (event stream)
Event modelStrongly typed eventsRaw event dictionaries
Human-in-the-LoopsendFeedback:Injected reply callback
Lifecycle completionThe Run completes automaticallyCall dispose when the workflow execution is complete