Copybook
Concepts

Definitions

Commands, snippets, prompts, and collections.

Everything your product shows is a definition. Every definition has a stable id, typed params, and metadata that the CLI catalogs.

KindHelperExample
CommanddefineCommandneon init --project-id <id>
SnippetdefineSnippetAn AI SDK example in TypeScript, Python, and cURL
PromptdefinePromptA "set up this app" prompt for a coding agent

Metadata

All three helpers accept the same metadata:

defineCommand({
  id: "neon.init", // required: lowercase, dotted, never reused
  title: "Initialize a project",
  description: "Links the current folder to a Neon project.",
  placements: ["console/create-project"], // where the product shows it
  tags: ["cli"],
  owners: ["@devrel"],
  aliases: ["neon.link"], // previous ids, so history follows a rename
  template: (t) => t`neon init`,
});

Ids

Ids are how everything else refers to a definition: snapshots, test results, eval history, and analytics. Use the form area.surface.name, keep ids lowercase, and never reuse one. When you rename a definition, add the old id to aliases.

Commands

A command is one shell command. Its verify option tells the test runner how far to go:

import { defineCommand, skip } from "@copybook/core";

export const login = defineCommand({
  id: "neon.login",
  template: (t) => t`neon login`,
  verify: skip("interactive: opens a browser"),
});
verifyWhat the runner does
"syntax" (default)Parses it without running it
"typecheck"Installs dependencies and typechecks it (snippets)
"run"Runs it in a sandbox with fixture values
skip(reason)Never runs it, and records why

Snippets

A snippet is a code block. Give it a lang and a template, or static code:

import { defineSnippet } from "@copybook/core";

export const env = defineSnippet({
  id: "acme.connect.env",
  lang: "dotenv",
  code: "DATABASE_URL=postgres://user:password@host/db",
});

When one snippet comes in several languages or frameworks, declare variants.

Prompts

A prompt is text for a coding agent. It usually embeds commands and snippets:

export const getStarted = definePrompt({
  id: "console.create-project.agent",
  template: (t) => t`
    1. ${t.cmd(install)}
    2. ${t.cmd(login)}
  `,
});

Embedded definitions keep their ids in the rendered tree. That's how the CLI knows that neon.login appears in this prompt, and how the test runner finds commands to run.

Collections

Sometimes definitions come from data, such as one snippet per model. defineCollection builds them with ids derived from a stable key:

import { defineCollection, defineSnippet } from "@copybook/core";

export const modelPages = defineCollection({
  idPrefix: "ai-gateway.model",
  items: models,
  key: (model) => model.slug, // never the array index
  build: (model, { id }) =>
    defineSnippet({ id, lang: "ts", template: (t) => t`...` }),
});

modelPages.get("claude-haiku-5-5"); // id: ai-gateway.model.claude-haiku-5-5

Prefer params over collections

If only one string changes between items, such as a model id, make it a param instead of generating a definition per item. See Variants.

On this page