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, picks up live updates over WebSocket, and fires change listeners as values shift.
The first runtime call triggers lazy initialization — the SDK fetches all configs, resolves inheritance and environment overrides into a local cache, and opens a shared WebSocket for live updates. Subsequent reads hit the cache.
Get Resolved Values
get(id) returns a live, dict-like proxy over the resolved values for the active environment. The proxy is the live subscription — every read sees the latest server-pushed state, no separate subscribe() call required.
import asyncio
from smplkit import AsyncSmplClient
async with AsyncSmplClient(environment="production", service="my-service") as client:
cfg = await client.config.get("user-service")
print(cfg["database.host"])
print(cfg["max_retries"])
print(cfg.get("cache_ttl_seconds")) # dict-style .get with default
for key, value in cfg.items():
print(key, value)Typed Resolution
Pass a model class as the second argument to get to project values through it. The proxy reconstructs the model from the latest values on each read, so attribute access type-checks against your model while still tracking live updates.
from pydantic import BaseModel
class Database(BaseModel):
host: str
port: int
pool_size: int
class UserServiceConfig(BaseModel):
database: Database
cache_ttl_seconds: int
enable_signup: bool
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)The SDK builds a nested structure from flat dot-notation keys (database.host → cfg.database.host) before passing it to your model.
Live Updates
The proxy returned by get is identity-stable — hold onto it and reads always reflect current values. After a server-side write, the WebSocket pushes the new values into the cache and subsequent reads on the same proxy see them.
cfg = await client.config.get("user-service", UserServiceConfig)
print(cfg.max_retries) # 3 — initial value
# ...someone updates max_retries to 7 via Console UI or management API...
await asyncio.sleep(0.2) # brief pause for the WS event to arrive
print(cfg.max_retries) # 7 — same proxy, new valueChange Listeners
Register callbacks that fire when config values change. Three scoping levels are available: global (any config), config-scoped (specific config), and item-scoped (specific item within a config).
@client.config.on_change
def on_any_change(event):
print(f"{event.config_id}.{event.item_key}: "
f"{event.old_value!r} -> {event.new_value!r}")
# Item-scoped via the live-proxy handle
common_cfg = await client.config.get("common")
@common_cfg.on_change("max_retries")
def on_retries_change(event):
print(f"max_retries changed to {event.new_value}")event.source is "websocket" when the change came from a server push and "manual" when it came from a refresh() call.
Inheritance
Configs that have a parent inherit from it (and transitively up the parent chain). Values defined in a child take precedence; anything not overridden falls through to the parent. A config with no parent is standalone — common is a built-in config you can use as a parent for shared defaults, but it is not applied automatically.
auth = await client.config.get("auth-module")
print(auth["session_ttl_minutes"]) # defined on auth-module
print(auth["mfa_enabled"]) # defined on auth-module
print(auth["app_name"]) # inherited from commonManual Refresh
refresh() re-fetches all configs, re-resolves values, and fires change listeners for any values that differ. WebSocket events trigger this automatically in production; call refresh() manually after suspecting drift, in short-lived scripts, or in tests.
await client.config.refresh()In Python, WebSocket events trigger automatic refresh. Use await asyncio.sleep(...) to allow the event to arrive in tests rather than calling refresh() manually.
waitUntilReady
If your code creates a client and immediately fires a management write that you expect to observe via change listeners, call waitUntilReady() first. Without it, the write can race the WebSocket subscribe and the SDK silently misses its own broadcast.
async with AsyncSmplClient(environment="production", service="my-service") as client:
await client.wait_until_ready()
# safe to fire writes that you expect to broadcast backThe same method exists in every SDK: client.wait_until_ready() (Python), client.waitUntilReady() (TS / Java), client.WaitUntilReady(ctx, 0) (Go), await client.WaitUntilReadyAsync() (C#). Production code that has been running for a while doesn't need it — only the boot-then-write-then-listen pattern does.
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:
cfg = client.config.get("user-service")
print(cfg["database.host"])
typed = client.config.get("user-service", UserServiceConfig)
print(typed.database.host)
@client.config.on_change
def on_change(event):
print(f"{event.config_id}.{event.item_key} changed")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

