Contexts
Reusable fragments that contribute system text, input fields, and tools to any prompt.
A Crux context is a reusable contribution created with context(), not the
model's context window. A prompt or another context can compose it through
use.
Contexts contribute system text, additional input fields, and optional tools. They can also bundle richer primitives such as memory, retrieval, grounding, skills, and blackboards.
What problem does this solve?
Once you have more than one or two prompts, parts of every prompt repeat: brand voice, terminology, schema definitions, tool instructions, the current date, the user's locale. Copy-pasting them into every prompt creates drift the moment one of them needs to change.
Contexts let you define a fragment once and compose it into any prompt via
use: [...]. Their input schemas merge into the prompt's input type, and their
system text is appended after the prompt's own system text. A context remains
exact unless an explicit representation wrapper authorizes a smaller form.
When should I use a context?
- The same instruction appears in two or more prompts (brand voice, terminology, output format conventions).
- You need a piece of system text that depends on input: current date, mode flag, fetched brand profile.
- You want token-budget-aware degradation: keep critical instructions, drop nice-to-haves under pressure.
- You need to share tools across prompts: declare them on a context, every prompt that uses the context inherits them.
- You want to bundle other primitives: memory, retrieval, grounding, skills, blackboards, or other contexts can live inside a reusable context via
use.
When should I NOT use a context?
- The instruction is specific to one prompt and unlikely to be reused: keep it inline as the prompt's
systemtext. - You only have one or two prompts in total: the indirection isn't worth it yet.
- You only need stateful recall with no reusable policy around it: put memory directly in the prompt's
usearray. Usecontext({ use: [memory] })when you want to package memory with reusable instructions.
Quick example
import { context, prompt } from "@use-crux/core";
import { z } from "zod";
// Static, always contributes the same text
const rules = context({
id: "rules",
system: "## Rules\nAlways respond in valid JSON.",
});
// Dynamic, text depends on input, skipped when input is missing
const brand = context({
id: "brand",
priority: 30,
input: z.object({ brandVoice: z.string().optional() }),
when: ({ input }) => !!input.brandVoice,
system: ({ input }) => `## Brand voice\n${input.brandVoice}`,
});
const reply = prompt({
id: "reply",
use: [rules, brand],
input: z.object({ message: z.string() }),
system: "You are a helpful assistant.",
prompt: ({ input }) => input.message,
});
// TypeScript merges inputs: { message: string, brandVoice?: string }How contexts merge
When a prompt resolves, contexts are processed in this order:
- Filter contexts whose own
whenreturns false, falsy entries, and unmatchedwhen()ormatch()branches. - Resolve each active
system()callback, including nestedcontext({ use })entries before the context that owns them within the same cache tier. - Compose the prompt's own system text first, followed by cached contexts
as a stable prefix and then uncached contexts as the droppable tail. Each
tier preserves
useorder. - Retain exact contributions unless
prefer(),summarizable(),offloadable(), ordroppable()explicitly authorizes another representation.
Priority is 0–100 (default 50). It orders fidelity decisions between
authorized ladders and does not reorder contexts within a cache tier. See
Context Caching for prefix behavior and
Context planning for representation rules.
Conditional Contexts
Most product prompts do not need every context on every call. A support answer might need account context only for signed-in users. A writing prompt might need brand voice only when the caller provides it. A routing prompt might need different instructions for draft, review, and publish modes.
Crux supports three conditional patterns. Use the one that matches where the decision belongs.
Put when on the context when the condition is intrinsic
If a context only makes sense when one of its own input fields is present, put when on the context definition.
import { context, prompt } from "@use-crux/core";
import { z } from "zod";
const brandVoice = context({
id: "brand-voice",
input: z.object({
brandVoice: z.string().optional(),
}),
when: ({ input }) => Boolean(input.brandVoice),
system: ({ input }) => `## Brand voice\n${input.brandVoice}`,
});
const writer = prompt({
id: "writer",
use: [brandVoice],
input: z.object({ topic: z.string() }),
system: "Write the draft.",
prompt: ({ input }) => input.topic,
});brandVoice becomes optional in the merged prompt input type because the context may be excluded at runtime.
Use when() in use when the condition is prompt-specific
Sometimes the same context is reusable, but one prompt only wants it in a specific mode. Keep the context unconditional, then wrap it at the call site.
import { context, prompt, when } from "@use-crux/core";
import { z } from "zod";
const seoGuidance = context({
id: "seo-guidance",
input: z.object({
mode: z.enum(["draft", "seo"]).optional(),
keyword: z.string().optional(),
}),
system: ({ input }) => `Optimize for keyword: ${input.keyword ?? "none"}`,
});
const articlePrompt = prompt({
id: "article",
use: [when(({ mode }) => mode === "seo", seoGuidance)],
input: z.object({ title: z.string() }),
system: "Write an article.",
prompt: ({ input }) => input.title,
});Use this when the same context should be always-on in one prompt, optional in another prompt, and absent in a third.
Use match() for mode-based switching
If exactly one branch should be active, use match() instead of a list of when() wrappers.
import { context, match, prompt } from "@use-crux/core";
import { z } from "zod";
const draftMode = context({
id: "draft-mode",
system: "Prioritize speed and outline-level structure.",
});
const reviewMode = context({
id: "review-mode",
system: "Be strict. Identify gaps, risks, and unsupported claims.",
});
const editor = prompt({
id: "editor",
use: [
match({
on: (input: { mode?: "draft" | "review" }) => input.mode ?? "draft",
cases: {
draft: draftMode,
review: reviewMode,
},
}),
],
input: z.object({
mode: z.enum(["draft", "review"]).optional(),
text: z.string(),
}),
system: "Edit the text.",
prompt: ({ input }) => input.text,
});match() is easier to read than several predicates when the branches are mutually exclusive. Branches can contain a single context or an array of contexts.
Use falsy entries for static flags
For feature flags or environment-specific composition, you can put falsy values directly in use. Crux ignores them.
const assistant = prompt({
id: "assistant",
use: [baseRules, process.env.NODE_ENV === "development" && debugContext],
system: "Help the user.",
});This is best for static decisions known when the prompt is created. Use when() for runtime decisions based on input.
Preview the composed request
Use preview() with the same input and model to measure the prospective
complete request without executing it. Conditional contexts follow the same
resolution path as execution.
Excluded contexts are not resolved, do not call async system() functions, and do not contribute tools. Their input keys still become optional in the merged type because Crux cannot know at compile time which runtime branch will be active.
Contexts Can Compose Other Primitives
context() also accepts use, just like prompt(). Nested entries resolve before the context's own system text. This lets you package a reusable policy with the memory, retrieval, grounding, skills, blackboard, or tools that make that policy work.
import { context, prompt } from "@use-crux/core";
import { memory, facts } from "@use-crux/core/memory";
import { skill } from "@use-crux/core/skill/node";
import { z } from "zod";
const userMemory = memory({
id: "user-memory",
store,
namespace: ({ input }) => `user:${input.userId}`,
blocks: [facts({ id: "facts", embed: dense })],
});
const escalationSkill = skill.fromFile("./skills/escalation/SKILL.md");
const personalizedSupport = context({
id: "personalized-support",
use: [userMemory, escalationSkill],
system:
"Use durable user memory when it is relevant. Load the escalation skill for account-risk or billing-risk questions.",
});
const supportAgent = prompt({
id: "support-agent",
use: [personalizedSupport],
input: z.object({
userId: z.string(),
question: z.string(),
}),
system: "Answer the support question.",
prompt: ({ input }) => input.question,
});This keeps prompt wiring small:
prompt -> personalizedSupport -> userMemory + escalationSkillThe nested memory still behaves like memory. It renders its context, exposes
its tools, participates in capture/flush, and appears in request evidence
through the same resolution path as if it were placed directly in the prompt
use array.
Use this pattern when a context is really a product capability, not just text. For one-off prompts, putting memory, retrieval, grounding, or skills directly in prompt({ use: [...] }) is simpler.
How this fits with the rest of Crux
- Memory can be placed directly in
prompt({ use })or bundled insidecontext({ use }). - Retrieval and grounding follow the same pattern when a context should own a reusable evidence policy.
- Skills are Markdown-based instruction sets that also flow through
use: [...]. - Tools can be declared on a context and inherited by every prompt that uses it.