Actors
An actor is a thing with identity that enforces a rule: an order that can't ship twice, a seat that can't be double-booked, a submission that moves through review. There's one instance per id, it owns its own state, and it's the only thing allowed to change that state — a handler asks the actor to do something; it never writes the actor's data directly.
This is the counterpart to a workflow. A workflow is a process that runs to completion; an actor is a thing that persists and guards an invariant. Reach for an actor when "this can't happen twice" or "only X is allowed when in state Y" has to hold no matter who asks.
Shape
The actor borrows its data shape, key, and lifecycle states from a defineSchema and fills in transitions per state:
import { defineActor, defineSchema } from "@nwire/forge";
import { z } from "zod";
export const SubmissionData = defineSchema({
name: "submission",
key: "submissionId",
fields: {
submissionId: z.string(),
studentId: z.string(),
verdict: z.string().optional(),
},
states: {
submitted: { initial: true },
"under-review": {},
graded: { final: true },
},
});
export const Submission = defineActor(
"submission",
({ data, validate, recordThat, states, when, after }) => {
const { submitted, underReview, graded } = states; // callable state bodies
// Reactions, scoped per state — return a state to transition, nothing to stay.
submitted(() => {
// assign folds the actor's data — pure, no side effects
when(AnswerWasSubmitted, (e, { assign }) => { assign({ ...e }); }); // stay
when(SubmissionWasAutoGraded, (e, { assign }) => { assign({ ...e }); return graded; });
when(SubmissionWasFlaggedForReview, () => underReview);
});
underReview(() => {
when(SubmissionWasManuallyGraded, () => graded);
// Schedule a deferred action while in this state. Cancelled
// automatically on state transition.
after("review-reminder", "3d", "submissions.send-review-reminder");
});
// `graded` is final in the schema — it declares no reactions.
// Pure invariant-enforcing method, callable via ctx.actor(Submission, id).
const flag = (reason: string) => {
validate({ reason }, [() => data.verdict === undefined || "already graded"]);
recordThat(SubmissionFlaggedForReview({ submissionId: data.submissionId, reason }));
};
return { flag };
},
{
schema: SubmissionData,
// Studio-aware: when in this state for > threshold, surface as "stuck"
stuckThresholds: {
"under-review": 48 * 60 * 60 * 1000,
},
// Hard SLA — Studio raises an alert and routes to escalateTo
slas: {
"under-review": {
maxDurationMs: 7 * 24 * 60 * 60 * 1000,
escalateTo: "curriculum-lead",
},
},
},
);Usage from a handler
const flagSubmission = defineAction({
name: "submissions.flag",
schema: FlagSubmissionInput,
handler: async (ctx) => {
const { input } = ctx;
const submission = await ctx.actor(Submission, input.submissionId);
// submission.state, submission.stateName, submission.key — plus methods
return submission.flag(input.reason); // throws if invariant violated
},
});What goes in assign vs methods
assign(patch)inside awhen— pure data fold. Called during the runtime's state-transition phase. Cannot read or call anything; read the livedatato compute the next value.- methods (defined in the closure, returned as a plain object) — pure invariant-enforcers called from handlers via
ctx.actor(Actor, id). Theyvalidateinvariants andrecordThatan event (or throw). Used for "only X can do Y when in state Z" rules.
Studio-aware metadata
| Field | What Studio does with it |
|---|---|
stuckThresholds: Record<state, ms> | Surfaces actor instances exceeding the threshold in the stuck-state inbox |
slas: Record<state, { maxDurationMs, escalateTo? }> | Hard SLA alerts with escalation routing |
The write-path rule
An actor is one of two ways to write state, and they don't mix: state that guards an invariant goes through an actor (locked, version-checked); plain data goes straight to the database. See write paths for why a single operation picks one, never both.
See also
defineActorreference — full API.- Workflow — the sibling for processes that run to completion.
- Write paths — actor xor direct-to-database.