ActionBuilder
@rotorsoft/act-root / act/src / ActionBuilder
Type Alias: ActionBuilder<TState, TEvents, TActions, TName, TSnap>
ActionBuilder<
TState,TEvents,TActions,TName,TSnap> =object
Defined in: libs/act/src/builders/state-builder.ts:152
Builder interface for defining actions (commands) on a state.
Actions represent user/system intents to modify state. Each action is validated against a schema, can have business rule invariants, and must emit one or more events.
Seeโ
state for complete usage examples
Type Parametersโ
TStateโ
TState extends Schema
State schema type
TEventsโ
TEvents extends Schemas
Event schemas type
TActionsโ
TActions extends Schemas
Action schemas type
TNameโ
TName extends string = string
State name literal type
TSnapโ
TSnap extends boolean = false
true once .snap(...) has been called. Gates the
.autocloses({ keep }) rolling-window option โ a windowed close is
meaningless without snapshots, so keep only typechecks after
.snap in the chain.
Propertiesโ
archivesโ
archives: (
archive) =>ActionBuilder<TState,TEvents,TActions,TName,TSnap>
Defined in: libs/act/src/builders/state-builder.ts:561
Declares the archiver the online close cycle runs before
truncating a stream this state's .autocloses(...) predicate
accepted. Hosts use it to write events to durable storage (S3,
an analytics warehouse, cold tier) before the tombstone lands,
so the truncate doesn't lose history that the operator still
needs.
Threads into CloseTarget.archive via the same plumbing
app.close({ stream, archive }) already uses โ the cycle holds
the stream's guard while the archiver runs, and a thrown
archiver leaves the stream guarded but un-truncated. No partial
truncate state, no data loss; the cycle retries the candidate
on the next tick.
One archiver per state. A second .archives(...) call replaces
the first (same shape as .snap / .discloses /
.autocloses). Absent โ the cycle truncates without an archive
step.
Parametersโ
archiveโ
AutocloseArchiver<TEvents>
(stream, head) => Promise<void>. Runs while
the stream is locked against new writes; the truncate runs
immediately after a successful resolve.
Returnsโ
ActionBuilder<TState, TEvents, TActions, TName, TSnap>
The ActionBuilder for chaining.
Exampleโ
Archive to S3 before truncate.
state({ Ticket: ticketSchema })
.emits({ TicketOpened, TicketResolved })
// ...
.autocloses({ is: "TicketResolved" })
.archives(async (stream) => {
const events = await loadEvents(stream);
await s3.upload(`tickets/${stream}.jsonl`, events);
})
autoclosesโ
autocloses: (
policy) =>ActionBuilder<TState,TEvents,TActions,TName,TSnap>
Defined in: libs/act/src/builders/state-builder.ts:513
Declares the online close predicate for this state. The
orchestrator's autoclose cycle iterates the state's streams once
per tick and calls the predicate per candidate; truthy results are
scheduled for atomic truncate-and-seed via Store.truncate on the
next batch.
One predicate per state. A second .autocloses(...) call replaces
the first (same shape as .snap / .discloses โ state-level, not
per-event). Absent โ the state opts out of online close entirely;
the cycle skips it and pays zero per-tick cost for it.
Pass a declarative AutoclosePolicy object literal covering the
three operational pressure points (after, is, reaches). Top-level
fields combine with AND; an optional or: {...} block opens an
alternative OR path. Validated via Zod at build time; misconfiguration
throws before act().build() completes.
Under the hood this compiles to a synthesized reaction (#1090) that runs
on a per-aggregate synthetic stream: it defers to head.created + the policy's min after while the cooldown holds and closes the stream once
the policy matches. There is no background sweep.
The function-predicate form was removed (#1090). .autocloses no
longer accepts (stream, head, count) => boolean; an opaque predicate has
no derivable due-time or terminal event to react to. For conditions the
declarative form can't express, call app.close(...) from your own logic.
Parametersโ
policyโ
[TSnap] extends [true] ? AutoclosePolicy : Omit<AutoclosePolicy, "keep"> & object
The declarative AutoclosePolicy bag.
Returnsโ
ActionBuilder<TState, TEvents, TActions, TName, TSnap>
The ActionBuilder for chaining.
Examplesโ
Declarative โ cooldown after terminal (a Ticket closes
90 days after resolution).
.autocloses({ is: "TicketResolved", after: { days: 90 } })
Declarative โ multi-terminal (an Order closes on any of
three terminal events, no cooldown).
.autocloses({ is: ["Shipped", "Delivered", "Cancelled"] })
Declarative โ time-only retention (a Session closes
after 24h regardless of head event).
.autocloses({ after: { days: 1 } })
Declarative โ pure cardinality cap.
.autocloses({ reaches: 10_000 })
Declarative โ primary cooldown + safety-net backstop.
.autocloses({
is: "TicketResolved", // primary trigger
after: { days: 90 }, // AND aged 90 days
or: { reaches: 10_000 }, // OR cardinality safety net
})
Declarative โ pure OR (only backstops, no primary
cooldown).
.autocloses({ or: { is: "TicketResolved", reaches: 10_000 } })
Rolling window โ keep the last 180 days of real events on a
live stream (requires .snap(...) earlier in the chain; keep
won't typecheck without it). Each eligible cycle prunes the prefix
below the closest safe snapshot older than now โ keep.
.snap((s) => s.patches >= 100)
.autocloses({ keep: { days: 180 } })
Terminate AND prune โ close 90 days after resolution,
meanwhile keep open streams pruned to a 180-day window.
.snap((s) => s.patches >= 100)
.autocloses({ is: "TicketResolved", after: { days: 90 }, keep: { days: 180 } })
buildโ
build: () =>
State<TState,TEvents,TActions,TName>
Defined in: libs/act/src/builders/state-builder.ts:583
Finalizes and builds the state definition.
Call this method after defining all actions, invariants, and patches to create the complete State object that can be registered with Act.
Returnsโ
State<TState, TEvents, TActions, TName>
The complete strongly-typed State definition
Exampleโ
const Counter = state({ Counter: schema })
.init(() => ({ count: 0 }))
.emits({ Incremented: z.object({ amount: z.number() }) })
.patch({ Incremented: ({ data }, state) => ({ count: state.count + data.amount }) })
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.build(); // Returns State<TState, TEvents, TActions, TName>
disclosesโ
discloses: (
disclose) =>ActionBuilder<TState,TEvents,TActions,TName,TSnap>
Defined in: libs/act/src/builders/state-builder.ts:422
Declares the disclosure predicate for sensitive(...)-marked event
fields. Gates external reads: returning true allows the actor to see
plaintext on the event; returning false substitutes "[REDACTED]".
When absent, the framework default-denies on every external read โ
fail-safe.
One predicate per state. A second .discloses(...) call replaces the
first (same shape as snapshots being state-level, not per-event).
The predicate receives the full event including merged PII so it can
branch on the payload itself (e.g.
event.data.ownerId === actor.id). Reducers, projections, and
reactions are unaffected โ they follow separate visibility rules
documented in #855.
Parametersโ
discloseโ
(event, actor) => boolean
Predicate (event, actor) => boolean. true =
plaintext, false = "[REDACTED]" substitution.
Returnsโ
ActionBuilder<TState, TEvents, TActions, TName, TSnap>
The ActionBuilder for chaining.
Exampleโ
Owner-or-admin disclosure
state({ User: userSchema })
.init(() => ({ ... }))
.emits({ UserRegistered: z.object({ email: sensitive(z.string()) }) })
.discloses((event, actor) =>
actor.id === event.stream || actor.roles?.includes("admin"))
onโ
on: <
TKey,TNewActions>(entry,options?) =>object
Defined in: libs/act/src/builders/state-builder.ts:219
Defines an action (command) that can be executed on this state.
Actions represent intents to change state - they should be named in imperative form (e.g., "CreateUser", "IncrementCounter", "PlaceOrder"). Actions are validated against their schema and must emit at least one event.
Pass a { ActionName: schema } record โ use shorthand { ActionName }
when the variable name matches the action name. The key becomes the
action name, the value the Zod schema.
Pass an optional second argument to declare a per-action retry
policy โ the orchestrator retries this action on
ConcurrencyError up to maxRetries extra times, applying
backoff between attempts when set. Omit the argument to keep the
current single-attempt behavior (ConcurrencyError surfaces on
first conflict).
Type Parametersโ
TKeyโ
TKey extends string
Action name (string literal type)
TNewActionsโ
TNewActions extends Schema
Action payload schema type
Parametersโ
entryโ
ActionEntry<TKey, TNewActions>
Single-key record { ActionName: schema }
options?โ
Optional per-action retry policy (ActionOptions).
Returnsโ
An object with .given() and .emit() for further configuration
emitโ
emit: {(
handler):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>; (event_name):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>; }
Defines the action handler that emits events. Same two overloads as
the post-.given() form above:
- Function โ receives
(action, snapshot)and returns one or more[EventName, data]tuples (orundefined). - String โ passthrough: the action payload becomes the event
data directly. Must reference an event declared in
.emits().
The two overloads are kept separate (rather than merged into a
handler | string union) so that TS contextual typing of the
function alternative isn't degraded by considering the string
branch โ under the union form TState could collapse to its
Schema constraint inside the callback.
Call Signatureโ
(
handler):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>
Parametersโ
handlerโ
ActionHandler<TState, TEvents, { [P in string]: TNewActions }, TKey>
Returnsโ
ActionBuilder<TState, TEvents, TActions & { [P in string]: TNewActions }, TName, TSnap>
Call Signatureโ
(
event_name):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>
Parametersโ
event_nameโ
keyof TEvents & string
Returnsโ
ActionBuilder<TState, TEvents, TActions & { [P in string]: TNewActions }, TName, TSnap>
Examplesโ
Passthrough (action payload = event data)
.emit("Incremented")
Single event
.emit((action) => ["Incremented", { amount: action.by }])
Multiple events
.emit((action) => [
["Incremented", { amount: action.by }],
["LogUpdated", { message: `Incremented by ${action.by}` }]
])
givenโ
given: (
rules) =>object
Adds business rule invariants that must hold before the action can execute.
Invariants are checked after loading the current state but before emitting
events. Each invariant pairs a description with a valid(state, actor?)
predicate โ when a predicate returns false, the action throws
InvariantError carrying the description. All invariants must pass for
the action to succeed.
Parametersโ
rulesโ
Invariant<TState>[]
Array of Invariant objects ({ description, valid })
Returnsโ
An object with .emit() to finalize the action
emitโ
emit: {(
handler):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>; (event_name):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>; }
Defines the action handler that emits events.
The handler receives the action payload and current state snapshot, and must return one or more events to emit. Events are applied to state via the patch handlers defined earlier.
Pass a string event name for passthrough: the action payload becomes the event data directly.
Call Signatureโ
(
handler):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>
Custom handler โ receives (action, snapshot) and returns one
or more [EventName, data] tuples (or undefined).
Parametersโ
handlerโ
ActionHandler<TState, TEvents, { [P in string]: TNewActions }, TKey>
Returnsโ
ActionBuilder<TState, TEvents, TActions & { [P in string]: TNewActions }, TName, TSnap>
Call Signatureโ
(
event_name):ActionBuilder<TState,TEvents,TActions&{ [P in string]: TNewActions },TName,TSnap>
Passthrough โ the action payload becomes the event data
directly. Must reference an event declared in .emits().
Parametersโ
event_nameโ
keyof TEvents & string
Returnsโ
ActionBuilder<TState, TEvents, TActions & { [P in string]: TNewActions }, TName, TSnap>
Paramโ
handler
Function that returns events to emit, or event name string for passthrough
Returnsโ
The ActionBuilder for chaining more actions
Examplesโ
Custom handler
.emit((action, snapshot) => {
const newBalance = snapshot.state.balance + action.amount;
return ["Deposited", { amount: action.amount, newBalance }];
})
Passthrough (action payload = event data)
.emit("TicketAssigned")
Exampleโ
.given([
{ description: "Must be active", valid: (state) => state.status === "active" },
{ description: "Must be the owner", valid: (state, actor) => state.ownerId === actor?.id }
])
Examplesโ
Simple action without invariants
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
Hot-stream action with retry + jittered exponential backoff
.on(
{ transfer: z.object({ amount: z.number() }) },
{
maxRetries: 5,
backoff: { strategy: "exponential", baseMs: 10, maxMs: 200, jitter: true },
}
)
.emit((action) => ["Transferred", { amount: action.amount }])
Action with business rules
.on({ withdraw: z.object({ amount: z.number() }) })
.given([
{ description: "Account must be open", valid: (state) => state.status === "open" },
{ description: "Funds must be available", valid: (state) => state.balance > 0 }
])
.emit((action) => ["Withdrawn", { amount: action.amount }])
Action with shorthand (variable name matches action name)
const OpenTicket = z.object({ title: z.string() });
.on({ OpenTicket })
.emit((action) => ["TicketOpened", { title: action.title }])
snapโ
snap: (
snap) =>ActionBuilder<TState,TEvents,TActions,TName,true>
Defined in: libs/act/src/builders/state-builder.ts:390
Defines a snapshotting strategy to optimize state reconstruction.
Snapshots store the current state at a point in time, allowing faster state loading
by avoiding replaying all events from the beginning. The snap function is called
after each event is applied and should return true when a snapshot should be taken.
Parametersโ
snapโ
(snapshot) => boolean
Predicate function that returns true when a snapshot should be taken
Returnsโ
ActionBuilder<TState, TEvents, TActions, TName, true>
The ActionBuilder for chaining
Examplesโ
Snapshot every 10 events
.snap((snapshot) => snapshot.patches >= 10)
Snapshot based on state size
.snap((snapshot) => {
const estimatedSize = JSON.stringify(snapshot.state).length;
return estimatedSize > 10000 || snapshot.patches >= 50;
})
Time-based snapshotting
.snap((snapshot) => {
const hoursSinceLastSnapshot = snapshot.patches * 0.1; // Estimate
return hoursSinceLastSnapshot >= 24;
})