Resources
•8 min read
Glossary
A reference guide to the key concepts and terminology in Directive.
Core Concepts
Facts
Facts are the observable state in your module. They're reactive values that trigger updates when changed.
schema: {
facts: {
userId: t.number(), // Primitive fact
user: t.object<User>().nullable(), // Complex object, starts as null
loading: t.boolean(), // Tracks async state
},
},
Facts are accessed via system.facts or context.facts in resolvers:
// Write to a fact – triggers derivation and constraint evaluation
system.facts.userId = 123;
// Read a fact – returns the current value
console.log(system.facts.userId);
See also: Facts documentation
Derivations
Derivations are computed values that automatically track their dependencies. They recompute only when their dependencies change.
derive: {
// Falls back to "Guest" when no user is loaded
displayName: (facts) => facts.user?.name ?? "Guest",
// Boolean shorthand derived from a single fact
isLoggedIn: (facts) => facts.user !== null,
},
Derivations can depend on other derivations:
derive: {
isAdmin: (facts) => facts.user?.role === 'admin',
// Compose derivations – reads isLoggedIn and isAdmin via the derive param
canEdit: (facts, derive) => derive.isLoggedIn && derive.isAdmin,
},
See also: Derivations documentation
Constraints
Constraints declare what must be true in your system. When a constraint's when condition is true, it generates a requirement.
constraints: {
needsUser: {
// Fire when we have a userId but haven't loaded the user yet
when: (facts) => facts.userId > 0 && !facts.user,
require: { type: "FETCH_USER" },
},
},
Key properties:
- when: Function that returns true when the constraint is active
- require: The requirement object to generate
- priority: Number for ordering (higher = first)
- after: Dependencies on other constraints
See also: Constraints documentation
Requirements
Requirements are typed objects that describe what the system needs. They're generated by constraints and fulfilled by resolvers.
// Minimal requirement – type is the only required field
{ type: "FETCH_USER" }
// Add properties to pass context to the resolver
{ type: "FETCH_USER", id: 123 }
// Requirements can carry any additional data the resolver needs
{
type: "SEND_NOTIFICATION",
title: "Welcome",
body: "Thanks for signing up",
priority: "high",
}
Requirements are matched to resolvers by their type field. Additional properties are available directly on the requirement object in the resolver's resolve function.
Resolvers
Resolvers fulfill requirements. They run when a matching requirement is active and handle the async logic to satisfy it.
resolvers: {
fetchUser: {
requirement: "FETCH_USER",
retry: { attempts: 3, backoff: "exponential" }, // Retry with increasing delays
timeout: 5000, // Abort after 5 seconds
resolve: async (req, context) => {
context.facts.loading = true;
context.facts.user = await api.getUser(context.facts.userId);
context.facts.loading = false; // Clears the loading flag when done
},
},
},
Key properties:
- requirement: The type string to match (or a type guard predicate function)
- key: Deduplication key function
- retry: Retry configuration (
{ attempts, backoff, initialDelay }) - timeout: Maximum execution time in ms
- resolve: The async function to run
- batch: Configuration for batching multiple requirements together
See also: Resolvers documentation
Effects
Effects are fire-and-forget side effects that run when facts change. Unlike resolvers, they don't fulfill requirements.
effects: {
logChanges: {
// Compare current and previous values to detect meaningful changes
run: (facts, prev) => {
if (prev?.userId !== facts.userId) {
analytics.track('user_changed', { userId: facts.userId });
}
},
},
},
Use effects for: logging, analytics, DOM updates, notifications.
See also: Effects documentation
System Architecture
Module
A Module is a self-contained unit that defines schema, constraints, resolvers, derivations, and effects.
// A module is a self-contained unit of state, logic, and side effects
const userModule = createModule("user", {
schema: { /* ... */ }, // Shape of facts, derivations, events, requirements
init: (facts) => { /* ... */ }, // Set initial values
derive: { /* ... */ }, // Computed values
constraints: { /* ... */ }, // Declarative rules
resolvers: { /* ... */ }, // Async fulfillment logic
effects: { /* ... */ }, // Fire-and-forget side effects
});
Modules are composable - you can combine multiple modules into a system.
See also: Module & System documentation
System
A System is the runtime that executes a module. It manages the reconciliation loop, plugin lifecycle, and provides the API to interact with state.
// The system wires a module to plugins and debug options
const system = createSystem({
module: userModule,
plugins: [loggingPlugin()],
debug: { timeTravel: true },
});
Key APIs:
system.start()- Start the systemsystem.destroy()- Stop and clean up the systemsystem.facts- Access factssystem.derive- Access derivationssystem.events- Dispatch events (e.g.,system.events.increment())system.settle()- Wait for all resolverssystem.getSnapshot()- Get current statesystem.inspect()- Get unmet requirements, inflight resolvers
Reconciliation Loop
The reconciliation loop is the core algorithm that:
- Evaluates all constraints
- Collects active requirements
- Matches requirements to resolvers
- Executes resolvers
- Repeats until settled (no new requirements)
Facts change --> Evaluate constraints --> Generate requirements
^ |
+---- Resolvers update facts <--- Execute resolvers
Settle
Settling means waiting for all pending resolvers to complete and the system to reach a stable state with no active requirements.
// Setting a fact kicks off the reconciliation loop
system.facts.userId = 123;
// settle() resolves once every constraint is satisfied
await system.settle();
// All resolvers have finished – derived data is ready
console.log(system.facts.user); // { id: 123, name: "John" }
Plugins & Extensions
Plugin
A Plugin extends the system with additional functionality. Plugins can hook into the system lifecycle.
const myPlugin = {
name: 'my-plugin',
// Lifecycle hooks – run at system creation and teardown
onInit: (system) => { /* Called when system is created */ },
onStart: (system) => { /* Called on system.start() */ },
onStop: (system) => { /* Called on system.stop() */ },
onDestroy: (system) => { /* Called on system.destroy() */ },
// Observation hooks – react to runtime events
onFactSet: (key, value, prev) => { /* Called when a fact changes */ },
onResolverStart: (resolver, req) => { /* Called when resolver starts */ },
onResolverComplete: (resolver, req, duration) => { /* Called on completion */ },
onResolverError: (resolver, req, error) => { /* Called on resolver error */ },
// Catch-all for unhandled errors
onError: (error) => { /* Called on system errors */ },
};
Built-in plugins: loggingPlugin, devtoolsPlugin, persistencePlugin.
See also: Plugin documentation
Time-Travel
Time-travel debugging allows you to navigate through the history of state changes.
// Enable time-travel to capture state snapshots automatically
const system = createSystem({
module: myModule,
debug: {
timeTravel: true,
maxSnapshots: 100, // Keep the last 100 snapshots in memory
},
});
system.start();
// Navigate through state history with the debug API
system.debug?.goBack();
system.debug?.goForward();
system.debug?.goTo(5); // Jump to a specific snapshot index
See also: Time-Travel documentation
Builders & Helpers
Directive provides builder utilities for creating type-safe constraints and resolvers outside of createModule(). These are useful for reusable definitions, shared libraries, and orchestrator configuration.
Core Builders (@directive-run/core)
Directive provides two categories of core builders: fluent builders for ergonomic chaining, and factory helpers for typed one-offs.
Fluent Builders
import { constraint, when, system, module } from '@directive-run/core';
| Function | Description |
|---|---|
constraint<Schema>() | Fluent builder – chain .when(), .require(), optional fields, .build() |
when<Schema>(condition) | Quick shorthand – .require() returns a valid constraint directly |
system() | Fluent builder – chain .module() or .modules(), options, .build() |
module(id) | Fluent builder – chain .schema(), .derive(), .events(), .build() |
// Full constraint builder
const escalate = constraint<typeof schema>()
.when(f => f.confidence < 0.7)
.require({ type: 'ESCALATE' })
.priority(50)
.build();
// Quick shorthand (no .build() needed)
const pause = when<typeof schema>(f => f.errors > 3)
.require({ type: 'PAUSE' })
.withPriority(50);
// System builder
const sys = system()
.module(counterModule)
.plugins([loggingPlugin()])
.build();
Factory Helpers
import {
constraintFactory,
resolverFactory,
typedConstraint,
typedResolver,
} from '@directive-run/core';
| Function | Description |
|---|---|
constraintFactory<Schema>() | Returns a factory with .create() for building multiple typed constraints against a schema |
resolverFactory<Schema>() | Returns a factory with .create() for building multiple typed resolvers against a schema |
typedConstraint<Schema, Req>(def) | One-off typed constraint (no factory needed) |
typedResolver<Schema, Req>(def) | One-off typed resolver (no factory needed) |
See also: Builders documentation for full API reference and examples
AI Builders (@directive-run/ai)
For orchestrator-level constraints. The AI adapter has its own constraint and when typed against OrchestratorConstraint (simplified). For general use, prefer the core builders above.
import { constraint, when, createOrchestratorBuilder } from '@directive-run/ai';
| Function | Description |
|---|---|
constraint<Facts>() | AI-scoped fluent builder (produces OrchestratorConstraint) |
when<Facts>(condition) | AI-scoped shorthand (produces OrchestratorConstraint) |
createOrchestratorBuilder<Facts>() | Fluent builder for the entire orchestrator config |
// Fluent builder – full control over every option
const escalate = constraint<MyFacts>()
.when((f) => f.confidence < 0.7)
.require({ type: 'ESCALATE' })
.priority(50)
.build();
// Quick shorthand – returns a valid constraint directly
const pause = when<MyFacts>((f) => f.errors > 3)
.require({ type: 'PAUSE' });
// Shorthand with priority
const halt = when<MyFacts>((f) => f.errors > 10)
.require({ type: 'HALT' })
.withPriority(100);
The orchestrator builder composes constraints, resolvers, guardrails, and plugins into a single chain:
const orchestrator = createOrchestratorBuilder<MyFacts>()
.withConstraint('escalate', escalate)
.withResolver('escalate', { requirement: 'ESCALATE', resolve: async () => { /* ... */ } })
.withInputGuardrail('pii', createPIIGuardrail({ redact: true }))
.withBudget(10000)
.build({ runner, autoApproveToolCalls: true });
See also: Orchestrator documentation for full builder examples
Related Terms
Proxy
Directive uses JavaScript Proxies to track fact access and mutations. This enables automatic dependency tracking for derivations and effects.
Deduplication
Deduplication prevents the same requirement from being resolved multiple times simultaneously. Controlled via the resolver's key function.
Backoff
Backoff strategies control retry timing. Options:
"none"- No delay between retries"linear"- Linear delay increase (initialDelay * attempt)"exponential"- Exponential delay increase (initialDelay * 2^attempt)
Snapshot
A Snapshot is a serializable representation of the system state, used for SSR hydration and persistence.
// Capture the full state as a plain, serializable object
const snapshot = system.getSnapshot();
// => { facts: { userId: 123, user: {...} }, version: 1 }

