Multi-agent orchestration

Several agents that work toward a common goal need a supervisor that decides which agent runs and in what order. A Workflow is the supervisor when the sequence of agent calls is fixed in code, and an Autonomous Agent coordinator is the supervisor when the model decides which agent runs next. The supervisor pattern and how to choose a supervisor are explained in Multi-agent coordination, and the coordination patterns in Coordination patterns.

This page covers workflow-based orchestration of request-based Agents. For model-driven coordination (delegation, handoff, teams, moderation) without writing a workflow, see Autonomous Agents and the coordination patterns.

To choose between the two, see Choosing a supervisor.

Workflow-driven orchestration

First, look at how to define a workflow that orchestrates several agents in a predefined steps. It uses both the WeatherAgent and the ActivityAgent. First it retrieves the weather forecast and then it finds suitable activities.

@Component(id = "agent-team")
public class AgentTeamWorkflow extends Workflow<AgentTeamWorkflow.State> {

  private static final Logger logger = LoggerFactory.getLogger(AgentTeamWorkflow.class);

  public record State(String userQuery, String weatherForecast, String answer) {
    State withWeatherForecast(String f) {
      return new State(userQuery, f, answer);
    }

    State withAnswer(String a) {
      return new State(userQuery, weatherForecast, a);
    }
  }

  private final ComponentClient componentClient;

  public AgentTeamWorkflow(ComponentClient componentClient) {
    this.componentClient = componentClient;
  }

  public Effect<Done> start(String query) {
    return effects()
      .updateState(new State(query, "", ""))
      .transitionTo(AgentTeamWorkflow::askWeather) (1)
      .thenReply(Done.getInstance());
  }

  public Effect<String> getAnswer() {
    if (currentState() == null || currentState().answer.isEmpty()) {
      String workflowId = commandContext().workflowId();
      return effects().error("Workflow '" + workflowId + "' not started, or not completed");
    } else {
      return effects().reply(currentState().answer);
    }
  }

  @Override
  public WorkflowSettings settings() {
    return WorkflowSettings.builder()
      .stepTimeout(AgentTeamWorkflow::askWeather, ofSeconds(60))
      .stepTimeout(AgentTeamWorkflow::suggestActivities, ofSeconds(60))
      .defaultStepRecovery(RecoverStrategy.maxRetries(2).failoverTo(AgentTeamWorkflow::error))
      .build();
  }

  @StepName("weather")
  private StepEffect askWeather() { (2)
    var forecast = componentClient
      .forAgent()
      .inSession(sessionId())
      .method(WeatherAgent::query)
      .invoke(currentState().userQuery);

    logger.info("Weather forecast: {}", forecast);

    return stepEffects()
      .updateState(currentState().withWeatherForecast(forecast)) (3)
      .thenTransitionTo(AgentTeamWorkflow::suggestActivities);
  }

  @StepName("activities")
  private StepEffect suggestActivities() {
    var request = (4)
      currentState().userQuery +
        "\nWeather forecast: " + currentState().weatherForecast;

    var suggestion = componentClient
      .forAgent()
      .inSession(sessionId())
      .method(ActivityAgent::query)
      .invoke(request);

    logger.info("Activities: {}", suggestion);

    return stepEffects()
      .updateState(currentState().withAnswer(suggestion)) (5)
      .thenEnd();
  }

  private StepEffect error() {
    return stepEffects().thenEnd();
  }

  private String sessionId() {
    // the workflow corresponds to the session
    return commandContext().workflowId();
  }
}
1 The workflow starts by asking for the weather forecast.
2 Weather forecast is retrieved by the WeatherAgent, which must extract the location and date from the user query.
3 The forecast is stored in the state of the workflow.
4 The forecast is included in the request to the ActivityAgent.
5 The final result is stored in the workflow state.

In steps 4 we explicitly include the forecast in the request to the ActivityAgent. That is not strictly necessary because the agents share the same session memory and thereby the ActivityAgent will already have the weather forecast in the context that is sent to the AI model.

The workflow runs these steps durably: a failed call is retried by the step’s recovery strategy, and a restarted workflow continues from its current step. See Drive the agent from a workflow.

Model-driven orchestration

When the orchestration sequence itself is a model judgment, the supervisor is an Autonomous Agent coordinator. The coordinator declares a Delegation capability listing the worker agents it may call. The runtime exposes each worker to the coordinator’s model as a tool. The model picks which worker to invoke based on the workers' @Component descriptions and what previous calls returned. The runtime persists task state, retries failed steps, and bounds the loop by an iteration limit.

Workers stay as plain request-based Agents. They do not know they are being orchestrated; the coordinator handles the routing.

import akka.javasdk.agent.autonomous.AgentDefinition;
import akka.javasdk.agent.autonomous.AutonomousAgent;
import akka.javasdk.agent.autonomous.capability.Delegation;
import akka.javasdk.agent.autonomous.capability.TaskAcceptance;
import akka.javasdk.annotations.Component;

@Component(
  id = "activity-coordinator",
  description = """
  Coordinates worker agents to suggest real-world activities for a user. \
  Decides whether to consult the weather agent, the activity agent, or both, \
  and synthesizes their results into a single suggestion.\
  """
)
public class ActivityCoordinator extends AutonomousAgent { (1)

  @Override
  public AgentDefinition definition() {
    return define()
      .instructions(
        """
        When delegating to the activity agent, include the userId from the task header \
        (the "User: <userId>" line) in the request so the agent can fetch the user's \
        preferences.\
        """
      ) (2)
      .capability(TaskAcceptance.of(ActivityTasks.SUGGEST_ACTIVITIES).maxIterationsPerTask(5)) (3)
      .capability(Delegation.to(WeatherAgent.class, ActivityAgent.class)); (4)
  }
}
1 Extend AutonomousAgent. There is no command handler; the runtime drives the loop until each task completes.
2 Optional definition-level instructions for guidance specific to this coordinator. Most coordinators do not need this.
3 Declare the accepted task type. maxIterationsPerTask bounds the model loop as a safety net.
4 List the workers the coordinator may delegate to. Delegation.to accepts both request-based Agents and other Autonomous Agents.

The model picks workers based on their @Component descriptions, so a worker’s description should describe what it is for, not how it is invoked. The WeatherAgent advertises itself as the source of weather information:

@Component(
  id = "weather-agent",
  name = "Weather Agent",
  description = """
  An agent that provides weather information. It can provide current weather, \
  forecasts, and other related information.\
  """
)
public class WeatherAgent extends Agent {

The ActivityAgent advertises its own purpose the same way, and the coordinator’s model chooses between them per request.

See Autonomous Agents for more details and the other coordination capabilities (handoff, teams, moderation). The planner-agent tutorial walks through this coordinator end-to-end.

Combining the two approaches

The two approaches are not mutually exclusive. A workflow can invoke an Autonomous Agent from any of its steps, the same way it invokes a request-based Agent. This fits scenarios where the overall sequence is fixed but a particular stage benefits from model-driven coordination. For example, a research stage where a coordinator delegates to several specialists, or a review stage that runs as a team or a moderated conversation. The workflow keeps the durable state machine across stages, while the Autonomous Agent handles the elaboration within a stage.

A step that waits for an Autonomous Agent is subject to the step timeout, and a step that times out does not stop the agent. If the agent can run longer than the step timeout, let the step only start the task: store the task id in the workflow state and end the step. A Consumer on the task events (@Consume.FromEventSourcedEntity(TaskEntity.class)) then sends the result to the workflow when the task completes.

Because the agent keeps running after a step timeout, do not terminate it from a step that can time out. A stopped agent instance id cannot be reused, so choose the instance id for each attempt in an earlier step and commit it to the workflow state before the step that starts the agent.