Skip to main content

Core API

9 min read

Core API Reference

Core functions and types for Directive.


createModule

Create a module definition.

// Define a module's schema, constraints, resolvers, and effects
function createModule<M extends ModuleSchema>(
  name: string,       // unique identifier for this module
  config: ModuleConfig<M>
): ModuleDef<M>

createModule Parameters

ParameterTypeDescription
namestringUnique module identifier
configModuleConfigModule configuration

Config Options

PropertyTypeDescription
schemaSchemaType definitions for facts, events, requirements
init(facts) => voidInitialize facts with default values
deriveRecord<string, Derivation>Computed values
effectsRecord<string, Effect>Side effect handlers
constraintsRecord<string, Constraint>Declarative rules
resolversRecord<string, Resolver>Requirement handlers

createModuleFactory

Create a factory that produces named module instances from a single definition.

function createModuleFactory<M extends ModuleSchema>(
  config: ModuleConfig<M>
): (name: string) => ModuleDef<M>

createModuleFactory Parameters

ParameterTypeDescription
configModuleConfigModule configuration (same as createModule, without name)

Returns

A function (name: string) => ModuleDef<M> that creates named instances.

Example

const chatRoom = createModuleFactory({
  schema: { facts: { messages: t.array<string>() } },
  init: (facts) => { facts.messages = []; },
});

const system = createSystem({
  modules: {
    lobby: chatRoom("lobby"),
    support: chatRoom("support"),
  },
});

createSystem

Create a runtime system.

// Create a live runtime from a module definition
function createSystem<M extends ModuleSchema>(
  config: SystemConfig<M>
): SingleModuleSystem<M>

createSystem Parameters

PropertyTypeDescription
moduleModuleModule definition
pluginsPlugin[]Optional plugins
traceTraceOptionTrace options
errorBoundaryErrorBoundaryConfigError handling configuration
initialFactsRecord<string, unknown>Initial fact values for hydration

System Properties

facts

Proxy object for reading and writing facts.

// Write a fact (triggers constraint evaluation and effects)
system.facts.count = 10;

// Read a fact (fully typed from your schema)
const count = system.facts.count;

derive

Read-only proxy for derivations. Also exposes runtime registration methods.

// Read derivations (recompute automatically when tracked facts change)
const total = system.derive.cartTotal;

// Runtime registration
system.derive.register("tripled", (facts) => facts.count * 3);
system.derive.assign("doubled", (facts) => facts.count * 20);
system.derive.unregister("tripled");
system.derive.call("doubled");            // recompute, ignoring cache
system.derive.isDynamic("tripled");       // true
system.derive.listDynamic();              // ["tripled"]

events

Accessor for the system's event definitions.

constraints

Runtime control for constraints.

MethodDescription
disable(id)Skip constraint during reconciliation
enable(id)Re-enable a disabled constraint
isDisabled(id)Check if disabled
register(id, def)Add a new constraint at runtime
assign(id, def)Override an existing constraint
unregister(id)Remove a dynamically registered constraint
call(id, props?)Evaluate and return requirements
isDynamic(id)Check if dynamically registered
listDynamic()List all dynamic constraint IDs
system.constraints.disable("myConstraint");
system.constraints.enable("myConstraint");
system.constraints.register("newRule", { when: ..., require: ... });
system.constraints.assign("newRule", { when: ..., require: ... });
system.constraints.unregister("newRule");

effects

Runtime control for effects.

MethodDescription
disable(id)Skip effect during reconciliation
enable(id)Re-enable a disabled effect
isEnabled(id)Check if enabled
register(id, def)Add a new effect at runtime
assign(id, def)Override an existing effect
unregister(id)Remove a dynamically registered effect
call(id)Execute effect immediately
isDynamic(id)Check if dynamically registered
listDynamic()List all dynamic effect IDs
system.effects.disable("myEffect");
system.effects.enable("myEffect");
system.effects.register("logger", { run: (facts) => console.log(facts) });

resolvers

Runtime control for resolvers.

MethodDescription
register(id, def)Add a new resolver at runtime
assign(id, def)Override an existing resolver
unregister(id)Remove a dynamically registered resolver
call(id, requirement)Execute resolver with a requirement
isDynamic(id)Check if dynamically registered
listDynamic()List all dynamic resolver IDs
system.resolvers.register("loadData", { requirement: "LOAD", resolve: ... });
await system.resolvers.call("loadData", { type: "LOAD", id: "123" });

history

Access the time-travel API (or null if not enabled).

// Time-travel is only available when history is enabled
if (system.history) {
  system.history.goBack();               // undo last state change
  system.history.goForward();            // redo
  console.log(system.history.snapshots); // all captured snapshots
}

isRunning / isSettled / isInitialized / isReady

Boolean status properties for the system lifecycle.

system.isRunning;      // true after start() is called
system.isSettled;      // true when all constraints and resolvers finish
system.isInitialized;  // true after module init() completes
system.isReady;        // true after the first reconciliation pass

System Methods

start

Start the system (runs module init and first reconciliation).

system.start();

stop

Stop the system (pauses reconciliation).

system.stop();

destroy

Clean up system resources.

system.destroy();

registerModule

Register a module dynamically on a running namespaced system.

system.registerModule(name: string, module: ModuleDef): void

The module is immediately wired into the constraint, resolver, effect, and derivation graphs. Throws if called during reconciliation or on a destroyed system.

getOriginal

Get the original definition that was overridden by assign().

system.getOriginal(
  type: "constraint" | "resolver" | "derivation" | "effect",
  id: string
): unknown | undefined

Returns the original module-defined definition, or undefined if no original exists for this type/id.

// After overriding a constraint with assign()
const original = system.getOriginal("constraint", "transition");

restoreOriginal

Restore the original definition that was overridden by assign().

system.restoreOriginal(
  type: "constraint" | "resolver" | "derivation" | "effect",
  id: string
): boolean

Re-assigns the original definition and removes the override tracking. Returns true if restoration succeeded, false if no original exists.

// Restore a previously overridden constraint
system.restoreOriginal("constraint", "transition"); // true

See Runtime Dynamics for the full override lifecycle.

dispatch

Dispatch an event.

// Send a typed event into the system for constraints and effects to react to
system.dispatch({ type: "EVENT_NAME", payload: data });

batch

Batch multiple fact mutations into a single reconciliation cycle.

// Group multiple writes so constraints evaluate once (not per write)
system.batch(() => {
  system.facts.a = 1;
  system.facts.b = 2;
});

read

Read a derivation value by ID.

// Read a specific derivation by its string ID
const total = system.read("cartTotal");

subscribe

Subscribe to fact or derivation changes. Accepts any combination of fact and derivation keys (auto-detected). Returns an unsubscribe function.

// Subscribe to fact changes
system.subscribe(["count"], () => console.log("count changed"));

// Subscribe to derivation changes
system.subscribe(["doubled"], () => console.log("doubled changed"));

// Subscribe to mix of facts and derivations
system.subscribe(["count", "doubled"], () => console.log("something changed"));

Signature:

subscribe(ids: Array<ObservableKeys<M>>, listener: () => void): () => void

watch

Watch a fact or derivation and receive old/new values. Accepts both fact and derivation keys (auto-detected). Returns an unsubscribe function.

Has three typed overloads: derivation-specific, fact-specific, and a generic fallback.

// Watch a fact
system.watch("count", (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`);
});

// Watch a derivation
system.watch("doubled", (newVal, oldVal) => {
  console.log(`doubled: ${oldVal} -> ${newVal}`);
});

// With custom equality
system.watch("config", callback, {
  equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b),
});

Options:

PropertyTypeDescription
equalityFn(a: T, b: T) => booleanCustom equality function to control when the callback fires. Defaults to ===.

when

Wait for a predicate to become true. Returns a promise that resolves when the condition is met. Useful for awaiting state transitions.

// Wait for a condition to become true
await system.when((facts) => facts.phase === "ready");

// With timeout
await system.when(
  (facts) => facts.count > 10,
  { timeout: 5000 }
);

Signature:

when(predicate: (facts: Facts<M>) => boolean, options?: { timeout?: number }): Promise<void>

Options:

PropertyTypeDescription
timeoutnumberMaximum time in ms to wait before rejecting. If omitted, waits indefinitely.

whenReady

Wait for the system to be fully ready (after first reconciliation).

// Block until init + first reconciliation completes (useful in tests/SSR)
await system.whenReady();

settle

Wait for all constraints and resolvers to complete.

// Wait for all in-flight constraints and resolvers to finish
await system.settle();

inspect

Get current system state for debugging.

// See what's pending, running, and resolved at this moment
const inspection = system.inspect();
// { unmet, inflight, constraints, resolvers }

explain

Get an explanation of why a requirement exists.

// Trace which constraint generated a requirement and why
const explanation = system.explain("requirement-id");

getSnapshot

Capture current state as a serializable snapshot.

// Serialize current state for persistence or transfer
const snapshot = system.getSnapshot();

restore

Restore from a snapshot.

// Rehydrate state from a previously captured snapshot
system.restore(snapshot);

getDistributableSnapshot

Export state for use outside the runtime (edge cache, API response, Redis). Derivations are included by default since they're computed read-only values safe to distribute. Facts are opt-in since they're mutable source-of-truth state that could become stale.

// Options
interface DistributableSnapshotOptions {
  includeDerivations?: string[];  // Which derivations to include (default: all)
  excludeDerivations?: string[];  // Derivations to exclude
  includeFacts?: string[];        // Which facts to include (default: none)
  ttlSeconds?: number;            // Snapshot TTL in seconds
  metadata?: Record<string, unknown>; // Custom metadata attached to snapshot
  includeVersion?: boolean;       // Include a version hash for diffing
}
// Export selected derivations (default behavior – safe to distribute)
const snapshot = system.getDistributableSnapshot({
  includeDerivations: ["effectivePlan", "canUseFeature"],
  ttlSeconds: 3600,
});

// Include specific facts alongside derivations
const snapshot = system.getDistributableSnapshot({
  includeDerivations: ["effectivePlan"],
  includeFacts: ["userId", "plan"],
  ttlSeconds: 3600,
});

// Export all derivations with version for cache diffing
const snapshot = system.getDistributableSnapshot({
  includeVersion: true,
  metadata: { region: "us-east-1" },
});

Returns { data: T, createdAt: number, expiresAt?: number, version?: string, metadata?: Record<string, unknown> }.

watchDistributableSnapshot

Watch for changes to distributable snapshot derivations.

// Automatically push updated snapshots whenever derivations change
const unsubscribe = system.watchDistributableSnapshot(
  { includeDerivations: ["effectivePlan"] },
  (snapshot) => {
    // Push to Redis/edge cache
  }
);

Builders & Helpers

Type-safe utilities for defining constraints and resolvers outside of createModule(). Useful for shared libraries, reusable definitions, and composable module configurations.

constraintFactory

Create a factory that produces typed constraints for a specific schema.

function constraintFactory<S extends Schema>(): {
  create<R extends Requirement>(constraint: TypedConstraint<S, R>): TypedConstraint<S, R>
}
import { constraintFactory, t } from '@directive-run/core';

const schema = { facts: { count: t.number(), threshold: t.number() } };
const factory = constraintFactory<typeof schema>();

const maxCount = factory.create({
  when: (facts) => facts.count > facts.threshold,
  require: { type: "RESET" },
});

// Use in any module with the same schema
const module = createModule("counter", {
  schema,
  constraints: { maxCount },
});

resolverFactory

Create a factory that produces typed resolvers for a specific schema.

function resolverFactory<S extends Schema>(): {
  create<R extends Requirement>(resolver: TypedResolver<S, R>): TypedResolver<S, R>
}
import { resolverFactory } from '@directive-run/core';

const factory = resolverFactory<typeof schema>();

const fetchUser = factory.create<{ type: "FETCH_USER"; userId: string }>({
  requirement: "FETCH_USER",
  resolve: async (req, context) => {
    context.facts.user = await api.getUser(req.userId);
  },
});

typedConstraint

One-off typed constraint creator. Simpler than constraintFactory when you only need a single definition.

function typedConstraint<S extends Schema, R extends Requirement>(
  constraint: TypedConstraint<S, R>
): TypedConstraint<S, R>
import { typedConstraint } from '@directive-run/core';

const lowStock = typedConstraint<typeof schema, { type: "REORDER" }>({
  when: (facts) => facts.stock < 10,
  require: { type: "REORDER" },
  priority: 50,
});

typedResolver

One-off typed resolver creator. Simpler than resolverFactory when you only need a single definition.

function typedResolver<S extends Schema, R extends Requirement>(
  resolver: TypedResolver<S, R>
): TypedResolver<S, R>
import { typedResolver } from '@directive-run/core';

const resetResolver = typedResolver<typeof schema, { type: "RESET" }>({
  requirement: "RESET",
  resolve: async (_req, context) => {
    context.facts.count = 0;
  },
});

AI Orchestrator

For AI agent orchestrators, see createAgentOrchestrator() in @directive-run/ai. See Orchestrator.


Next Steps

Previous
Schema & Types

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management