Skip to content
KONIGI

AI Assistants / Memory and context / Custom instructions

3 of 5

Custom instructions

The same correction gets retyped at the start of every conversation.

Updated September 12, 2026

Problem

Every conversation opens with the same three sentences. Write in British English. Do not use bullet points. I am a designer, not an engineer. Typing them again is a tax on every single session, and forgetting to type them produces an answer that has to be thrown away.

Solution

A persistent block of text the viewer writes, prepended to every conversation. The distinction from memory is worth holding firmly: these are declared rather than inferred. The viewer wrote them, knows they exist, and can change them. Memory is the system’s guess about them, and confusing the two in one interface makes both harder to reason about.

The real design problem isn’t the text field. It’s that instructions written once become invisible and keep working. Six months later an answer is strangely formal, or keeps producing tables, and the cause is a sentence written in February that the viewer has forgotten writing. HAX guideline 17 asks for global controls over what the system does, and the control is only real if the viewer can find the thing that’s currently in force.

Two cheap mechanisms fix most of that. The first is a visible indicator on the conversation showing that custom instructions are active, with a click to see them. The second is a date: showing when each instruction was written turns an invisible standing order into something a person can audit.

Scope is the second decision. One global block is simple and wrong for anyone who uses the product in more than one role, because instructions for writing code are actively harmful when drafting an email. Per-project or per-workspace instructions, layered over a small global set, matches how people actually work. Layering has to resolve predictably, and the resolution order should be stated rather than discovered.

The field also needs honesty about its limits. Instructions are advisory, not binding. A model will drift from them over a long conversation, weight them against the immediate request, and occasionally ignore them outright. An interface that presents the box as a settings panel implies a determinism the mechanism doesn’t have, and the resulting complaint is that the product ignores its own settings. Framing them as standing guidance is more accurate and sets a recoverable expectation.

Length works against the viewer in a way nobody warns them about. A long instruction block consumes context on every turn and dilutes itself, so the twentieth rule weakens the first nineteen. A visible character budget does more good here than most affordances in this pattern.

Use when

Someone uses the assistant repeatedly with stable preferences that are cheap to state and expensive to repeat.

Don’t use when

Use is occasional or the account is shared. Instructions written by one person on a shared login silently reshape everyone else’s answers, and the people affected have no idea the block exists.

Trade-offs

Standing instructions remove repetition and make behaviour harder to debug, since the prompt on screen is no longer the whole input. They also compound with memory: an inferred preference and a declared one can contradict, and few products say which wins. Per-project scoping is more useful and adds a resolution order people have to learn. Every instruction spends context on every turn forever. Against a long conversation that’s a real cost, and a silent one.

Checklist

  • Can the viewer tell from a conversation that instructions are active?
  • Is the text reachable in one click from where it takes effect?
  • Is the date each instruction was written visible?
  • Do global and project instructions layer in a stated order?
  • Is there a budget or a warning as the block grows?
  • Does the interface promise determinism it cannot deliver?
  • What happens when an instruction conflicts with a stored memory?
  • Can instructions be turned off for one conversation without deleting them?
  • On a shared account, is it clear whose instructions are in force?
  • Are the instructions included when a conversation is shared or exported?

Compare

ChatGPT keeps a global instruction block and adds per-project instructions above it, so the layering is explicit in the interface rather than implied. Claude attaches the equivalent to projects, so the natural unit is a body of work rather than the account. That fits anyone who switches between roles. Gemini leans toward inferring preferences from past chats instead of asking for them, trading the audit trail for less setup. Notion puts the equivalent in the workspace as written documentation the assistant reads, so the standing instructions are also a thing colleagues can see and edit.

Memory chip is the inferred counterpart and the one this gets confused with. Scoped context determines which layer of instructions applies. Conversation history is where instructions quietly shaped every past answer. First-run state is where the existence of this feature should be introduced. Refusal is what an over-restrictive instruction block starts producing.

Custom instructions anatomy A standing instruction block with each rule dated, a project layer above a global layer with the resolution order stated, a budget showing how much of every turn the block consumes, and an indicator on the conversation that instructions are in force. Written once, in force forever, forgotten by March This project Cite the source file for every claim 1 Sep Everywhere British English, no bullet lists 4 Feb I am a designer, not an engineer 4 Feb project overrides global spent on every turn, forever advisory rather than binding, and a long block dilutes itself 3 instructions active on the conversation, one click to the text 1 2 3 4 1 DATE EVERY RULE An answer that is strangely formal traces to a sentence written in February. 2 A STATED ORDER Code instructions are harmful when drafting an email. Layer, and say how. 3 A VISIBLE BUDGET The twentieth rule weakens the first nineteen and costs context every turn. 4 DO NOT PROMISE SETTINGS A model drifts from these and weighs them against the immediate request. On a shared login, one person's standing instructions reshape everyone else's answers silently.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Written once, in force forever, forgotten by March. Each rule carries a date, the project layer overrides the global one, and the block spends part of every turn whether or not it still applies.

shadcn
npx shadcn@latest add button progress
Tokens
--card--card-foreground--muted--muted-foreground--border--status-warn--chart-1--scale-seq-3

This project

  • Cite the source file for every claim

Everywhere

  • British English, no bullet lists
  • I am a designer, not an engineer

project overrides global

100 of 400 characters, spent on every turn, forever

Standing guidance: advisory rather than binding, and a long block dilutes itself

CustomInstructions.tsxTwo scopes in a stated order, a date on every rule, a character budget computed from the text, and a chip that counts what is in force.

import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";

/** A rule the viewer wrote, and when. The date is what turns a standing order
 *  into something a person can audit six months later. */
export type Rule = { text: string; written: Date };

/** Two scopes, no more. Project sits above global and wins on conflict; the
 *  order is printed rather than left to be discovered. */
export type Scope = "project" | "global";
export type Layer = { scope: Scope; name: string; rules: Rule[] };

const SCOPE_ORDER: Scope[] = ["project", "global"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const day = (d: Date) => `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;

type Props = {
  layers: Layer[];
  /** Characters the block may spend on each turn. Every rule is prepended to
   *  every conversation, so the cost is per turn and it never stops. */
  limit: number;
  /** The chip on the conversation opens the text. One click, no settings dive. */
  onOpen: () => void;
};

export function CustomInstructions({ layers, limit, onOpen }: Props) {
  const ordered = [...layers].sort((a, b) => SCOPE_ORDER.indexOf(a.scope) - SCOPE_ORDER.indexOf(b.scope));
  const rules = ordered.flatMap((l) => l.rules);
  const used = rules.reduce((n, r) => n + r.text.length, 0);
  const pct = Math.min(100, Math.round((used / limit) * 100));

  return (
    <div className="grid grid-cols-[1.1fr_0.9fr] gap-5 rounded-lg border bg-card p-4 text-[11px]">
      <div className="rounded-lg border bg-muted">
        {ordered.map((layer, i) => (
          <div key={layer.scope} className={i > 0 ? "border-t" : undefined}>
            <p className="px-3 pt-2.5 uppercase tracking-wide text-muted-foreground">{layer.name}</p>
            <ul className="pb-2.5">
              {layer.rules.map((r) => (
                <li key={r.text} className="flex items-baseline gap-3 px-3 pt-1.5">
                  <span className="text-card-foreground">{r.text}</span>
                  <time className="ml-auto tabular-nums text-muted-foreground" dateTime={r.written.toISOString()}>
                    {day(r.written)}
                  </time>
                </li>
              ))}
            </ul>
          </div>
        ))}
      </div>

      <div>
        <p className="text-muted-foreground">project overrides global</p>
        <Progress value={pct} className="mt-2 h-2.5 bg-muted [&>div]:bg-scale-seq-3" aria-label="context budget" />
        <p className="mt-1.5 text-muted-foreground">
          {used} of {limit} characters, spent on every turn, forever
        </p>
        {/* The box is not a settings panel and should not look like one. */}
        <p className="mt-4 leading-relaxed text-status-warn">
          Standing guidance: advisory rather than binding, and a long block dilutes itself
        </p>
      </div>

      <div className="col-span-2 border-t pt-4">
        <Button variant="ghost" size="sm" onClick={onOpen} className="h-7 rounded-md bg-chart-1/15 px-3 text-[11px] text-chart-1 hover:bg-chart-1/25 hover:text-chart-1">
          {rules.length} instructions active
        </Button>
      </div>
    </div>
  );
}

demo.tsxHow it is called: one project rule over two global ones, 99 characters of a 400 budget.

import { CustomInstructions } from "./CustomInstructions";

/**
 * One project rule over two global ones, each dated. 99 characters against a
 * 400 budget puts the bar at a quarter. The chip is what a conversation shows;
 * clicking it would open this panel.
 */
export default function Demo() {
  return (
    <CustomInstructions
      layers={[
        {
          scope: "project",
          name: "This project",
          rules: [{ text: "Cite the source file for every claim", written: new Date("2026-09-01") }],
        },
        {
          scope: "global",
          name: "Everywhere",
          rules: [
            { text: "British English, no bullet lists", written: new Date("2026-02-04") },
            { text: "I am a designer, not an engineer", written: new Date("2026-02-04") },
          ],
        },
      ]}
      limit={400}
      onOpen={() => {}}
    />
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.