Dispatch from a Route

You want one route to invoke another — for example, a coordinator agent that delegates a step to a specialized research subagent. Here's how.

Idiomatic: use task() with subagents

The idiomatic approach is to use Dawn's built-in subagent dispatch. Add a subagents/researcher/ directory next to your coordinator route, and the runtime auto-generates a task tool for the coordinator.

text
src/app/(public)/research/
  index.ts              ← coordinator agent
  subagents/
    researcher/
      index.ts          ← specialist subagent
      tools/
        webSearch.ts
src/app/(public)/research/subagents/researcher/index.ts
import { agent } from "@dawn-ai/sdk"
 
export default agent({
  model: "gpt-5-mini",
  description: "Search the web and return concise findings for a given query.",
  systemPrompt: "You are a research specialist. Search thoroughly and cite your sources.",
})

This is an internal task tool call emitted by the coordinator's model, not TypeScript route code that an author imports or calls:

ts
task({
  subagent: "researcher",
  input: "Find the latest LLM benchmark results for 2025.",
})

Dawn wires the child route, runs it, and returns its final output as the tool result — all within the same process. See Subagents for the full convention and discovery rules.

Advanced: cross-service dispatch

Use raw HTTP when you need to call an agent in a different service (a separately deployed Dawn project or any AP-compatible endpoint). This is not the recommended path for routes within the same project.

src/app/(public)/orchestrator/[job]/index.ts
import type { RuntimeContext } from "@dawn-ai/sdk"
import type { z } from "zod"
import type state from "./state.js"
 
type OrchestratorState = z.infer<typeof state> & { readonly job: string }
 
export async function workflow(state: OrchestratorState, _ctx: RuntimeContext) {
  // The dev server binds an EPHEMERAL port unless `dawn dev --port <n>` is
  // passed. Set DAWN_RUNTIME_URL explicitly rather than hard-coding a port.
  const baseUrl = process.env.DAWN_RUNTIME_URL
  if (!baseUrl) throw new Error("DAWN_RUNTIME_URL is not set")
 
  // Create a thread first.
  const threadRes = await fetch(`${baseUrl}/threads`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({}),
  })
  if (!threadRes.ok) {
    throw new Error(`thread creation failed: ${threadRes.status}`)
  }
  const { thread_id } = (await threadRes.json()) as { thread_id: string }
 
  // Run the remote route and wait for the result.
  const res = await fetch(`${baseUrl}/threads/${thread_id}/runs/wait`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      route: "/research#agent",
      input: { messages: [{ role: "user", content: state.job }] },
    }),
  })
 
  if (!res.ok) {
    throw new Error(`dispatch failed: ${res.status}`)
  }
 
  const remoteState = (await res.json()) as {
    readonly messages?: readonly { readonly content?: unknown }[]
  }
  const lastMessage = remoteState.messages?.at(-1)
  const summary = typeof lastMessage?.content === "string" ? lastMessage.content : ""
  return { ...state, summary }
}

Notes on cross-service dispatch

  • EPHEMERAL port. dawn dev picks a random port unless you pass --port <n>. Never hard-code 127.0.0.1:3001 — pass DAWN_RUNTIME_URL via env instead. With a fixed port: dawn dev --port 3001.
  • AP body shape. The request body is { route, input } where route is the assistant_id string (e.g. "/research#agent") and input is the route's state payload.
  • Forward headers when needed. If the target route's middleware expects auth headers, propagate them on the inner fetch — middleware runs on every runs/wait and runs/stream call.
  • Cancellation does not cross the service boundary. Parent cancellation or shutdown does not automatically cancel the remote Agent Protocol run. Disconnecting the wait request is an AP viewer disconnect, so the remote run intentionally continues. To propagate cancellation, retain the remote thread id and call POST /threads/:thread_id/cancel when the caller's own ctx.signal aborts.
  • Use /threads/:id/runs/stream for long-running children. Same body, SSE response — see Stream Output.