defineQuery
defineQuery(projection, options) declares a read function over a projection. CQRS read path — queries never read actors.
Signature
function defineQuery<TState, TSchema extends ZodTypeAny, TResult>(
projection: ProjectionDefinition<TState>,
options: QueryOptions<TState, TSchema, TResult>,
): QueryDefinition<TState, TSchema, TResult>QueryOptions
interface QueryOptions<TState, TSchema, TResult> {
name: string
description?: string
schema: TSchema
execute: (state: TState, input: z.output<TSchema>) => TResult | Promise<TResult>
// Studio-aware
slo?: { p95LatencyMs?: number }
cacheable?: boolean // hint — opt-in cache adapters honor it
}Example
export const submissionsByStudent = defineQuery(SubmissionsByStudent, {
name: "submissions.by-student",
description: "Avi's submission history, newest first.",
schema: z.object({
studentId: z.string(),
status: z.enum(["submitted", "under-review", "graded"]).optional(),
limit: z.number().default(20),
}),
slo: { p95LatencyMs: 50 },
cacheable: true,
execute: (state, { studentId, status, limit }) => {
let items = state.byStudent[studentId] ?? []
if (status) items = items.filter((s) => s.status === status)
return items.slice(0, limit)
},
})Invocation
As an HTTP route
defineModule("submissions", {
queries: [submissionsByStudent],
routes: defineRoute({
"GET /submissions": submissionsByStudent,
}),
})The HTTP wire automatically parses query params against the schema (?studentId=avi&status=graded&limit=10).
From a handler
defineHandler(reviewSubmission, async (input, ctx) => {
const history = await ctx.query(submissionsByStudent, { studentId: input.studentId })
// …
})ctx.query passes the envelope's tenant automatically. No manual threading.
Directly via runtime
const result = await app.runtime.query("submissions.by-student", { studentId: "avi" })For tests, scripts, the CLI. Specify tenant as the third arg (runtime.query(name, input, tenant)).
Why queries instead of "just call the projection store"
Three reasons:
- Validation — query inputs go through zod, same as action inputs. Wrong shape returns a 400 (HTTP) or throws (in-process).
- HTTP wire ergonomics —
defineRoute({ "GET /x": myQuery })mounts it with the right query-param parsing automatically. - Studio — queries appear in the canvas as teal stickies; observed latency from the telemetry stream scores against
slo.p95LatencyMs.
Caching
cacheable: true is a hint, not enforcement. Future cache middleware (@nwire/cache) can read this and cache the result keyed by (name, input, tenant). With no cache middleware mounted, the flag is a no-op.
If your query reads a slow projection store and the data doesn't change often, set cacheable: true and add the cache middleware later — no code changes when you do.
Studio rendering
Queries appear as smaller teal stickies on the EventStorm canvas, anchored to their projection. The edge from projection → query is automatic. SLO scorecard shows declared p95LatencyMs vs observed.