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.
src/app/(public)/research/
index.ts ← coordinator agent
subagents/
researcher/
index.ts ← specialist subagent
tools/
webSearch.tsimport { 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:
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.
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 devpicks a random port unless you pass--port <n>. Never hard-code127.0.0.1:3001— passDAWN_RUNTIME_URLvia env instead. With a fixed port:dawn dev --port 3001. - AP body shape. The request body is
{ route, input }whererouteis theassistant_idstring (e.g."/research#agent") andinputis 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 everyruns/waitandruns/streamcall. - Use
/threads/:id/runs/streamfor long-running children. Same body, SSE response — see Stream output.