dawn

State

Every route has a state type declared in a colocated state.ts file. State is the contract between the caller, the route's entry, and any tools the entry invokes.

The shape

// src/app/(public)/hello/[tenant]/state.ts
export interface HelloState {
  readonly tenant: string
  readonly greeting?: string
}

HelloState is imported into index.ts and used as the first parameter of the workflow function (or the equivalent graph state type).

import type { HelloState } from "./state.js"

export async function workflow(state: HelloState, ctx) {
  // state.tenant is string
  // state.greeting is string | undefined
}

Dynamic segments become state fields

If your route's directory contains a dynamic segment like [tenant], that segment's value is populated on the state field of the same name at runtime.

src/app/(public)/hello/[tenant]/  →  state.tenant populated from the pathname

Rules

  1. 1

    State must be JSON-serializable

    Dawn serializes state across runtime boundaries (dev server, scenario tests, LangGraph Platform). Use primitives, plain objects, arrays. No classes, no Dates, no Maps.

  2. 2

    Prefer readonly

    Mark fields readonly. 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 returns { ...state, newField } — merging new outputs into the existing state. Fields added during the run are typed as optional (greeting?: string) so the caller isn't forced to provide them.

State flow

caller stdin  →  state  →  workflow/graph/chain  →  new state  →  stdout
                              ↓
                           ctx.tools

The state is the only thing that crosses the runtime boundary. Everything else — tool results, intermediate values — lives inside the entry for the duration of a single run.