# Warlock Context — full skills > Package: `@warlock.js/context` > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/context/skills/`. Re-run `node scripts/generate-llms.mjs` after any change. ## define-context `@warlock.js/context/define-context/SKILL.md` --- name: define-context description: 'Extend Context to define an AsyncLocalStorage-backed typed context — implement buildStore, use run / enter / update / get / set / getStore / clear / hasContext. Triggers: `Context`, `Context`, `buildStore`, `run`, `enter`, `update`, `get`, `set`, `getStore`, `clear`, `hasContext`; "share user/tenant/trace id across async calls", "AsyncLocalStorage typed wrapper", "request-scoped store without thread-through"; typical import `import { Context } from "@warlock.js/context"`. Skip: orchestrating multiple contexts — `@warlock.js/context/orchestrate-contexts/SKILL.md`; native `AsyncLocalStorage`, `cls-hooked`, `nest-context`, React Context.' --- # Define a context `@warlock.js/context` is a tiny wrapper on Node.js's `AsyncLocalStorage`. You extend the abstract `Context` class to declare what your context stores, and you get a typed get/set/run API that propagates through every async call inside the scope. ## Install ```bash yarn add @warlock.js/context ``` ## Shape ```ts import { Context } from "@warlock.js/context"; interface UserContextStore { userId: string; role: "admin" | "user"; tenantId: string; } class UserContext extends Context { /** * Called by `contextManager.buildStores(payload)` for each registered context. * Override to provide initialization logic for this context's store. */ public buildStore(payload?: Record): UserContextStore { return { userId: payload?.userId ?? "", role: payload?.role ?? "user", tenantId: payload?.tenantId ?? "", }; } } export const userContext = new UserContext(); ``` `buildStore` is the **only** abstract method. Everything else is provided by `Context`. ## Usage modes ### `run()` — scoped execution ```ts await userContext.run( { userId: "123", role: "admin", tenantId: "acme" }, async () => { // Context is available throughout this async scope and any awaited calls inside it. const userId = userContext.get("userId"); // "123" const role = userContext.get("role"); // "admin" await someAsyncOperation(); // context propagates through awaits }, ); ``` Use `run()` when you have a clear scope boundary (a request handler, a job, a CLI command). The context is auto-cleaned when the callback returns. ### `enter()` — middleware-style, no callback ```ts function authMiddleware(req, res, next) { userContext.enter({ userId: req.user.id, role: req.user.role, tenantId: req.headers["x-tenant-id"], }); next(); // context lives for the rest of the request } ``` Use `enter()` when the framework doesn't give you a callback to wrap (Express-style middleware). Under the hood it's `AsyncLocalStorage.enterWith(store)`. ### `update()` — merge into the current context ```ts userContext.update({ role: "admin" }); // existing store: { userId, role, tenantId } → { userId, role: "admin", tenantId } ``` If there's no current store, `update` creates one with the partial (cast to the full type). Use for incremental enrichment as the request flows through layers. ## Reading ```ts const userId = userContext.get("userId"); // TStore[K] | undefined const store = userContext.getStore(); // TStore | undefined const inside = userContext.hasContext(); // boolean ``` `get` is the daily-use accessor. `getStore` returns the whole record. `hasContext` distinguishes "key absent" from "no context at all" (which matters for safety checks at the framework boundary). ## Writing within a context ```ts userContext.set("role", "admin"); // sugar for update({ role: "admin" }) ``` Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want. ## Clearing ```ts userContext.clear(); // replaces the current store with an empty object of TStore ``` Rare in app code. The auto-cleanup at the end of `run()` is the normal path. ## Convenience getters via subclass Add domain-friendly getters on the subclass when a key is read a lot: ```ts class TenantContext extends Context { public buildStore(payload?: Record): TenantStore { return { tenantId: payload?.tenantId ?? "", tenantName: payload?.tenantName ?? "", config: payload?.config ?? {}, }; } public get tenantId() { return this.get("tenantId"); } public get config() { return this.get("config"); } } ``` Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site. ## What it's NOT for - **Persistent state across requests.** AsyncLocalStorage is per-call-tree; data dies when the scope ends. Use a cache, a database, or a singleton for cross-request data. - **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates. - **Sync code that doesn't `await` anything.** Works, but if there's no async boundary the context add-overhead is wasted — just pass the data as a parameter. ## See also - [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton. ## Things NOT to do - Don't make every cross-cutting concern a context. Build one when it has its own lifecycle (request, db transaction, trace span). For one-off data, a function argument is clearer. - Don't capture the store reference outside the scope — it's freed when `run()` ends. Read the value out before exiting the scope if you need it later. - Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable. - Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`. ## orchestrate-contexts `@warlock.js/context/orchestrate-contexts/SKILL.md` --- name: orchestrate-contexts description: 'Orchestrate multiple Context instances via the contextManager singleton — register, buildStores, runAll, enterAll, clearAll. Triggers: `contextManager`, `register`, `buildStores`, `runAll`, `enterAll`, `clearAll`, `unregister`, `getContext`, `hasContext`; "run multiple contexts active for the same scope", "register contexts at boot", "avoid nested run() calls for request + database + tenant"; typical import `import { contextManager } from "@warlock.js/context"`. Skip: defining a single context class — `@warlock.js/context/define-context/SKILL.md`; native `AsyncLocalStorage` nesting, `cls-hooked` namespaces.' --- # Orchestrate contexts — `contextManager` `contextManager` is a singleton that knows about every registered `Context` and runs them all together so you don't write nested `run()` calls by hand. ## Why use it You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting: ```ts // ❌ Without the manager — fragile, easy to forget a layer await requestContext.run(reqStore, async () => databaseContext.run(dbStore, async () => tenantContext.run(tenantStore, async () => handle()), ), ); // ✅ With the manager — one call, deterministic order await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle); ``` ## Register contexts at boot ```ts import { contextManager } from "@warlock.js/context"; import { requestContext } from "./request-context"; import { databaseContext } from "./database-context"; import { tenantContext } from "./tenant-context"; contextManager .register("request", requestContext) .register("database", databaseContext) .register("tenant", tenantContext); ``` Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites. ## Build stores + run The typical flow is two-step: build initial stores from a request-like payload, then run. ```ts app.use(async (req, res, next) => { const stores = contextManager.buildStores({ request: req, response: res, tenantId: req.headers["x-tenant-id"], }); await contextManager.runAll(stores, async () => { await next(); }); }); ``` `buildStores(payload)` calls each registered context's `buildStore(payload)` (the abstract method on `Context` — see [`@warlock.js/context/define-context/SKILL.md`](@warlock.js/context/define-context/SKILL.md)) and returns `{ [contextName]: store }`. `runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`. ## `enterAll()` — middleware without a callback When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return): ```ts function contextMiddleware(req, res, next) { const stores = contextManager.buildStores({ request: req, response: res }); contextManager.enterAll(stores); next(); } ``` `enterAll` calls `enter()` on each registered context whose name has a **truthy** store value — a `name` with no key (or a falsy value like `undefined`) is skipped, leaving any already-active store for that context untouched. The entered contexts live for the rest of the request. Each can still be `clear()`-ed later. Use `runAll` when you can — `enterAll` doesn't auto-clean. ## Lookup + introspection ```ts contextManager.hasContext("tenant"); // boolean const tenant = contextManager.getContext("tenant"); // returns the registered instance or undefined contextManager.unregister("debug"); // remove a context contextManager.clearAll(); // clear stores on every context ``` `getContext` returns the registered instance cast to `T` (the generic is constrained to `Context`, so pass the concrete context class — `getContext("tenant")`). It's an unchecked cast: an unknown `name` returns `undefined`, and a wrong type argument won't be caught at runtime. Useful in shared utilities that want to read from a context without importing it at the call site. ## Order matters `runAll` nests in **registration order**. The first-registered context is the outermost layer. If contexts have ordering constraints (database before tenant because tenant resolves via the db), register them in that order. ```ts contextManager .register("trace", traceContext) // outermost — runs first .register("request", requestContext) .register("database", databaseContext) .register("tenant", tenantContext); // innermost — runs last, inside all others ``` ## Real-world: multi-tenant request lifecycle ```ts import { Context, contextManager } from "@warlock.js/context"; import { randomUUID } from "crypto"; class TraceContext extends Context<{ traceId: string; startTime: number }> { public buildStore(): { traceId: string; startTime: number } { return { traceId: randomUUID(), startTime: Date.now() }; } public get traceId() { return this.get("traceId"); } } class RequestContext extends Context<{ request: any; response: any }> { public buildStore(payload?: any) { return { request: payload?.request, response: payload?.response }; } } class TenantContext extends Context<{ tenantId: string }> { public buildStore(payload?: any) { return { tenantId: payload?.tenantId ?? "" }; } } export const traceContext = new TraceContext(); export const requestContext = new RequestContext(); export const tenantContext = new TenantContext(); contextManager .register("trace", traceContext) .register("request", requestContext) .register("tenant", tenantContext); // In your HTTP layer: async function handleRequest(req: any, res: any) { const stores = contextManager.buildStores({ request: req, response: res, tenantId: req.headers["x-tenant-id"], }); return contextManager.runAll(stores, async () => { // All three contexts active here: console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`); await routeAndDispatch(req, res); }); } ``` ## Things NOT to do - Don't register the same context under two names — every context is its own singleton and shares its store across registrations, but multiple names confuse `buildStores` (the payload is split per name). - Don't `runAll` an empty stores map — every key without a registered context is silently ignored, and missing contexts get `{}` as their store. Better to construct the stores explicitly and fail loudly when a key is missing. - Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics. - Don't expect `enterAll()` to auto-clean. It's a one-way setup — pair with `clearAll()` at the end of the request, or just use `runAll` when you can. ## overview `@warlock.js/context/overview/SKILL.md` --- name: overview description: 'Front-door orientation for `@warlock.js/context` — typed AsyncLocalStorage wrappers for sharing data (user, tenant, trace, request) across async calls without thread-through. Extend `Context` for a single context; use `contextManager` to orchestrate several at once. TRIGGER when: code imports anything from `@warlock.js/context`; user asks "what does @warlock.js/context do", "AsyncLocalStorage but typed", "share user/tenant across async without thread-through", "compare context vs cls-hooked / nest-context"; package.json adds `@warlock.js/context`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/context/define-context/SKILL.md`, `@warlock.js/context/orchestrate-contexts/SKILL.md`); plain `AsyncLocalStorage` usage with no `Context<>` wrapper; React Context (this package is server-side / Node-only).' --- # `@warlock.js/context` — overview Two-file package: a typed wrapper over Node's `AsyncLocalStorage` and a singleton that runs several of them together. That's it. The entire surface fits in two skills below — most callers only need the first one. ## When to reach for it - You have request-scoped data (user, tenant, trace id, db transaction) that you'd otherwise thread through every function as a parameter. One `userContext.get("userId")` anywhere down the call tree replaces five layers of plumbing. - You're inside a `@warlock.js/*` project and want consistent context handling across modules. The framework already uses this package internally for request + user + tenant contexts. - You'd reach for `cls-hooked`, `nest-context`, or a bare `AsyncLocalStorage` and want a typed wrapper with `run` / `enter` / `update` / `get` / `set` semantics out of the box. Skip if your call chain is a single function with no `await` boundaries — just pass the data as a parameter. Skip if you need cross-request shared state — that's a cache or database, not a context. ## What it is in one sentence Each `Context` subclass declares a typed store shape and what payload builds it; the framework uses Node's `AsyncLocalStorage` under the hood so the store propagates through every `await` inside the scope and disappears when the scope ends. ## Skills index Two task skills cover everything. The first one is the daily-use one; the second is for when you have multiple contexts active at the same time. ### [`define-context/`](../define-context/SKILL.md) Extend `Context` to declare what your context stores and how it's built. Implement `buildStore(payload?)`; use `run` / `enter` / `update` / `get` / `set` / `getStore` / `clear` / `hasContext` to interact with the store from anywhere in the async scope. Load when sharing data (user, tenant, trace id) across async calls without threading it through every function — i.e. ~90% of the time you use this package. ### [`orchestrate-contexts/`](../orchestrate-contexts/SKILL.md) Register multiple `Context` instances on the `contextManager` singleton and run them all together with a single call. Covers `register` / `unregister`, `buildStores`, `runAll` / `enterAll`, `clearAll`, `getContext` / `hasContext`. Load when several contexts apply to the same scope (request + database + trace + tenant) and you'd otherwise nest `run()` calls by hand. ## Two operating modes — when to pick which | Situation | Reach for | | --- | --- | | Framework gives you a callback to wrap | `context.run(store, callback)` | | Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` | | You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` | | Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end | | You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. | ## What this package deliberately doesn't do - **Persist data across requests.** AsyncLocalStorage dies with the scope. For cross-request state, reach for `@warlock.js/cache` or a database. - **Cross-thread context.** Worker threads don't share AsyncLocalStorage with the main thread. Serialize the data you need across the boundary and re-enter on the other side. - **Browser context.** Server-side only. The browser equivalent is React Context or a state library. - **Implicit context for non-async code.** If your call stack has no `await`, the storage works but there's no propagation benefit — just pass the value as a parameter. ## See also - [`@warlock.js/core/overview/SKILL.md`](@warlock.js/core/overview/SKILL.md) — the parent framework; `context` is one of its foundation packages. - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes the front-door skill in `.claude/skills/warlock-js-context-overview/`.