Advanced Patterns
•9 min read
Definition Meta
Add optional metadata to any definition for better debugging, richer devtools, and self-documenting systems.
Meta is supported on all 7 definition types: modules, facts, events, constraints, resolvers, effects, and derivations.
Why Meta?
Directive's inspect(), explain(), and devtools show definition IDs like auth::needsLogin or fetchUser. Meta lets you attach human-readable context – labels, descriptions, categories, and colors – so these surfaces are immediately useful without checking source code.
Meta is purely informational. It is never read during reconciliation – zero runtime overhead.
Adding Meta
Every definition type (constraints, resolvers, effects, derivations) accepts an optional meta field.
Modules
const authModule = createModule("auth", {
schema: { /* ... */ },
meta: {
label: "Authentication",
description: "Handles login, tokens, and session management",
category: "auth",
},
// ... constraints, resolvers, etc.
});
Module meta surfaces in inspect().modules and system.meta.module(id). Useful for architecture diagrams, module health dashboards, and onboarding documentation.
Facts (Schema Fields)
schema: {
facts: {
score: t.number().meta({ label: "Player Score", category: "data" }),
email: t.string().meta({ label: "Email", tags: ["pii"] }).nullable(),
plain: t.number(), // no meta – works as before
},
}
The .meta() method chains with all schema builder methods (.nullable(), .min(), .maxLength(), etc.). Surfaces in inspect().facts and system.meta.fact(key). Use tags: ["pii"] to mark fields for compliance scanning.
Events
events: {
// Function form (no meta) – unchanged
increment: (facts) => { facts.count += 1; },
// Object form with meta
reset: {
handler: (facts) => { facts.count = 0; },
meta: { label: "Reset Counter", category: "ui" },
},
},
Event meta surfaces in inspect().events and system.meta.event(name).
Constraints
constraints: {
needsLogin: {
when: (facts) => !facts.user,
require: { type: "LOGIN" },
meta: {
label: "Requires Authentication",
description: "User is not logged in",
category: "auth",
color: "#f59e0b",
},
},
},
Resolvers
resolvers: {
login: {
requirement: "LOGIN",
resolve: async (req, context) => { /* ... */ },
meta: {
label: "OAuth Login Flow",
description: "Exchanges authorization code for access token",
},
},
},
Effects
effects: {
logPhase: {
run: (facts) => console.log(facts.phase),
meta: { label: "Phase Logger", category: "logging" },
},
},
Derivations
Derivations support two forms. The existing function form works exactly as before. The new object form uses compute instead of a bare function:
derive: {
// Function form (no meta) – unchanged
isReady: (facts) => facts.status === "ready",
// Object form with meta
displayName: {
compute: (facts) => `${facts.firstName} ${facts.lastName}`,
meta: { label: "Display Name", description: "Full name for UI" },
},
},
The DefinitionMeta Type
import type { DefinitionMeta } from "@directive-run/core";
interface DefinitionMeta {
label?: string; // Human-readable name
description?: string; // Longer explanation
category?: string; // Grouping key (e.g., "auth", "data", "ui")
color?: string; // CSS hex color (e.g., "#f59e0b")
tags?: string[]; // Multi-dimensional labels for filtering
[key: string]: unknown; // Extensible for plugins
}
You don't need to import DefinitionMeta to use it – inline objects are inferred correctly. The import is useful when building reusable helpers or plugins.
Suggested Categories
| Category | Use for |
|---|---|
auth | Authentication and authorization |
data | Data fetching and persistence |
ui | UI state and display logic |
logging | Logging and analytics |
lifecycle | System lifecycle management |
Categories are freeform strings – custom categories work, these are conventions for consistency.
Tags
Use tags for multi-dimensional filtering. A constraint can be both "auth" and "critical":
constraints: {
needsLogin: {
when: (facts) => !facts.user,
require: { type: "LOGIN" },
meta: {
label: "Requires Auth",
category: "auth",
tags: ["critical", "security"],
},
},
},
Tags are separate from category – use category for primary grouping, tags for cross-cutting concerns.
Where Meta Surfaces
system.meta (O(1) queries)
Query meta directly by definition ID without building a full inspection snapshot:
// O(1) lookups
system.meta.module("auth")?.label; // "Authentication"
system.meta.constraint("needsLogin")?.label; // "Requires Auth"
system.meta.resolver("login")?.description; // "Exchanges code for token"
system.meta.effect("log")?.category; // "logging"
system.meta.derivation("displayName")?.tags; // ["ui", "critical"]
// Returns undefined for unknown IDs
system.meta.constraint("nonexistent"); // undefined
// Bulk queries across all definition types
system.meta.byTag("pii"); // MetaMatch[] – tagged fields, and derivations that read them
system.meta.byTag("pii", { kind: "fact" }); // just the facts
// One definition, no walk. O(1) for a fact.
system.meta.carriesTag("fact", "email", "pii"); // true
// Told when the answer can have moved, so nothing has to poll.
const stop = system.meta.subscribe(["pii"], rebuild, { immediate: true });
Each MetaMatch contains { kind, id, meta, tagOrigin } where kind is "module" | "fact" | "event" | "constraint" | "resolver" | "effect" | "derivation". tagOrigin is "inherited" on a derivation matched through the tag inheritance below and "authored" when the tag was written on the definition itself – always present, so you never have to read meaning into a missing field.
carriesTag returns boolean | undefined, and the third state is the point. undefined means the runtime could not answer – a derivation body threw, or the definition is unknown. Reading that as "no tag, nothing to redact" is the failure this surface exists to prevent, so default it to the safe side: screen, redact, escalate.
Use byTag("pii") for a compliance inventory, carriesTag for the per-write question.
Use system.meta in plugins, custom devtools, and AI tooling for efficient meta access.
Tags travel down the derivation graph
A tag on a fact is a claim about the value. A derivation carries that value forward – often unchanged – so it carries the claim:
schema: {
facts: { email: t.string().meta({ tags: ["pii"] }) },
derivations: { domain: t.string() },
},
derive: { domain: (facts) => facts.email.split("@")[1] },
system.meta.byTag("pii");
// [ { kind: "fact", id: "email", tagOrigin: "authored" },
// { kind: "derivation", id: "domain", tagOrigin: "inherited" } ]
system.meta.derivation("domain")?.inheritedTags; // ["pii"]
inheritedTags is the difference, not the union – a tag the author wrote is still reported under tags. Inheritance is transitive: a derivation reading a derivation reading a tagged fact inherits too.
Saying where the claim stops
Some derivations are the point at which the claim no longer holds – a hash, a bucket, a count, a redaction:
derive: {
bucket: {
compute: (facts) => hash(facts.email) % 16,
meta: { tagBoundary: true },
},
},
Because that is a statement about the value, it holds downstream too: a derivation reading bucket is not walked through to bucket's inputs. It is a separate key rather than an empty tags: [] so a derivation can be sanitized and tagged something unrelated at the same time.
What it can and cannot tell you
Inheritance follows what a derivation actually read on its last computation – the same tracking that lets derivations work without a deps array. A body that branches records the branch the current state takes:
derive: { shown: (facts) => (facts.consented ? facts.email : "") },
While consented is true, shown inherits pii. When it flips, shown stops reading email and stops inheriting.
Read byTag("pii") as "every value carrying PII in the state the system is in", not "every value that ever could". For the second question you want the source, not the runtime.
If you are upgrading from before 1.27.0
Two results grew. Anything acting on byTag() – a redactor, an audit filter, a compliance sweep – now covers derivations it did not before. And system.meta.derivation(id) returns { inheritedTags: [...] } where it used to return undefined for a derivation with no authored meta, so if (system.meta.derivation(id)) no longer means "did anyone annotate this". Compare against ?.label or ?.tags instead.
If you are upgrading from 1.29.x
The shape changed and there are no aliases. MetaMatch.type is now kind; via?: "inherited" is now tagOrigin, always present with both values named; meta: { inheritsTags: false } is now meta: { tagBoundary: true } – note the polarity flip. byCategory() and revision() are gone: use byTag(tag, { kind }) and subscribe().
If you were polling revision() to keep a cache current, you probably do not need the cache at all. carriesTag is O(1) for a fact and cannot go stale.
inspect()
system.inspect() includes meta on all seven definition types:
const inspection = system.inspect();
for (const c of inspection.constraints) {
console.log(c.id, c.meta?.label ?? c.id);
}
for (const r of inspection.resolverDefs) {
console.log(r.id, r.meta?.label ?? r.id);
}
for (const e of inspection.effects) {
console.log(e.id, e.meta?.label ?? e.id);
}
for (const d of inspection.derivations) {
console.log(d.id, d.meta?.label ?? d.id);
}
// Modules too
for (const m of inspection.modules) {
console.log(m.id, m.meta?.label ?? m.id);
}
explain()
system.explain() uses meta.label instead of the raw constraint ID and includes meta.description when present:
Without meta:
Requirement "LOGIN" (id: req-1)
├─ Produced by constraint: needsLogin
├─ Constraint priority: 0
With meta:
Requirement "LOGIN" (id: req-1)
├─ Produced by constraint: Requires Authentication
├─ Constraint priority: 0
├─ Description: User is not logged in
Devtools
The devtools plugin reads meta from inspect() and uses it for:
- Node labels in the constraint/resolver graph
- Color coding by category
- Tooltips with descriptions
Trace Enrichment
When trace: true is enabled, every trace entry automatically carries meta inline on its sub-arrays:
const sys = createSystem({ module: mod, trace: true });
sys.start();
// After reconciliation...
const entry = sys.trace![0];
entry.constraintsHit[0].meta?.label; // "Requires Auth"
entry.resolversStarted[0].meta?.label; // "OAuth Login Flow"
entry.factChanges[0].meta?.label; // "Player Score"
entry.effectsRun[0].meta?.label; // "Phase Logger"
entry.derivationsRecomputed[0].meta?.label; // "Display Name"
Meta flows with the causal event – trace viewers show human-readable labels without secondary lookups.
AI Agent Context
The @directive-run/ai package can inject system meta into LLM prompts automatically:
import { createAgentOrchestrator } from "@directive-run/ai";
const orchestrator = createAgentOrchestrator({
runner,
metaContext: true, // Inject system meta into agent instructions
});
Or build context manually with toAIContext() and formatSystemMeta():
import { toAIContext } from "@directive-run/ai";
const context = toAIContext(system);
// Returns formatted markdown with modules, constraints, resolvers,
// facts, events, effects, derivations – only annotated definitions
The output is token-efficient – sections with no annotated definitions are omitted entirely.
Dynamic Definitions
Meta works with dynamically registered definitions:
system.constraints.register("dynamicCheck", {
when: (facts) => facts.score < 0,
require: { type: "FIX_SCORE" },
meta: { label: "Score Validator", category: "data" },
});
Security
Meta objects are frozen at registration time using Object.create(null) + Object.freeze. This prevents:
- Prototype pollution (no
__proto__chain) - Mutation after registration
- Accidental shared state between definitions
Production Bundles
Meta values are string literals that survive minification and ship in production bundles. This is by design – meta powers production features like AI agent reasoning, compliance scanning (tags: ["pii"]), and observability dashboards.
Avoid putting sensitive information in meta fields:
- Internal API paths (e.g.,
/api/internal/auth/refresh) - Employee names or personal information
- Sensitive business rules you don't want visible in client bundles
Use generic labels instead: "Auth Check" rather than "Refreshes JWT via /api/internal/auth/refresh".
Next Steps
- Constraints – Full constraint API
- Resolvers – Full resolver API
- Effects – Full effects API
- Derivations – Full derivations API
- DevTools – Visualize meta in the devtools panel

