Skip to main content

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 system
  • system.destroy() - Stop and clean up the system
  • system.facts - Access facts
  • system.derive - Access derivations
  • system.events - Dispatch events (e.g., system.events.increment())
  • system.settle() - Wait for all resolvers
  • system.getSnapshot() - Get current state
  • system.inspect() - Get unmet requirements, inflight resolvers

Reconciliation Loop

The reconciliation loop is the core algorithm that:

  1. Evaluates all constraints
  2. Collects active requirements
  3. Matches requirements to resolvers
  4. Executes resolvers
  5. 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';
FunctionDescription
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';
FunctionDescription
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';
FunctionDescription
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


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 }
Previous
Debug with Time-Travel
Next
FAQ

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 State Management for TypeScript