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.",
})

The coordinator calls the subagent by name at runtime:

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({}),
  })
  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: { query: state.job },
    }),
  })
 
  if (!res.ok) {
    throw new Error(`dispatch failed: ${res.status}`)
  }
 
  const result = (await res.json()) as { readonly output: string }
  return { ...state, summary: result.output }
}

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.
  • Use /threads/:id/runs/stream for long-running children. Same body, SSE response — see Stream output.

Related