Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,46 @@ result = client.organizations.list_organizations(
> [!NOTE]
> The WorkOS API currently honors `Idempotency-Key` only on the [Create Audit Log Event](https://workos.com/docs/reference/audit-logs/event) endpoint (`audit_logs.create_event`). Other endpoints accept the header but do not deduplicate requests, so a retried mutation elsewhere can still create a duplicate.

## Feature Flags Runtime Client

The runtime client keeps an environment's feature flag configuration in memory and evaluates flags locally, with no network request per evaluation. It polls the configuration in the background (every 30 seconds by default) with the client's API key, base URL and retry settings. Create one long-lived instance per process and share it across requests. It is for trusted server-side code only.

```python
from workos import WorkOSClient

workos = WorkOSClient(api_key="sk_...")
flags = workos.create_feature_flags_runtime_client()
flags.start() # polls on a daemon thread
flags.wait_until_ready(timeout=5) # raises TimeoutError if no configuration arrives

flags.is_enabled("new-checkout", {"user": "user_123", "organization": "org_123"})
flags.is_enabled("new-checkout", {"workspace": "ws_1"}, default=True)
flags.get_all_flags({"organization": "org_123"}) # {"new-checkout": True, ...}

flags.close() # on shutdown; or use `with workos.create_feature_flags_runtime_client() as flags:`
```

**Evaluation.** The context maps a target type to the exact, case-sensitive ID being evaluated. `user` and `organization` are target types like any [custom target](https://workos.com/docs/feature-flags/custom-targets) type, and an ID of `None` is skipped. A disabled flag serves its off value. For an enabled flag, the first rule whose conditions all match wins, so a rule serving `False` can exclude a user before a later rule includes their organization; otherwise the flag's default value applies. A missing flag, an unready client, or a flag without a boolean answer returns `default` (`False` unless you pass one), and `get_all_flags` omits flags without an answer. Evaluation never raises: invalid context entries are ignored with a logged warning and the rest is evaluated.

**Failures.** A failed poll keeps the last good configuration and retries with exponential backoff. Transient failures (429, 500, 502, 503, 504, timeouts, connection errors) are also retried within one refresh, up to the client's `max_retries` and within `request_timeout`. A 429's `Retry-After` is honored up to 60 seconds, both by those retries and before the next poll. The client streams the response and gives every network wait only the time left, so a refresh takes at most `request_timeout` plus one read of a response body that stalls; a custom `HTTPBackend` that is not an httpx client receives the remaining time as its per-request timeout instead. A 401, from a poll or from `refresh()`, stops polling for good: create a new client with a valid API key. Failures are logged to the `workos.feature_flags.runtime` logger (pass `logger=` to use your own) and reported to error listeners. `flags.status` shows readiness, the last error, refresh counts and timestamps, `polling_stopped`, `flag_count` and `cache_age()`.

**Notifications.** `on_change` (or `add_change_listener`) fires after a refresh installs a different configuration, including the first load, with the previous and current snapshots and the changed slugs. `on_error` fires for failed background polls and `on_status` after every refresh. Listeners run one at a time; their exceptions are logged and never affect polling.

**Bootstrap.** Persist the configuration and pass it back to make a new client ready before its first poll:

```python
from workos import FeatureFlagsConfiguration

flags = workos.create_feature_flags_runtime_client(
bootstrap=FeatureFlagsConfiguration.from_json(cached_json), # raises on unreadable JSON
on_change=lambda event: save_cache(event.current.to_json()),
)
```

**Inspection and manual refresh.** `flags.get_flag(slug)` returns a flag's configuration as received, with its rules in order. `flags.refresh()` fetches the configuration immediately, raising on failure, and works without `start()`, e.g. in scripts or serverless functions.

**Processes.** The runtime client polls through the `WorkOSClient`'s HTTP client, so close it before closing the `WorkOSClient` or an HTTP client you supplied. Create the `WorkOSClient` and the runtime client in each worker process, after forking, rather than in an app gunicorn loads with `--preload`. Background threads must be enabled, so run uWSGI with `--enable-threads`.

## Type Safety

This SDK ships with full type annotations (`py.typed` / PEP 561) and works with mypy, pyright, and IDE autocompletion out of the box. All API resource models are `@dataclass(slots=True)` classes with `from_dict()` / `to_dict()` for serialization.
Expand Down
6 changes: 6 additions & 0 deletions src/workos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
TransportTimeout,
)
from ._pagination import AsyncPage, ListMetadata, SyncPage
from .feature_flags.runtime import (
FeatureFlagsConfiguration,
FeatureFlagsRuntimeClient,
)
from .public_client import create_public_client
from ._types import NOT_GIVEN, NotGiven, RequestOptions

Expand Down Expand Up @@ -51,4 +55,6 @@
"TransportError",
"TransportTimeout",
"TransportConnectError",
"FeatureFlagsRuntimeClient",
"FeatureFlagsConfiguration",
]
6 changes: 6 additions & 0 deletions src/workos/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ def pkce(self) -> PKCE:
"""PKCE (Proof Key for Code Exchange) utilities."""
return PKCE()

# A plain function bound as a method, so its signature and docs live next
# to the runtime client's constructor.
from .feature_flags.runtime._factory import (
create_feature_flags_runtime_client as create_feature_flags_runtime_client,
)

# @oagen-ignore-end


Expand Down
28 changes: 27 additions & 1 deletion src/workos/feature_flags/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,30 @@
# This file is hand-maintained. The feature flags runtime client evaluates
# flags locally from a polled configuration; it is not part of the OpenAPI spec.

"""Local evaluation of WorkOS feature flags from a polled configuration."""
"""Local evaluation of WorkOS feature flags from a polled configuration.

Use :class:`FeatureFlagsRuntimeClient` with :class:`workos.WorkOSClient`.
"""

from ._configuration import (
ConditionDefinition,
FeatureFlagsConfiguration,
FeatureFlagsConfigurationError,
FlagDefinition,
RuleDefinition,
)
from ._context import EvaluationContext
from ._sync import FeatureFlagsRuntimeClient
from ._types import FlagsChangedEvent, RuntimeStatus

__all__ = [
"ConditionDefinition",
"EvaluationContext",
"FeatureFlagsConfiguration",
"FeatureFlagsConfigurationError",
"FeatureFlagsRuntimeClient",
"FlagDefinition",
"FlagsChangedEvent",
"RuleDefinition",
"RuntimeStatus",
]
Loading
Loading