Tools

Tools are the units of work a route's entry can invoke. They live in a tools/ subdirectory inside a route, are discovered automatically, and have their input and output types inferred from TypeScript source — no Zod schemas required for tools. (Route state in state.ts does use Zod; see State.)

A minimal tool

src/app/(public)/hello/[tenant]/tools/greet.ts
export default async (input: { readonly tenant: string }) => {
  return { greeting: `Hello, ${input.tenant}!` }
}

That's it. Dawn extracts the input and output types at build time using the TypeScript compiler API and writes them into .dawn/dawn.generated.d.ts. The tool becomes available as ctx.tools.greet inside workflow/graph route entries (fully typed), and is wired into generated agent deployment entries by dawn build.

The runtime signature

The full runtime signature accepted by tool discovery is (input, ctx) => ..., where ctx is { signal: AbortSignal, middleware?: Readonly<Record<string, unknown>> }:

src/app/(public)/hello/[tenant]/tools/greet.ts
export default async (
  input: { readonly tenant: string },
  ctx: { signal: AbortSignal; middleware?: Readonly<Record<string, unknown>> },
) => {
  // Cooperate with cancellation — the AbortSignal is always present.
  const res = await fetch(`https://example.com/hello?tenant=${input.tenant}`, {
    signal: ctx.signal,
  })
  // ctx.middleware is the readonly bag populated by allow({ ... }) in src/middleware.ts.
  return { greeting: await res.text() }
}

The second parameter is optional but recommended for any tool that does network I/O, holds long-running work, or needs middleware-derived context. See Middleware for how defineMiddleware and allow(context) populate ctx.middleware.

Invoking a tool

Inside a workflow or graph route, tools are invoked through ctx.tools.<name>(...):

src/app/(public)/hello/[tenant]/index.ts
// workflow form
import type { RuntimeContext } from "@dawn-ai/sdk"
import type { RouteTools } from "dawn:routes"
import type { z } from "zod"
import type state from "./state.js"
 
type HelloState = z.infer<typeof state>
 
export async function workflow(
  state: HelloState,
  ctx: RuntimeContext<RouteTools<"/hello/[tenant]">>,
) {
  const result = await ctx.tools.greet({ tenant: state.tenant })
  return { ...state, greeting: result.greeting }
}

ctx.tools.greet has full IntelliSense — input shape, return shape, everything.

Inside an agent route, you do not call tools yourself. dawn build wires the route's tools into the generated LangGraph entry and the LLM invokes them as needed. See Routes for both forms side by side.

Input and output rules

  1. 1

    Use a typed input parameter

    Annotate the input parameter with an inline type. This is what Dawn's compiler pass extracts.

    ts
    export default async (input: { readonly tenant: string; readonly limit?: number }) => { ... }
  2. 2

    Mark fields readonly

    readonly is preserved through type generation. Use it on every field — tool inputs are pure data, never mutated.

  3. 3

    Keep inputs and outputs JSON-serializable

    Dawn serializes through the runtime boundary. Classes, Dates, Maps, functions — none of them survive. Use primitives, plain objects, and arrays.

  4. 4

    Return a plain object

    Always return an object literal, not a primitive or array. Dawn's type inference handles shape-based returns cleanly; a bare return someString muddles the generated types.

The generated declarations

dawn typegen writes two artifacts:

  • .dawn/dawn.generated.d.ts — the ambient type module that backs import type { RouteTools } from "dawn:routes". The emitted shape pivots over a RouteTools<P> lookup keyed by each discovered route pathname; dawn typegen populates the entries with concrete tool signatures inferred from source.
  • .dawn/routes/<routeSlug>/tools.json — per-route tool-schema manifests consumed by dawn build when emitting LangGraph entries.

To inspect the exact shape Dawn emits in your app, run dawn typegen and open .dawn/dawn.generated.d.ts.

Regenerate manually if needed:

text
dawn typegen

Common patterns

  • External API wrappers — one tool per endpoint. Input shape mirrors the endpoint's parameters; output is the parsed response.
  • Database reads — input includes the filters; output is the record(s). Keep queries cheap — tools are meant to be fast.
  • LLM calls — input is the prompt variables; output is the parsed model response. Defer orchestration to the route entry.
  • Pure transformations — no side effects, just shape-to-shape mapping. Dawn's testing makes these trivial to verify.

Related