Flags Runtime
The runtime API is the day-to-day SDK experience. Flags are created via the Console UI or the management API. The runtime declares typed flag handles in code, evaluates them locally with JSON Logic, and reacts to definition changes in real time over WebSocket.
The first call to flag.get() triggers lazy initialization — the SDK fetches all flag definitions, opens the shared WebSocket, and caches everything locally. Subsequent calls are pure local evaluation; there is no explicit connect() step.
Declare Flag Handles
Flag handles are local declarations that map a flag id to a typed default. They do not create flags on the server. The code-level default is what should be served if smplkit is unreachable or the flag does not exist — typically the safe / conservative value.
checkout = client.flags.boolean_flag("checkout-v2", default=False)
banner = client.flags.string_flag("banner-color", default="red")
retries = client.flags.number_flag("max-retries", default=3)
theme = client.flags.json_flag("ui-theme", default={"mode": "light"})Attach Context
Targeting rules need a context — a list of Context objects describing the user, account, device, etc. doing the request. There are three ways to attach context, in order of priority (highest wins):
- Per-call override — pass
context=[...]toflag.get()directly. - Ambient context — set once per request from middleware. Stored per-request via contextvars / thread-local /
IDisposable. - Context provider — a callback the SDK invokes on every evaluation that doesn't already have context.
The Python and Java SDKs use ambient context (client.set_context(...)). The TypeScript and Go SDKs use a provider callback (client.flags.setContextProvider(...)). C# offers both.
Ambient Context (per-request)
from smplkit import Context
# Typically called once from middleware at request start.
# Returns a scope object usable as a `with` block to revert on exit.
client.set_context([
Context("user", "alice@acme.com", plan="enterprise", beta_tester=True),
Context("account", "1234", region="us", industry="technology"),
])
# Later, on the same task / thread:
if checkout.get():
render_new_checkout()set_context() returns a scope object that doubles as a with block — handy for impersonation or scoped overrides:
with client.set_context([
Context("user", "u-impersonated", plan="enterprise"),
]):
if checkout.get():
...
# original context restored hereContext Provider (callback)
import { Context } from "@smplkit/sdk";
// Wire once at startup; the callback fires on every flag.get()
client.flags.setContextProvider(() => [
new Context("user", currentUser.email, {
plan: currentUser.plan,
beta_tester: currentUser.betaTester,
}),
new Context("account", String(currentAccount.id), {
region: currentAccount.region,
}),
]);
const isV2 = checkout.get(); // uses the providerPer-call Override
For background jobs, tests, or admin tools, pass context directly to get(). This bypasses the ambient context / provider for that single call.
result = checkout.get(context=[
Context("user", "test-user", plan="free", beta_tester=False),
Context("account", "test-account", region="jp"),
])Evaluate Flags
get() is synchronous — local JSON Logic over the cached definitions. Rules are matched in order (within the active environment); the first matching rule's serve value wins. If no rule matches, the per-environment default falls back to the flag default, then to the code-level default.
checkout_value = checkout.get()
banner_value = banner.get()
retries_value = retries.get()The first call lazy-initializes the runtime (fetch + WebSocket subscribe). For deterministic boot — useful in tests and showcases that fire writes immediately and want to observe the resulting WebSocket events — call wait_until_ready() once at startup:
async with AsyncSmplClient(environment="production", service="my-service") as client:
await client.wait_until_ready()Change Listeners
Register callbacks that fire when flag definitions change. Two scoping levels: global (any flag) and id-scoped (specific flag id).
Global Listener
@client.flags.on_change
def on_any_change(event):
print(f"Flag '{event.id}' updated via {event.source}")Flag-Scoped Listener
@client.flags.on_change("banner-color")
def on_banner_change(event):
print(f"banner-color changed via {event.source}")event.source is "websocket" for server-pushed updates and "manual" for refreshes triggered by refresh().
Manual Refresh
refresh() re-fetches all flag definitions and clears the evaluation cache. WebSocket updates trigger this automatically; call manually after suspecting drift, in short-lived scripts, or in tests.
await client.flags.refresh()Cache Statistics
The SDK caches resolved values per-evaluation-key. Repeated calls with the same context skip JSON Logic entirely. stats() exposes hit/miss counters for diagnostics.
stats = client.flags.stats()
print(f"hits={stats.cache_hits} misses={stats.cache_misses}")Bulk-Register Contexts
For populating the Console rule builder's autocomplete with real context types and attributes, bulk-register contexts via the management API. The runtime client buffers these and flushes periodically (every 5s by default); pass flush=true (or call flush()) to force an immediate POST.
from smplkit import Context
client.manage.contexts.register([
Context("user", "alice@acme.com", plan="enterprise"),
Context("account", "1234", region="us"),
])
client.manage.contexts.flush() # optional — drain the buffer nowSync 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, Context
with SmplClient(environment="production", service="my-service") as client:
checkout = client.flags.boolean_flag("checkout-v2", default=False)
# Per-request middleware:
client.set_context([
Context("user", request.user.email, plan=request.user.plan),
])
if checkout.get():
render_new_checkout()Next Steps
- Flags Management — Create and configure flags programmatically
- Smpl Logging — Control log verbosity without redeploying
- API Reference — Flags — Full REST API documentation

