Skip to navigation

Using the Prompt Registry

Version, compile, and pull prompts at runtime
View as Markdown

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). 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:

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"
# 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:

EndpointReturns
GET /api/prompts/{org}/{registry}/{prompt}/{version}?model=gpt-4oThe 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-4obody, compiled, and metadata for one exact version. All four parameters are required.

Response fields:

FieldDescription
bodyThe raw prompt source exactly as authored (frontmatter included)
compiledPromptlets expanded and model preprocessors applied (plus environment preprocessors when you pass environment to the compile endpoint) — template variables are preserved
metadataArtifact metadata (description, resolved components, hash, etc.)

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:

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:

{{ 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):

[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:

```#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:

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)

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)
PromptletsImport references like {{ import "..." }} appear as-isPromptlet content inlined
Model blocks#ECP-IF-MODEL directives appear as-isCorrect model branch selected
Environment blocks#ECP-IF-ENV directives appear as-isResolved 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 caseDebugging, auditing, viewing template syntaxProduction 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:

1

Draft

Author or edit a prompt or promptlet under Prompts or Promptlets in the Ellaworks app. Drafts are saved automatically and are not visible to consumers of the registry.

2

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.

3

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.

4

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.

RegistryVisibilityContents
agency/sharedPrivateReusable promptlets — compliance disclaimers, tone-of-voice, escalation procedures
agency/client-acmePrivateAcme Corp’s prompt artifacts (support agent, booking agent, etc.)
agency/client-globexPrivateGlobex’s prompt artifacts

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

{{ 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:

# 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 for that client’s provider credentials and variables (company name, support URL, escalation contacts). Use fleets to group each client’s agents for bulk operations.

The Service Provider plan includes unlimited private registries, designed for exactly this scaling pattern. See 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.

RegistryOwnerContents
enterprise/sharedPlatform teamBrand voice, compliance language, formatting standards
enterprise/customer-supportCX teamSupport agent prompts, triage logic, CSAT follow-ups
enterprise/sales-enablementSales opsLead qualification, objection handling, demo prep prompts
enterprise/internal-toolsPlatform teamCode 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:

{{ 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.

# 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
# 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.
# 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

Do not create separate registries for dev, staging, and prod. Use environments 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 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

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()

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