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/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 in options and declare reactions with when inside callable state bodies:
import { defineActor } from "@nwire/forge"
export const Submission = defineActor(
"submission",
({ states, when }) => {
const { submitted, underReview, graded } = states;
submitted(() => {
when(AnswerWasSubmitted, (e, { assign }) => { assign({ ...e }); });
when(AnswerWasFlagged, (e, { assign }) => { assign({ confidence: e.confidence }); return underReview; });
});
underReview(() => {
when(SubmissionWasManuallyGraded, (e, { assign }) => { assign({ verdict: e.verdict }); return graded; });
});
// `graded` is `final` in the schema — it declares no reactions.
},
{ schema: SubmissionData },
)The actor inherits key, initial, and the set of valid state names from the schema. The closure declares transitions and timers for non-final states only.
Errors caught at boot:
- A
whenreaction scoped to a state the schema marksfinal: true.
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/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/mongo — Mongo adapter
- Concepts → Actor