Appearance
Best Practices
The SDK is small and well-typed; using it safely is more about how you wire it into your application than about the SDK itself. These are the patterns we've seen work in production.
Credentials
Never commit keys to source
Use .env (gitignored), environment variables, or a secrets manager. The SDK reads plain strings — it doesn't care where they came from.
One key per service
If service A leaks its key, you revoke and reissue without touching service B. Keys are cheap; rotation should be local.
Name keys for traceability
checkout-api-prod beats key3. The name appears in audit logs and the API Keys page.
Rotate periodically
Even without leaks. Quarterly is sensible. Tie it to your existing secrets-rotation cadence.
Construct once, use many
The constructor is cheap but adds zero value when called multiple times. Single instance per process, injected as a dependency:
python
# app/deps.py
import os
from dotenv import load_dotenv
from elsai_prompts.prompt_manager import PromptManager
load_dotenv()
pm = PromptManager(
api_key=os.environ["ELSAI_API_KEY"],
project_id=os.environ["ELSAI_PROJECT_ID"],
environment=os.environ["ELSAI_ENVIRONMENT"],
)python
# Anywhere else:
from app.deps import pm
prompt = pm.get_active_prompt_version("...")Cache fetches
The SDK does no caching. Every get_active_prompt_version call is a network round trip. That's deliberate — caching policy is application-specific.
Simple in-memory cache (good default):
python
from datetime import datetime, timedelta, timezone
_cache = {}
_TTL = timedelta(seconds=60)
def get_prompt(name):
now = datetime.now(timezone.utc)
hit = _cache.get(name)
if hit and now - hit[0] < _TTL:
return hit[1]
prompt = pm.get_active_prompt_version(name)
_cache[name] = (now, prompt)
return promptFor multi-process apps, swap the dict for Redis with the same shape.
TIP
TTL tradeoff: shorter = fresher prompts, more API load; longer = staler prompts, more resilience to upstream outages. 60 seconds is a defensible default.
Add observability
Wrap fetches with metrics + structured logs:
python
import time
import logging
logger = logging.getLogger(__name__)
def fetch_observed(name):
started = time.monotonic()
try:
prompt = pm.get_active_prompt_version(name)
elapsed_ms = (time.monotonic() - started) * 1000
logger.info(
"prompt_fetch_ok",
extra={"prompt_name": name, "sha": prompt.sha, "ms": elapsed_ms},
)
return prompt
except Exception as e:
elapsed_ms = (time.monotonic() - started) * 1000
logger.exception(
"prompt_fetch_failed",
extra={"prompt_name": name, "ms": elapsed_ms, "error": type(e).__name__},
)
raiseWith OpenTelemetry:
python
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def fetch_traced(name):
with tracer.start_as_current_span("prompt.fetch", attributes={"prompt.name": name}) as span:
prompt = pm.get_active_prompt_version(name)
span.set_attribute("prompt.sha", prompt.sha or "")
return promptHandle 409 explicitly
The 409 path means "the platform is refusing to serve unreleased content." How you respond depends on whether the prompt is critical:
python
from elsai_prompts.prompt_manager import PromptNotReleasedInEnvironmentError
# Critical path - fail loudly:
try:
prompt = pm.get_active_prompt_version("payment_confirmation")
except PromptNotReleasedInEnvironmentError:
logger.critical("payment_confirmation prompt missing in production")
raise # page the on-call
# Non-critical - fall back:
try:
prompt = pm.get_active_prompt_version("footer_message")
text = prompt.render({})
except PromptNotReleasedInEnvironmentError:
text = "© 2026 Acme Inc." # safe defaultNever except Exception away a 409 — you'll silently mask configuration bugs.
Pre-warm on startup
For a hot path that can't afford a cold-cache miss, fetch the prompt at startup:
python
PROMPTS_TO_PREWARM = ["welcome_email", "support_dialog", "summarizer"]
def prewarm_prompts():
for name in PROMPTS_TO_PREWARM:
try:
_ = pm.get_active_prompt_version(name)
logger.info("prewarmed %s", name)
except Exception as e:
logger.exception("prewarm failed for %s", name)If you fail-fast on prewarm errors, your deploy will refuse to roll out when a critical prompt is misconfigured — that's usually the right tradeoff for production.
Lock the SDK version
Pin elsai-prompts to a specific version in your requirements file:
elsai-prompts==2.0.0Not >=2.0.0. Not ~=2.0. A specific version. SDK upgrades should be a deliberate change with a tested release, not something that comes along on pip install.
Test against the real platform
Mocking the SDK in unit tests is fine for application logic. But have at least one integration test that hits a real prompt against a test project:
python
def test_integration_fetches_real_prompt():
"""Hits the real platform - requires test creds in env."""
pm = PromptManager(
api_key=os.environ["ELSAI_TEST_KEY"],
project_id=os.environ["ELSAI_TEST_PROJECT"],
environment="development",
)
prompt = pm.get_active_prompt_version("smoke_test_prompt")
assert prompt.kind in ("instruction", "f_string", "chat", "structured")This catches schema drift between the SDK and the platform that mocked tests never will.
Don't reinvent variable validation
Use prompt.variables as the source of truth:
python
def render_safe(prompt, inputs):
declared = {v["name"] for v in prompt.variables}
extra = inputs.keys() - declared
missing = declared - inputs.keys()
if missing:
raise ValueError(f"Missing: {missing}")
if extra:
logger.warning("Extra inputs ignored: %s", extra)
return prompt.render(inputs)Common anti-patterns
❌ Constructing a new PromptManager per request
No benefit, plus you re-parse env vars on every request. Construct once at startup.
❌ Catching all exceptions and returning a default
Masks 409s (release config bugs), 404s (key revoked, prompt renamed), and 5xx (platform down). Each deserves a different response. Match exception types explicitly.
❌ Hardcoding prompts as a fallback for the same content
Defeats the point of centralized prompts. If you must have a default, make it a safe degraded message, not the same content the prompt would have produced.
❌ Using environment='production' in development
Causes 409s if the production version differs from dev, and worse, lets dev code silently serve customer-facing prompts. Use the environment that matches your runtime.
❌ Skipping the env filter ('just give me whatever is active')
The env filter is the whole point — it prevents unreleased content from leaking. There's no SDK flag to disable it, and there shouldn't be.