defineSchema
defineSchema({ ... }) declares an actor's data shape + lifecycle states + storage hints as a named, standalone artifact. The actor borrows from it.
Why it's separate from defineActor:
- Studio can render the data model without booting the actor.
- Persistent stores (
@nwire/store-mongo,@nwire/store-prisma) readindexes+uniqueto build adapter-level constraints. - Projections can reference the same schema for view typing.
- Validation at boot catches bad state names + key references before any event flows.
Signature
import { defineSchema } from "@nwire/forge"
function defineSchema<TFields extends Record<string, ZodTypeAny>>(
options: SchemaOptions<TFields>,
): SchemaDefinition<TFields>SchemaOptions
interface SchemaOptions<TFields> {
name: string // unique within the app
key: keyof TFields & string // which field is the actor id
fields: TFields // { fieldName: z.string(), ... }
states: Record<string, { initial?: true; final?: true }>
storage?: {
indexes?: ReadonlyArray<ReadonlyArray<string>> // adapter hint
unique?: ReadonlyArray<ReadonlyArray<string>> // adapter hint
}
}Validation at define time:
- Exactly one state must be marked
initial: true. keymust exist infields.
Example
import { z } from "zod"
import { defineSchema } from "@nwire/forge"
export const SubmissionData = defineSchema({
name: "submission",
key: "submissionId",
fields: {
submissionId: z.string(),
studentId: z.string(),
exerciseId: z.string(),
answer: z.string(),
confidence: z.number().optional(),
verdict: z.string().optional(),
submittedAt: z.string().datetime(),
gradedAt: z.string().datetime().optional(),
},
states: {
submitted: { initial: true },
"under-review": {},
graded: { final: true },
},
storage: {
indexes: [["studentId"], ["exerciseId"]],
unique: [["studentId", "exerciseId"]],
},
})What you get back
SubmissionData.$kind // "schema"
SubmissionData.name // "submission"
SubmissionData.key // "submissionId"
SubmissionData.initial // "submitted" (computed from states)
SubmissionData.finals // ["graded"] (computed from states)
SubmissionData.zodSchema // z.object(fields) (compiled at define time)
SubmissionData.storage // { indexes, unique }Binding to an actor
The schema doesn't carry transitions — those live on defineActor. Pass the schema as schema: and declare per-state on: / after: blocks:
import { defineActor } from "@nwire/forge"
export const Submission = defineActor({
schema: SubmissionData,
states: {
submitted: {
on: {
[AnswerWasSubmitted.name]: { assign: (_s, e) => ({ ...e }) },
[AnswerWasFlagged.name]: {
target: "under-review",
assign: (_s, e) => ({ confidence: e.confidence }),
},
},
},
"under-review": {
on: {
[SubmissionWasManuallyGraded.name]: {
target: "graded",
assign: (_s, e) => ({ verdict: e.verdict }),
},
},
},
// `graded` is `final` in the schema — omit it here.
},
methods: { ... },
})The actor inherits name, key, initial, and the set of valid state names from the schema. The actor's states block fills in transitions and timers for non-final states only.
Errors caught at boot:
- Declaring
on:for a state the schema marksfinal: true. - Declaring transitions for a state not listed in the schema (catches typos).
Storage hints
indexes and unique are hints, not promises. Each adapter honors what it can:
| Adapter | Honors indexes | Honors unique |
|---|---|---|
InMemoryActorStore | ignored | ignored |
@nwire/store-file | ignored | ignored |
@nwire/store-mongo | creates real indexes at boot | creates unique indexes |
@nwire/store-prisma (planned) | generated in migrations | generated in migrations |
Studio renders the hints alongside the actor view so reviewers know which fields are query-fast.
Reusing the zod schema
SchemaDef.zodSchema is a compiled z.object(fields). Use it anywhere you need to validate or type a record of the actor's shape — projection serializers, test fixtures, OpenAPI emitters:
const parsed = SubmissionData.zodSchema.parse(payload)
type Submission = z.output<typeof SubmissionData.zodSchema>See also
- defineActor — bind the schema to transitions
- @nwire/store-mongo — Mongo adapter
- Concepts → Actor