Skip to content

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.

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

  1. Per-call override — pass context=[...] to flag.get() directly.
  2. Ambient context — set once per request from middleware. Stored per-request via contextvars / thread-local / IDisposable.
  3. 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)

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

python
with client.set_context([
    Context("user", "u-impersonated", plan="enterprise"),
]):
    if checkout.get():
        ...
# original context restored here

Context Provider (callback)

typescript
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 provider

Per-call Override

For background jobs, tests, or admin tools, pass context directly to get(). This bypasses the ambient context / provider for that single call.

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

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

python
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

python
@client.flags.on_change
def on_any_change(event):
    print(f"Flag '{event.id}' updated via {event.source}")

Flag-Scoped Listener

python
@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.

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

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

python
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 now

Sync Client (Python)

For synchronous applications (Django, Flask, CLI tools), use SmplClient instead of AsyncSmplClient. The API is identical but without await.

python
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