Config Runtime
The runtime API is the day-to-day SDK experience. Configs are created via the Console UI or the management API; the runtime resolves values, subscribes to live updates, and reacts to changes.
The first runtime call triggers lazy initialization — it fetches all configs, resolves inheritance and environment overrides, and opens a WebSocket for live updates. Subsequent calls are local cache reads.
Get Resolved Values
get(key) returns a flat dict/map of resolved values for the current environment. Inheritance is walked, environment overrides applied, values unwrapped.
async with AsyncSmplClient(environment="production", service="my-service") as client:
config = await client.config.get("user_service")
print(config["database.host"])
print(config["max_retries"])
print(config.get("cache_ttl_seconds"))Typed Resolution
Pass a model class or struct to get for typed access. The SDK builds a nested structure from flat dot-notation keys and constructs the model.
from pydantic import BaseModel
class Database(BaseModel):
host: str
port: int
name: str
pool_size: int
class UserServiceConfig(BaseModel):
database: Database
cache_ttl_seconds: int
enable_signup: bool
app_name: str = ""
max_retries: int = 3
cfg = await client.config.get("user_service", UserServiceConfig)
print(cfg.database.host)
print(cfg.cache_ttl_seconds)
print(cfg.max_retries)Subscribe to Live Updates
subscribe() returns a live proxy that always reflects the latest server-side state. When a WebSocket event arrives, the proxy updates automatically — no polling or re-fetching needed.
live = await client.config.subscribe("user_service", UserServiceConfig)
print(live.database.host)
print(live.cache_ttl_seconds)The proxy always returns current values — after a server-side change propagates via WebSocket, subsequent reads reflect the update.
Change Listeners
Register callbacks that fire when config values change. Three scoping levels are available: global (any config), config-scoped (specific config key), and item-scoped (specific item within a config).
@client.config.on_change
def on_any_change(event):
print(f"{event.config_key}.{event.item_key}: "
f"{event.old_value!r} -> {event.new_value!r}")
@client.config.on_change("common", item_key="max_retries")
def on_retries_change(event):
print(f"max_retries changed to {event.new_value}")Inheritance
Configs inherit from their parent (and transitively up to common). Values defined in a child take precedence; anything not overridden falls through to the parent.
auth = await client.config.get("auth_module")
print(auth["session_ttl_minutes"])
print(auth["mfa_enabled"])
print(auth["app_name"])session_ttl_minutes and mfa_enabled come from auth_module. app_name is inherited from common.
Manual Refresh
refresh() re-fetches all configs, re-resolves values, and fires change listeners for any values that differ. In production, WebSocket events trigger this automatically. Manual refresh is useful for demos, scripts, and edge cases.
await asyncio.sleep(2)
new_retries = (await client.config.get("user_service")).get("max_retries")In Python, WebSocket events trigger automatic refresh. Use asyncio.sleep() to allow the event to arrive rather than calling refresh manually.
Sync Client (Python)
For synchronous applications (Django, Flask, CLI tools), use SmplClient instead of AsyncSmplClient. The API is identical but without await.
from smplkit import SmplClient
with SmplClient(environment="production", service="my-service") as client:
config = client.config.get("user_service")
host = config["database.host"]
cfg = client.config.get("user_service", UserServiceConfig)
print(cfg.database.host)
live = client.config.subscribe("user_service", UserServiceConfig)
print(live.database.host)Next Steps
- Config Management — Create and manage configs programmatically
- Smpl Flags — Control feature rollouts with rules-based targeting
- API Reference — Config — Full REST API documentation

