State

Routes may declare a state contract in a colocated state.ts file. State is the JSON payload passed to the route entry and returned by the route runtime.

The shape

When present, state.ts default-exports a Zod schema or Standard Schema value. Agent routes discover and apply state defaults from it, and you can derive the TypeScript type via z.infer<typeof state>:

import { z } from "zod"
 
export default z.object({
  tenant: z.string().default(""),
  /** Accumulated context from tool call results */
  context: z.string().default(""),
})

Agent-state discovery extracts defaults by validating {}. A schema that rejects {} is skipped, so its agent state defaults and generated types are not produced. Make every top-level field accept missing input, usually with .default(...). Agent routes apply defaults from a successfully discovered schema automatically. Plain workflows must parse their unknown input explicitly, as above; their schema defaults appear only after state.parse(input) succeeds.

Dynamic segments are route params

If your route's directory contains a dynamic segment like [tenant], Dawn records that segment in generated route metadata. Today, route execution is input-driven: pass the value in the JSON input you send to dawn run, runs/wait, or runs/stream.

text
src/app/(public)/hello/[tenant]/  →  route id /hello/[tenant]

For example:

bash
echo '{"tenant":"acme"}' | dawn run '/hello/[tenant]'

The schema covers state your entry reads or the caller supplies. Do not rely on a concrete pathname with an inline tenant value to populate tenant; the current resolver matches the parameterized route id (/hello/[tenant]) or the route entry file path.

Custom reducers

Each route may include a reducers/ directory with one file per state field. The default export is a (current, incoming) => merged function that overrides Dawn's default merge for that field.

src/app/(public)/hello/[tenant]/reducers/context.ts
export default (current: string, incoming: string) =>
  current ? `${current}\n${incoming}` : incoming

The file basename must equal the state-field name (context.ts → reduces state.context). Reducers are useful for accumulation patterns (concatenating logs, summing counters, deduplicating lists) where the default object spread is too coarse.

Rules

  1. 1

    State must be JSON-serializable

    Dawn serializes state across runtime boundaries (dawn run, dawn dev, the Node runtime, Hono builds, and scenario tests), and LangSmith serializes state at its own boundary. Use primitives, plain objects, arrays. No classes, no Dates, no Maps.

  2. 2

    Prefer readonly

    Mark fields readonly (or use Zod's .readonly()) when they should not be mutated. The entry function returns a new state object rather than mutating the input. This keeps scenario tests deterministic and LangGraph checkpoint-safe.

  3. 3

    Outputs accumulate

    A workflow can return { ...state, newField } when you want to preserve incoming state. Dawn returns the route output; it does not automatically merge workflow output with the input for you.

State flow

State crosses the runtime boundary at every entry point:

  • dawn run — JSON via stdin, JSON via stdout.
  • runs/wait and runs/stream under dawn dev, the Node runtime (dawn start or .dawn/build/server.mjs), and Hono builds — Dawn's HTTP envelope uses { route, input }, where route is <routeId>#<kind>.
  • LangSmith deployments are a separate product boundary. They use LangSmith's own request envelope, keyed by assistant_id; do not send Dawn's { route, input } body unchanged.

Tool results, intermediate values, and other in-route data live inside the entry for the duration of a single run; only the route result crosses the boundary.