Skip to main content
Artifacts are rich content panels that appear alongside the conversation. When the model calls createDocument or updateDocument, it picks an artifact kind, and the system routes that to the correct server-side handler and client-side renderer.

Built-in artifact kinds

The artifactKinds constant in lib/artifacts/server.ts defines the four supported types:
lib/artifacts/server.ts
export const artifactKinds = ['text', 'code', 'image', 'sheet'] as const;
The ArtifactKind type is derived in components/artifact.tsx from the registered client-side artifact definitions:
components/artifact.tsx
export const artifactDefinitions = [
  textArtifact,
  codeArtifact,
  imageArtifact,
  sheetArtifact,
];
export type ArtifactKind = (typeof artifactDefinitions)[number]['kind'];
ArtifactKind resolves to 'text' | 'code' | 'image' | 'sheet' and is used across the codebase wherever an artifact type is referenced — in tool parameters, document handlers, database queries, and UI components.

How the AI decides when to create an artifact

The system prompt in lib/ai/prompts.ts instructs the model when to use createDocument and updateDocument:
lib/ai/prompts.ts
export const artifactsPrompt = `
Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user.

When asked to write code, always use artifacts. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language.

DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT.

This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation.

**When to use \`createDocument\`:**
- For substantial content (>10 lines) or code
- For content users will likely save/reuse (emails, code, essays, etc.)
- When explicitly requested to create a document
- For when content contains a single code snippet

**When NOT to use \`createDocument\`:**
- For informational/explanatory content
- For conversational responses
- When asked to keep it in chat

**Using \`updateDocument\`:**
- Default to full document rewrites for major changes
- Use targeted updates only for specific, isolated changes
- Follow user instructions for which parts to modify

**When NOT to use \`updateDocument\`:**
- Immediately after creating a document
`;
The reasoning model (chat-model-reasoning) receives only the regularPrompt and never the artifactsPrompt. This keeps the reasoning model from calling artifact tools, which it does not support.

Server-side handlers

Each artifact kind has a server-side handler in artifacts/<kind>/server.ts. Handlers are created with createDocumentHandler() and must implement two callbacks: onCreateDocument and onUpdateDocument. Both receive a DataStreamWriter and use it to stream content to the client in real time.

Text handler

artifacts/text/server.ts
import { smoothStream, streamText } from 'ai';
import { myProvider } from '@/lib/ai/models';
import { createDocumentHandler } from '@/lib/artifacts/server';
import { updateDocumentPrompt } from '@/lib/ai/prompts';

export const textDocumentHandler = createDocumentHandler<'text'>({
  kind: 'text',
  onCreateDocument: async ({ title, dataStream }) => {
    let draftContent = '';

    const { fullStream } = streamText({
      model: myProvider.languageModel('artifact-model'),
      system:
        'Write about the given topic. Markdown is supported. Use headings wherever appropriate.',
      experimental_transform: smoothStream({ chunking: 'word' }),
      prompt: title,
    });

    for await (const delta of fullStream) {
      const { type } = delta;

      if (type === 'text-delta') {
        const { textDelta } = delta;

        draftContent += textDelta;

        dataStream.writeData({
          type: 'text-delta',
          content: textDelta,
        });
      }
    }

    return draftContent;
  },
  onUpdateDocument: async ({ document, description, dataStream }) => {
    // same pattern — stream text-delta events, accumulate, return final content
  },
});

Code handler

The code handler streams a structured object with a code field using streamObject, then emits code-delta events:
artifacts/code/server.ts
import { z } from 'zod';
import { streamObject } from 'ai';
import { myProvider } from '@/lib/ai/models';
import { codePrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
import { createDocumentHandler } from '@/lib/artifacts/server';

export const codeDocumentHandler = createDocumentHandler<'code'>({
  kind: 'code',
  onCreateDocument: async ({ title, dataStream }) => {
    let draftContent = '';

    const { fullStream } = streamObject({
      model: myProvider.languageModel('artifact-model'),
      system: codePrompt,
      prompt: title,
      schema: z.object({
        code: z.string(),
      }),
    });

    for await (const delta of fullStream) {
      const { type } = delta;

      if (type === 'object') {
        const { object } = delta;
        const { code } = object;

        if (code) {
          dataStream.writeData({
            type: 'code-delta',
            content: code ?? '',
          });

          draftContent = code;
        }
      }
    }

    return draftContent;
  },
  // onUpdateDocument follows the same pattern
});

Handler registration

All handlers are collected in lib/artifacts/server.ts and looked up by kind at runtime:
lib/artifacts/server.ts
export const documentHandlersByArtifactKind: Array<DocumentHandler> = [
  textDocumentHandler,
  codeDocumentHandler,
  imageDocumentHandler,
  sheetDocumentHandler,
];
When createDocument or updateDocument runs, it calls documentHandlersByArtifactKind.find(h => h.kind === kind) to select the correct handler.

Client-side renderers

Each artifact kind has a client-side renderer in artifacts/<kind>/client.tsx. Renderers are instances of the Artifact class and define:
PropertyPurpose
kindMust match the server-side handler’s kind
descriptionShort description shown in the UI
initializeAsync setup called when the artifact panel opens
onStreamPartHandles incoming dataStream events and updates local state
contentReact component that renders the artifact body
actionsToolbar buttons shown in the artifact panel header
toolbarContextual action buttons shown inside the artifact

Text client

The text renderer listens for text-delta events to accumulate content as it streams, and suggestion events to display inline suggestions:
artifacts/text/client.tsx
export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
  kind: 'text',
  description: 'Useful for text content, like drafting essays and emails.',
  initialize: async ({ documentId, setMetadata }) => {
    const suggestions = await getSuggestions({ documentId });
    setMetadata({ suggestions });
  },
  onStreamPart: ({ streamPart, setMetadata, setArtifact }) => {
    if (streamPart.type === 'suggestion') {
      setMetadata((metadata) => ({
        suggestions: [...metadata.suggestions, streamPart.content as Suggestion],
      }));
    }

    if (streamPart.type === 'text-delta') {
      setArtifact((draftArtifact) => ({
        ...draftArtifact,
        content: draftArtifact.content + (streamPart.content as string),
        status: 'streaming',
      }));
    }
  },
  content: ({ content, metadata, status, ...props }) => {
    return (
      <Editor
        content={content}
        suggestions={metadata ? metadata.suggestions : []}
        status={status}
        {...props}
      />
    );
  },
  // actions and toolbar defined here
});

Code client

The code renderer listens for code-delta events and renders a CodeEditor. It also provides a Run action that executes Python code in-browser using Pyodide:
artifacts/code/client.tsx
export const codeArtifact = new Artifact<'code', Metadata>({
  kind: 'code',
  description:
    'Useful for code generation; Code execution is only available for python code.',
  onStreamPart: ({ streamPart, setArtifact }) => {
    if (streamPart.type === 'code-delta') {
      setArtifact((draftArtifact) => ({
        ...draftArtifact,
        content: streamPart.content as string,
        status: 'streaming',
      }));
    }
  },
  // content renders <CodeEditor />
  // actions include Run, Undo, Redo, Copy
});

createDocumentHandler internals

createDocumentHandler() in lib/artifacts/server.ts wraps your onCreateDocument and onUpdateDocument callbacks. After each callback completes and returns the final content string, it automatically persists the document to the database:
lib/artifacts/server.ts
export function createDocumentHandler<T extends ArtifactKind>(config: {
  kind: T;
  onCreateDocument: (params: CreateDocumentCallbackProps) => Promise<string>;
  onUpdateDocument: (params: UpdateDocumentCallbackProps) => Promise<string>;
}): DocumentHandler<T> {
  return {
    kind: config.kind,
    onCreateDocument: async (args) => {
      const draftContent = await config.onCreateDocument(args);

      if (args.session?.user?.id) {
        await saveDocument({
          id: args.id,
          title: args.title,
          content: draftContent,
          kind: config.kind,
          userId: args.session.user.id,
        });
      }
    },
    // onUpdateDocument follows the same pattern
  };
}
Your onCreateDocument callback is responsible for streaming content via dataStream and returning the final accumulated string. createDocumentHandler takes care of saving it.
If onCreateDocument returns an empty string or the session user ID is not present, the document will not be saved to the database.