> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ellaworks.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ellaworks.ai/_mcp/server.

# Using the Prompt Registry

The Ellaworks Prompt Registry works like a package manager for AI prompts. Instead of embedding prompt strings in your application code, you author them in the registry with semantic versioning, then pull the compiled result at runtime via API.

## Prompts as Versioned Artifacts

Every prompt in a registry is a versioned artifact with:

* **Semantic versioning** (e.g. `1.0.0`, `1.1.0`, `2.0.0`) for safe rollbacks and predictable upgrades
* **Model-specific variants** — the same prompt name can have different content optimized for `gpt-4o`, `claude-3`, or a `generic` fallback
* **Metadata frontmatter** — description, tags, and configuration stored alongside the prompt content
* **Content hashing** — every version is hashed for integrity verification and drift detection

### Promptlets

Promptlets are reusable prompt components that can be composed into larger prompts. Think of them like shared utility functions — define a tone-of-voice section, a compliance disclaimer, or a tool-usage preamble once, then reference it across multiple prompts. Imports are expanded when a prompt is compiled and published, so each published prompt version is a self-contained snapshot. When you publish a new promptlet version, a consuming prompt picks it up the next time that prompt is compiled and published (as long as the new version satisfies its import range).

## Pulling Prompts at Runtime

Fetch prompts over HTTP from `https://app.ellaworks.ai/api`. Private registries require an API key, sent as `X-API-Key` or `Authorization: Bearer` (same `ela_…` key; see [API Keys](/api-keys)). Public registries can be read without a key.

The simplest option for runtime use is the **compile** endpoint. `version` accepts an exact version (`1.2.0`), a semver range (`^1.0`), or `latest`, and defaults to the latest published version; `model` and `environment` select model and environment branches:

```bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.ellaworks.ai/api/prompts/your-org/my-registry/customer-support/compile?version=%5E1.0&model=gpt-4o&environment=prod"
```

```bash
# Send Accept: text/plain to get just the compiled text
curl -H "Authorization: Bearer YOUR_API_KEY" -H "Accept: text/plain" \
  "https://app.ellaworks.ai/api/prompts/your-org/my-registry/customer-support/compile?model=gpt-4o"
```

The JSON response contains `compiled`, `metadata`, and the resolved `version` and `model`.

Two other read endpoints return the stored artifact without applying an environment:

| Endpoint                                                                                                                                | Returns                                                                                                                                                                           |
| --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/prompts/{org}/{registry}/{prompt}/{version}?model=gpt-4o`                                                                     | The full published artifact as a JSON download: `prompt`, `version`, `model`, `body`, `compiled`, `hash`, `components`, `metadata`. `version` may be exact, a range, or `latest`. |
| `GET /api/registries/artifact-versions/content?registryRef=your-org/my-registry&promptName=customer-support&version=1.0.0&model=gpt-4o` | `body`, `compiled`, and `metadata` for one exact version. All four parameters are required.                                                                                       |

**Response fields:**

| Field      | Description                                                                                                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `body`     | The raw prompt source exactly as authored (frontmatter included)                                                                                                                |
| `compiled` | Promptlets expanded and model preprocessors applied (plus environment preprocessors when you pass `environment` to the compile endpoint) — **template variables are preserved** |
| `metadata` | Artifact metadata (description, resolved components, hash, etc.)                                                                                                                |

> **Note**
>
> `compiled` begins with a `<!-- prm-metadata … prm-metadata -->` comment block carrying the same metadata. Strip it before sending the prompt to a model (the Python example below shows how). Prompts authored as a JSON array of role-tagged messages (e.g. `system`, `user`) compile to a JSON-encoded messages array rather than plain text.

If a model-specific variant isn't published for the requested `model`, the `generic` variant is returned.

### What compilation resolves vs what it leaves for you

Compilation happens in stages. Ellaworks handles structural resolution (at publish time, then at deploy or fetch time), and your application handles context substitution at runtime.

**Stage 1 — Ellaworks resolves when the prompt is compiled and published:**

* **Promptlet imports** — `{{ import "tone-of-voice@^1.0" }}` is replaced with the promptlet's content. Use `{{ import "org/registry/name@^1.0" }}` to import from another registry. The version range can also be declared in the prompt's frontmatter instead of inline.
* **Model preprocessors** — `#ECP-IF-MODEL gpt-4o` blocks are kept only for that model; `#ECP-NOT-MODEL gpt-4o` blocks are kept for every model *except* that one. Each model variant is compiled separately.

**Stage 2 — Ellaworks resolves once the target environment is known (at deploy, or via the compile endpoint's `environment` parameter):**

* **Environment preprocessors** — `#ECP-IF-ENV prod` / `#ECP-NOT-ENV prod` blocks are included or excluded based on the target environment. Until then they are preserved in `compiled`.

Every directive is written as the header of a markdown code fence; the fence itself delimits the block (there is no closing directive). The legacy `#PRM-` prefix is still accepted.

**Stage 3 — Ellaworks resolves once variable values are known (at deploy or per request):**

* **Variable preprocessors** — `#ECP-IF-VAR store.size == "LARGE"` blocks are included or excluded by comparing a template variable to a value (`==` / `!=`). Imports inside the block are still expanded at compile time; the block's inclusion is decided later, once the variable's value is known. If a variable is still unknown at deploy time, the directive is preserved and resolved per request.

**Stage 4 — Your application substitutes at runtime:**

* **Template variables** — `{{variable_name}}` placeholders remain in the compiled output for you to fill in with your own runtime context

This separation is intentional. Ellaworks manages the structural composition of your prompt (which components, which model variant, which environment branch), while your application injects the dynamic context that changes per request (user names, session data, query parameters).

### Template Variable Syntax

Template variables use Mustache double braces and support dot notation for nested access. Mustache sections (`{{#name}}…{{/name}}`) render their contents only when the variable is truthy, and `{{env.variables.NAME}}` references a variable defined on the target environment:

```text
Hello {{user.name}}, welcome to {{company_name}}.

Your account ID is {{session.account_id}}.

{{#premium}}
As a premium member, you have access to priority support.
{{/premium}}
```

### Example: What the compiled output looks like

Suppose you author this prompt in the registry:

````text
{{ import "compliance-disclaimer@^1.0" }}

You are a support agent for {{company_name}}.
The caller's name is {{caller.name}} and their account is {{caller.account_id}}.

```#ECP-IF-MODEL gpt-4o
Use markdown formatting in your responses.
```

```#ECP-NOT-MODEL gpt-4o
Use XML tags to structure your responses.
```

Always follow the guidelines above.
````

When you fetch it with `model=gpt-4o`, the `compiled` field returns (after the metadata comment):

```text
[Compliance disclaimer content inlined here by Ellaworks]

You are a support agent for {{company_name}}.
The caller's name is {{caller.name}} and their account is {{caller.account_id}}.

Use markdown formatting in your responses.

Always follow the guidelines above.
```

Notice: the promptlet import and model preprocessor are resolved, but `{{company_name}}`, `{{caller.name}}`, and `{{caller.account_id}}` remain as placeholders for your application to fill in.

#### Variable directives (`#ECP-IF-VAR`)

Use `#ECP-IF-VAR` to switch which content — including which promptlet import — ships, based on a template variable's value. Like the other directives, each block is a directive-headed markdown code fence; the fence itself is the delimiter:

````text
```#ECP-IF-VAR store.size == "LARGE"
{{ import "large-store-playbook@^1.0" }}
```
```#ECP-IF-VAR store.size == "SMALL"
{{ import "small-store-playbook@^1.0" }}
```
````

The comparison supports `==` and `!=` against a string value. The import inside each block is expanded at compile time, but which block is kept is decided later, when the value of `store.size` is known — at deploy time (from agent/environment variables) or per request. If the variable is not yet known at deploy time, the directive is preserved in the compiled output and resolved on each request.

A few semantics worth knowing:

* **Variable namespace.** IF-VAR conditions resolve against the same variables as `{{ ... }}` substitution. Agent/request variables are referenced at the top level (`#ECP-IF-VAR store.size == "LARGE"`); environment variables are referenced under `env.variables.*` (`#ECP-IF-VAR env.variables.TIER == "gold"`), mirroring `{{env.variables.TIER}}`.
* **Fails closed at the final stage.** At the last stage before the prompt reaches the model (the runtime proxy), any IF-VAR directive that still can't be resolved — the variable was never supplied, or its value isn't a comparable primitive — is **removed** (its block is excluded) and a warning is logged. The `#ECP-IF-VAR` syntax is never sent to the model. So a missing variable **excludes** the gated content rather than including it. (At earlier stages the directive is preserved so a later stage can resolve it.)
* **Comparable values.** Conditions compare against a string literal, so string, number, and boolean values are matched by their string form (`3` matches `"3"`, `true` matches `"true"`). A variable that resolves to an object, array, or `null` is treated as unresolvable (not compared), and follows the fail-closed behavior above.

### Substituting Variables in Your Application

After fetching the compiled prompt, substitute the template variables with your runtime context before sending it to an LLM:

```python
import re
import requests

ELLAWORKS_API_KEY = "ela_your-api-key"
BASE_URL = "https://app.ellaworks.ai/api"
METADATA_BLOCK = re.compile(r"<!--\s*prm-metadata\s*\n[\s\S]*?\nprm-metadata\s*-->\s*")


def get_compiled_prompt(registry_ref, prompt_name, version="latest", model="generic", environment=None):
    """Fetch a compiled prompt from the Ellaworks registry."""
    params = {"version": version, "model": model}
    if environment:
        params["environment"] = environment
    response = requests.get(
        f"{BASE_URL}/prompts/{registry_ref}/{prompt_name}/compile",
        params=params,
        # Or: headers={"Authorization": f"Bearer {ELLAWORKS_API_KEY}"}
        headers={"X-API-Key": ELLAWORKS_API_KEY},
    )
    response.raise_for_status()
    # Drop the embedded metadata comment before using the prompt
    return METADATA_BLOCK.sub("", response.json()["compiled"]).strip()


def substitute_variables(template: str, context: dict) -> str:
    """Replace {{variable}} placeholders with values from a context dict.

    Supports dot notation: {{user.name}} resolves to context["user"]["name"].
    Unmatched variables are left as-is.
    """
    def resolve(match):
        key = match.group(1).strip()
        value = context
        for part in key.split("."):
            if isinstance(value, dict) and part in value:
                value = value[part]
            else:
                return match.group(0)  # leave unmatched variables intact
        return str(value)

    return re.sub(r"\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}", resolve, template)


# 1. Fetch the compiled prompt (promptlets and preprocessors resolved)
compiled = get_compiled_prompt(
    registry_ref="acme/production",
    prompt_name="customer-support",
    version="^2.1",
    model="gpt-4o",
    environment="prod",
)

# 2. Substitute runtime context (template variables resolved)
system_prompt = substitute_variables(compiled, {
    "company_name": "Acme Corp",
    "caller": {
        "name": "Jane Doe",
        "account_id": "ACC-12345",
    },
})

# 3. Use the final prompt with your LLM
print(system_prompt)
```

> **Note**
>
> When deploying agents through Ellaworks (e.g. to Vapi), environment preprocessors and template variable substitution are handled automatically using the target environment and the variables defined on your agent and environment. The manual substitution shown above is only needed when you pull prompts directly via the API for use in your own application code.

### When to use `body` vs `compiled`

|                        | Raw (`body`)                                             | Compiled (`compiled`)                                                                             |
| ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Promptlets**         | Import references like `{{ import "..." }}` appear as-is | Promptlet content inlined                                                                         |
| **Model blocks**       | `#ECP-IF-MODEL` directives appear as-is                  | Correct model branch selected                                                                     |
| **Environment blocks** | `#ECP-IF-ENV` directives appear as-is                    | Resolved only when an `environment` is supplied (compile endpoint or deploy); otherwise preserved |
| **Template variables** | `{{variable}}` placeholders appear as-is                 | `{{variable}}` placeholders appear as-is (for you to substitute)                                  |
| **Use case**           | Debugging, auditing, viewing template syntax             | Production runtime — substitute variables and send to LLM                                         |

This pattern keeps your application code free of prompt strings. When you publish a new prompt version, an application that requests `latest` or a matching range (e.g. `^2.1`) picks up the change without a code deploy.

## Versioning & Compilation Workflow

The typical lifecycle for a prompt is:

#### Draft

Author or edit a prompt or promptlet under **Prompts** or **Promptlets** in the [Ellaworks app](https://app.ellaworks.ai/auth/login). Drafts are saved automatically and are not visible to consumers of the registry.

#### Compile & Preview

The publish dialog shows a compilation preview so you can verify that promptlet imports resolve correctly and that model and environment preprocessors produce the expected result. Template variables are left as placeholders.

#### Publish

Publish the draft as a new version (e.g. `1.0.0 → 1.1.0`). In the publish dialog, choose the **Version**, the **Model** (or **All Variants**), the target registry, and an optional **Commit message** (Ellaworks can generate one), which appears in the prompt's version history. Only organization owners and admins can publish. Published versions are immutable — a version and model pair can't be republished with different content.

#### Deploy or Pull

Reference the published version in an agent deployment, or pull it at runtime via the API.

### Model-Specific Variants

The same prompt name can have variants optimized for different models. Publishing with **All Variants** creates a `generic` variant plus one variant for each model named in the prompt's `#ECP-IF-MODEL` / `#ECP-NOT-MODEL` directives. When you request a prompt with `model=gpt-4o`, Ellaworks returns the GPT-4o-specific variant if one exists, otherwise falls back to the `generic` version.

This is useful when different models need different formatting, token budgets, or instruction styles. For example, Claude models may benefit from XML-structured prompts while GPT models may work better with markdown.

## Structuring Your Registries

Registries are the primary organizational boundary in Ellaworks. How you partition them determines who can collaborate on which prompts, which artifacts version independently, and where access control boundaries fall. Below are three proven patterns — pick the one that matches your situation, or combine them.

### Per-Client Registries

If you are a service provider or agency managing AI solutions for multiple clients, create a **private registry per client** alongside a **shared registry** for common promptlets.

| Registry               | Visibility | Contents                                                                           |
| ---------------------- | ---------- | ---------------------------------------------------------------------------------- |
| `agency/shared`        | Private    | Reusable promptlets — compliance disclaimers, tone-of-voice, escalation procedures |
| `agency/client-acme`   | Private    | Acme Corp's prompt artifacts (support agent, booking agent, etc.)                  |
| `agency/client-globex` | Private    | Globex's prompt artifacts                                                          |

Client-specific prompts import shared promptlets by their full `org/registry/name` reference, so you author common logic once:

```text
{{ import "agency/shared/compliance-disclaimer@^1.0" }}
{{ import "agency/shared/escalation-procedure@^2.0" }}

You are a support agent for {{company_name}}.
Handle all inquiries following the compliance and escalation guidelines above.
```

The shared promptlets are inlined when each client prompt is published, so fetching from a client's registry returns the fully composed prompt:

```python
# Pull the same prompt structure, customized per client
acme_prompt = get_compiled_prompt(
    registry_ref="agency/client-acme",
    prompt_name="support-agent",
    version="1.2.0",
    model="gpt-4o",
)

globex_prompt = get_compiled_prompt(
    registry_ref="agency/client-globex",
    prompt_name="support-agent",
    version="1.0.0",
    model="gpt-4o",
)
```

Pair each client registry with a dedicated [environment](/environment-setup) for that client's provider credentials and variables (company name, support URL, escalation contacts). Use [fleets](/environment-setup#create-a-fleet-optional) to group each client's agents for bulk operations.

> **Note**
>
> The Service Provider plan includes unlimited private registries, designed for exactly this scaling pattern. See [Plans](/creating-an-account#plans) for details.

### Per-Domain Registries

Larger organizations with multiple teams building AI products benefit from a **registry per domain or business unit**. Each team owns its registry and release cadence while sharing common promptlets through a cross-team registry.

| Registry                      | Owner         | Contents                                                  |
| ----------------------------- | ------------- | --------------------------------------------------------- |
| `enterprise/shared`           | Platform team | Brand voice, compliance language, formatting standards    |
| `enterprise/customer-support` | CX team       | Support agent prompts, triage logic, CSAT follow-ups      |
| `enterprise/sales-enablement` | Sales ops     | Lead qualification, objection handling, demo prep prompts |
| `enterprise/internal-tools`   | Platform team | Code review agents, incident responders, onboarding bots  |

The CX team can iterate on support prompts at their own pace without affecting sales prompts, and vice versa. Shared promptlets keep brand voice consistent across every domain:

```text
{{ import "enterprise/shared/brand-voice@^1.0" }}
{{ import "enterprise/shared/data-handling-policy@^3.0" }}

You are a customer support agent for {{company_name}}.
Always follow the brand voice and data handling policy above.

{{#premium}}
This caller is a premium customer. Prioritize their request.
{{/premium}}
```

This pattern works well when different teams need different approval workflows — the CX team can ship daily while the compliance-sensitive internal-tools registry requires review before every publish.

### Shared Component Registries

Regardless of whether you structure by client or by domain, dedicate a registry to **reusable promptlets only**. Think of it as a shared library that other registries depend on.

Common promptlets to centralize:

* **Compliance disclaimers** — legal language that must appear in every customer-facing prompt
* **Tone-of-voice guides** — brand personality instructions
* **Tool-usage preambles** — instructions for how the model should invoke tools
* **Error-handling patterns** — how to respond when something goes wrong
* **Output format standards** — JSON schemas, markdown formatting rules

When you publish a new promptlet version in the shared registry, every prompt that imports it with a compatible semver range (e.g. `@^1.0`) picks it up the next time that prompt is compiled and published — no need to edit the import references. To see which prompts consume a promptlet, open its details and check **Dependent Components**.

```text
# In org/shared — promptlet: "output-format" v1.0.0
Always respond in valid JSON with the following structure:
- "answer": your response text
- "confidence": a number between 0 and 1
- "sources": an array of source references
```

```text
# In org/customer-support — prompt: "support-agent"
{{ import "org/shared/compliance-disclaimer@^1.0" }}
{{ import "org/shared/output-format@^1.0" }}

You are a support agent for {{company_name}}.
Route the caller to the correct department based on their issue.
```

```text
# In org/sales — prompt: "lead-qualifier"
{{ import "org/shared/output-format@^1.0" }}

You are a lead qualification assistant.
Evaluate the prospect based on: {{qualification_criteria}}.
```

Both prompts share the same output format. When you publish `output-format` `1.1.0`, both pick it up on their next publish without any change to their source.

### Registries vs Environments

> **Warning**
>
> Do not create separate registries for `dev`, `staging`, and `prod`. Use [environments](/environment-setup) and `#ECP-IF-ENV` preprocessors instead. Registries define organizational boundaries (who owns which prompts); environments define deployment boundaries (which credentials and variables apply at runtime). Mixing the two leads to duplicated prompts and version drift.

## Storing OpenClaw Souls

The registry is well suited for storing [OpenClaw](https://github.com/opensouls/community) soul definitions. A soul's personality, drives, and behavioral instructions are essentially structured prompts — they benefit from the same versioning, model-specific variants, and runtime resolution that Ellaworks provides.

### Why store souls in Ellaworks?

* **Version control** — roll back to a previous soul version if a change causes regressions
* **Model-specific tuning** — maintain separate soul definitions optimized for different LLM backends
* **Runtime resolution** — pull the latest soul at startup without redeploying your application
* **Collaboration** — your team can iterate on soul definitions in the Ellaworks editor with compilation previews

### Example: OpenClaw wrapper with Ellaworks

```python
import re
import requests

ELLAWORKS_API_KEY = "ela_your-api-key"
ELLAWORKS_BASE_URL = "https://app.ellaworks.ai/api"


def fetch_soul(registry_ref: str, soul_name: str, version: str = "latest", model: str = "generic") -> str:
    """Fetch a compiled soul definition from the Ellaworks registry as plain text."""
    response = requests.get(
        f"{ELLAWORKS_BASE_URL}/prompts/{registry_ref}/{soul_name}/compile",
        params={"version": version, "model": model},
        headers={
            # Or: "Authorization": f"Bearer {ELLAWORKS_API_KEY}"
            "X-API-Key": ELLAWORKS_API_KEY,
            "Accept": "text/plain",
        },
    )
    response.raise_for_status()
    # Drop the embedded metadata comment
    return re.sub(r"<!--\s*prm-metadata\s*\n[\s\S]*?\nprm-metadata\s*-->\s*", "", response.text).strip()


class EllaworksSoul:
    """A simple OpenClaw soul wrapper that sources its definition from Ellaworks."""

    def __init__(self, registry_ref: str, soul_name: str, version: str = "latest", model: str = "generic"):
        self.registry_ref = registry_ref
        self.soul_name = soul_name
        self.version = version
        self.model = model
        self.definition = None

    def load(self):
        """Load the soul definition from the registry."""
        self.definition = fetch_soul(
            self.registry_ref,
            self.soul_name,
            self.version,
            self.model,
        )
        return self

    def get_system_prompt(self) -> str:
        """Return the compiled soul definition as a system prompt."""
        if not self.definition:
            self.load()
        return self.definition


# Usage: load a soul optimized for GPT-4o
soul = EllaworksSoul(
    registry_ref="acme/souls",
    soul_name="helpful-assistant",
    version="2.0.0",
    model="gpt-4o",
).load()

# Use with your OpenClaw agent
system_prompt = soul.get_system_prompt()
print(f"Loaded soul ({len(system_prompt)} chars)")

# Switch to a Claude-optimized variant — same soul, different model tuning
claude_soul = EllaworksSoul(
    registry_ref="acme/souls",
    soul_name="helpful-assistant",
    version="2.0.0",
    model="claude-3",
).load()
```

> **Note**
>
> Store your soul definitions under a dedicated registry (e.g. `your-org/souls`) to keep them organized separately from your agent prompts and promptlets.

## Next steps

#### [Deploying Agents](/deploying-agents)

Deploy a voice agent with tools to VAPI

#### [API Reference](/api-reference)

Explore the full API