> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Prompt management — versioned prompts with the SDK

> Fetch, compile, create, and manage versioned prompts from the Zespan prompt library using PromptClient, with built-in caching and variable substitution. TypeScript and Python.

The Zespan prompt library stores versioned prompts that your application fetches at runtime. This lets you update prompts without redeploying code, experiment with variants using labels, and track which prompt version produced which LLM output. `PromptClient` is the SDK interface to this library, available in both the TypeScript and Python SDKs with the same method names and behavior (Python uses `snake_case`).

## Getting a `PromptClient`

The `PromptClient` is available two ways: from the initialized `zespan` client, or by constructing it directly.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan } from "@zespan/sdk";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

  // Via the client (recommended)
  const prompts = zespan.getClient().prompts;

  // Or import and construct directly
  import { PromptClient, getZespanClient } from "@zespan/sdk";
  const prompts = new PromptClient(getZespanClient());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan
  from zespan import get_client

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])

  # Via the client (recommended)
  prompts = get_client().prompts

  # Or import and construct directly
  from zespan import PromptClient
  prompts = PromptClient(get_client())
  ```
</CodeGroup>

## Fetching a prompt

`get(name, options?)` (TypeScript) / `get(name, ...)` (Python) fetches a prompt by name. By default it returns the latest version. Results are cached locally for 5 minutes.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Latest version
  const prompt = await prompts.get("support-reply");

  // Specific version
  const prompt = await prompts.get("support-reply", { version: 3 });

  // By label — useful for staging/production separation
  const prompt = await prompts.get("support-reply", { label: "production" });

  // Skip the local cache and always fetch fresh
  const prompt = await prompts.get("support-reply", { cache: false });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Latest version
  prompt = prompts.get("support-reply")

  # Specific version
  prompt = prompts.get("support-reply", version=3)

  # By label — useful for staging/production separation
  prompt = prompts.get("support-reply", label="production")

  # Skip the local cache and always fetch fresh
  prompt = prompts.get("support-reply", cache=False)
  ```
</CodeGroup>

<ParamField query="name" type="string" required>
  The name of the prompt to fetch.
</ParamField>

<ParamField query="version" type="number">
  Fetch a specific version number. Mutually exclusive with `label`.
</ParamField>

<ParamField query="label" type="string">
  Fetch the version currently associated with this label (e.g. `"production"`, `"staging"`, `"canary"`). Mutually exclusive with `version`.
</ParamField>

<ParamField query="cache" type="boolean" default="true">
  When `true`, the result is cached locally for 5 minutes. Set to `false` to bypass the cache for this call.
</ParamField>

<ParamField query="fallback" type="object">
  A local fallback to use if the fetch fails and there's no usable cache. Shape: `{ type?: "text" | "chat", prompt: <content, same shape as a real prompt's `prompt` field>, config?: object }` (TypeScript) or the equivalent `dict` with the same keys (Python). See [Handling fetch failures](#handling-fetch-failures) below.
</ParamField>

## Handling fetch failures

If `get()` can't reach the prompt library — a network blip, an outage, whatever — it degrades gracefully instead of throwing immediately:

1. If an earlier successful fetch for the same prompt (same name, version, and label) is still in the local cache, that cached copy is served even if it's past the 5-minute TTL.
2. If there's no cached copy at all, and you passed a `fallback` option, the client synthesizes a prompt-like result from it that you can still call `compile()` on.
3. If neither a stale cache nor a fallback is available, `get()` raises the underlying fetch error — the original behavior.

The returned prompt is flagged with `isFallback: true` (TypeScript) or `is_fallback: True` (Python) whenever it came from the `fallback` option, so you can detect degraded mode and log or alert on it. A stale-cache hit is not flagged — from the caller's perspective it's just a slightly older copy of the real prompt.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const prompt = await prompts.get("support-reply", {
    label: "production",
    fallback: {
      type: "text",
      prompt: "You are a helpful support agent. Answer the customer's question concisely.",
      config: { model: "gpt-4o", temperature: 0.3 },
    },
  });

  if (prompt.isFallback) {
    console.warn("Serving fallback prompt — prompt library unreachable");
  }

  const text = prompts.compile(prompt, { customer_name: "Alex" });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  prompt = prompts.get(
      "support-reply",
      label="production",
      fallback={
          "type": "text",
          "prompt": "You are a helpful support agent. Answer the customer's question concisely.",
          "config": {"model": "gpt-4o", "temperature": 0.3},
      },
  )

  if prompt.get("is_fallback"):
      print("Serving fallback prompt — prompt library unreachable")

  text = prompts.compile(prompt, {"customer_name": "Alex"})
  ```
</CodeGroup>

<Note>
  Passing `cache: false` (TypeScript) or `cache=False` (Python) disables both caching and stale-cache serving for that call. A fetch failure with caching disabled goes straight to the `fallback` option (if provided) or raises — it will not return a previously cached value from an earlier call, even if one exists.
</Note>

## Compiling prompts with variables

`compile(prompt, variables, placeholders?)` substitutes `{{variable}}` placeholders in a prompt's text with the values you provide, and — for `chat`-type prompts — splices in any message placeholders.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const prompt = await prompts.get("support-reply", { label: "production" });

  const text = prompts.compile(prompt, {
    customer_name: "Alex",
    order_id: "ORD-7821",
    product: "wireless headphones",
  });

  // Use the compiled text in an LLM call
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [
      { role: "system", content: text },
      { role: "user", content: userMessage },
    ],
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  prompt = prompts.get("support-reply", label="production")

  text = prompts.compile(prompt, {
      "customer_name": "Alex",
      "order_id": "ORD-7821",
      "product": "wireless headphones",
  })

  # Use the compiled text in an LLM call
  response = openai_client.chat.completions.create(
      model="gpt-4o",
      messages=[
          {"role": "system", "content": text},
          {"role": "user", "content": user_message},
      ],
  )
  ```
</CodeGroup>

For `chat`-type prompts, `compile` returns the resolved message array directly — no parsing required:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const messages = prompts.compile(chatPrompt, variables);
  // messages: { role: string; content: string }[]
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  messages = prompts.compile(chat_prompt, variables)
  # messages: list[{"role": str, "content": str}]
  ```
</CodeGroup>

<ParamField body="prompt" type="object" required>
  The prompt returned from `get()` (including a fallback result — see [Handling fetch failures](#handling-fetch-failures) above).
</ParamField>

<ParamField body="variables" type="object">
  A map of `{{variable}}` names to the values that replace them. Applied to every message's content in a `chat` prompt, including messages inserted via `placeholders`.
</ParamField>

<ParamField body="placeholders" type="object">
  Only relevant for `chat`-type prompts. Maps a placeholder name to the array of `{ role, content }` messages to splice into that slot. See [Composing messages with placeholders](#composing-messages-with-placeholders) below.
</ParamField>

### Composing messages with placeholders

A `chat`-type prompt's message array can include a placeholder — a named slot, distinct from a regular `{ role, content }` message — for content you only know at call time, such as conversation history or retrieved context. Author the placeholder once when you `create()` the prompt, then fill it in on every `compile()` call.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // The prompt's message array, as authored via create() or in the dashboard:
  // [
  //   { role: "system", content: "You are {{persona}}, a helpful assistant." },
  //   { type: "placeholder", name: "history" },
  //   { role: "user", content: "{{question}}" },
  // ]

  const prompt = await prompts.get("chat-assistant", { label: "production" });

  const messages = prompts.compile(
    prompt,
    { persona: "a support agent", question: "Where's my order?" },
    {
      history: [
        { role: "user", content: "Hi, I placed an order yesterday." },
        { role: "assistant", content: "Happy to help — what's the order number?" },
      ],
    },
  );

  // messages: [
  //   { role: "system", content: "You are a support agent, a helpful assistant." },
  //   { role: "user", content: "Hi, I placed an order yesterday." },
  //   { role: "assistant", content: "Happy to help — what's the order number?" },
  //   { role: "user", content: "Where's my order?" },
  // ]

  const response = await openai.chat.completions.create({ model: "gpt-4o", messages });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # The prompt's message array, as authored via create() or in the dashboard:
  # [
  #   {"role": "system", "content": "You are {{persona}}, a helpful assistant."},
  #   {"type": "placeholder", "name": "history"},
  #   {"role": "user", "content": "{{question}}"},
  # ]

  prompt = prompts.get("chat-assistant", label="production")

  messages = prompts.compile(
      prompt,
      {"persona": "a support agent", "question": "Where's my order?"},
      {
          "history": [
              {"role": "user", "content": "Hi, I placed an order yesterday."},
              {"role": "assistant", "content": "Happy to help — what's the order number?"},
          ],
      },
  )

  # messages: [
  #   {"role": "system", "content": "You are a support agent, a helpful assistant."},
  #   {"role": "user", "content": "Hi, I placed an order yesterday."},
  #   {"role": "assistant", "content": "Happy to help — what's the order number?"},
  #   {"role": "user", "content": "Where's my order?"},
  # ]

  response = openai_client.chat.completions.create(model="gpt-4o", messages=messages)
  ```
</CodeGroup>

If you don't supply a value for a placeholder, its slot is simply dropped from the output — no empty message is inserted and no error is thrown. `{{variable}}` substitution still applies to every message, whether it was authored directly in the prompt or supplied via `placeholders`.

## Listing prompts

`list(nameFilter?)` (TypeScript) / `list(name_filter=None)` (Python) returns all prompts in your project. Optionally filter by a name substring.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // All prompts
  const allPrompts = await prompts.list();

  // Filter by name
  const supportPrompts = await prompts.list("support");
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # All prompts
  all_prompts = prompts.list()

  # Filter by name
  support_prompts = prompts.list(name_filter="support")
  ```
</CodeGroup>

## Creating a new prompt version

`create(params)` (TypeScript) / `create(...)` (Python) creates a new prompt version. Each call creates a new version number — existing versions are never modified.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const newPrompt = await prompts.create({
    name: "support-reply",
    type: "text",
    prompt: "You are a helpful support agent for {{company_name}}. " +
            "Reply to the customer's message about their order {{order_id}} " +
            "in a friendly, concise tone.",
    config: {
      model: "gpt-4o",
      temperature: 0.3,
      max_tokens: 500,
    },
    labels: ["staging"],
    tags: ["support", "v2"],
    commitMessage: "Add company name variable for white-label support",
  });

  console.log(`Created version ${newPrompt.version}`);
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  new_prompt = prompts.create(
      name="support-reply",
      prompt_type="text",
      prompt=(
          "You are a helpful support agent for {{company_name}}. "
          "Reply to the customer's message about their order {{order_id}} "
          "in a friendly, concise tone."
      ),
      config={
          "model": "gpt-4o",
          "temperature": 0.3,
          "max_tokens": 500,
      },
      labels=["staging"],
      tags=["support", "v2"],
      commit_message="Add company name variable for white-label support",
  )

  print(f"Created version {new_prompt['version']}")
  ```
</CodeGroup>

<ParamField body="name" type="string" required>
  The prompt name. Must match the name of an existing prompt to add a version, or creates a new prompt if it does not exist.
</ParamField>

<ParamField body="type" type="string" required>
  `"text"` for a plain string prompt with `{{variable}}` placeholders, or `"chat"` for an array of messages. (Python's keyword argument is named `prompt_type` since `type` is a reserved builtin.)
</ParamField>

<ParamField body="prompt" type="any" required>
  The prompt content. A string for `text` type; an array of message objects for `chat` type.
</ParamField>

<ParamField body="config" type="object">
  Suggested model configuration (model, temperature, max\_tokens). Stored with the prompt for reference but not enforced by the SDK.
</ParamField>

<ParamField body="labels" type="string[]">
  Labels to apply to this version immediately on creation (e.g. `["staging"]`).
</ParamField>

<ParamField body="tags" type="string[]">
  Searchable tags for organizing prompts in the dashboard.
</ParamField>

<ParamField body="commitMessage" type="string">
  Human-readable description of what changed in this version. Appears in the version history view. (Python: `commit_message`.)
</ParamField>

## Updating labels

`updateLabels(name, version, labels)` (TypeScript) / `update_labels(name, version, labels)` (Python) replaces all labels on a specific version. Use this to promote a version from staging to production.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Promote version 4 to production
  await prompts.updateLabels("support-reply", 4, ["production"]);

  // Move staging label to a newer version
  await prompts.updateLabels("support-reply", 5, ["staging"]);
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Promote version 4 to production
  prompts.update_labels("support-reply", 4, ["production"])

  # Move staging label to a newer version
  prompts.update_labels("support-reply", 5, ["staging"])
  ```
</CodeGroup>

<Note>
  Labels are not exclusive by default. The same label (e.g. `"production"`) can exist on multiple versions simultaneously. If you want a single canonical production version, update the old version's labels to remove `"production"` before applying it to the new version.
</Note>

## Clearing the cache

The local cache has a 5-minute TTL. Call `clearCache()` (TypeScript) / `clear_cache()` (Python) to invalidate it manually — useful after creating a new version or updating labels in the same process.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Clear cache for a specific prompt
  prompts.clearCache("support-reply");

  // Clear all cached prompts
  prompts.clearCache();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Clear cache for a specific prompt
  prompts.clear_cache("support-reply")

  # Clear all cached prompts
  prompts.clear_cache()
  ```
</CodeGroup>

## Managing prompts from an AI assistant

Zespan's hosted MCP server exposes prompt management as tools an AI assistant — Claude Desktop, Cursor, or any MCP client — can call directly: listing prompts, fetching a prompt's resolved content and detected `{{variables}}`, creating a new version, updating tags, and setting or moving a label (e.g. promoting a version to `production`).

See the [Zespan MCP guide](/guides/zespan-mcp#prompt-management-tools) for the full tool list and client setup.

## Complete workflow

This example shows the full lifecycle: create a prompt, promote it to production, fetch it by label, compile it with variables, and use it in an LLM call.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan, PromptClient } from "@zespan/sdk";
  import OpenAI from "openai";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });
  const openai = zespan.wrapOpenAI(new OpenAI());
  const promptClient = new PromptClient(zespan.getClient());

  // 1. Create a new prompt version and put it in staging
  const version = await promptClient.create({
    name: "support-reply",
    type: "text",
    prompt:
      "You are a support agent for {{company_name}}. " +
      "Help the customer with their question about order {{order_id}}. " +
      "Be concise and friendly.",
    labels: ["staging"],
    commitMessage: "Initial support reply template",
  });

  // 2. After testing, promote to production
  await promptClient.updateLabels("support-reply", version.version, ["production"]);
  promptClient.clearCache("support-reply");

  // 3. At request time: fetch by label, compile, and call the LLM
  async function handleSupportRequest(
    customerId: string,
    orderId: string,
    userMessage: string
  ): Promise<string> {
    const prompt = await promptClient.get("support-reply", { label: "production" });

    const systemPrompt = promptClient.compile(prompt, {
      company_name: "Acme Corp",
      order_id: orderId,
    });

    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [
        { role: "system", content: systemPrompt },
        { role: "user", content: userMessage },
      ],
    });

    return response.choices[0].message.content ?? "";
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan
  from zespan import get_client, PromptClient

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])
  zespan.patch_openai()
  prompt_client = PromptClient(get_client())

  # 1. Create a new prompt version and put it in staging
  version = prompt_client.create(
      name="support-reply",
      prompt_type="text",
      prompt=(
          "You are a support agent for {{company_name}}. "
          "Help the customer with their question about order {{order_id}}. "
          "Be concise and friendly."
      ),
      labels=["staging"],
      commit_message="Initial support reply template",
  )

  # 2. After testing, promote to production
  prompt_client.update_labels("support-reply", version["version"], ["production"])
  prompt_client.clear_cache("support-reply")

  # 3. At request time: fetch by label, compile, and call the LLM
  import openai
  openai_client = openai.OpenAI()

  def handle_support_request(customer_id: str, order_id: str, user_message: str) -> str:
      prompt = prompt_client.get("support-reply", label="production")

      system_prompt = prompt_client.compile(prompt, {
          "company_name": "Acme Corp",
          "order_id": order_id,
      })

      response = openai_client.chat.completions.create(
          model="gpt-4o",
          messages=[
              {"role": "system", "content": system_prompt},
              {"role": "user", "content": user_message},
          ],
      )

      return response.choices[0].message.content or ""
  ```
</CodeGroup>

<Tip>
  The 5-minute cache TTL means your application automatically picks up prompt updates without restarting. For immediate rollouts, call `clearCache("prompt-name")` / `clear_cache("prompt-name")` in your deployment pipeline after updating labels.
</Tip>
