Flags
Package flags is feature flags: ship it dark, then turn it on for who you pick.
Package flags is feature flags: ship it dark, then turn it on for who you pick.
| Base URL | https://api.hanzo.ai |
| Operations | 8 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-0063 · Feature Flags Standard — Draft · read the specification →
This proposal defines the feature flag platform for the Hanzo ecosystem. Hanzo Flags provides boolean flags, multivariate flags, percentage rollouts and user targeting — the primitive every other plane composes, including the AI-specific splits that make model routing, prompt selection and retrieval strategy switchable without a deploy.
Evaluation semantics are PostHog-compatible — the embedded evaluator is pinned
to a 621-case parity table (apps/flags/engine.go:11-13) — so a PostHog-shaped
client repoints by changing the host. An OpenFeature adapter, if published,
wraps the generated SDK rather than defining the wire.
The A/B plane that composes this primitive — the experiment registry, its
analysis and its decision — is the separate experiments capability, HIP-1311.
Evaluator: github.com/hanzoai/flags — the
stateless Go engine (flags/go) compiled into the cloud binary
Serving: apps/flags in hanzoai/cloud, at /v1/flags — there is no
standalone flags service, port or image; SDK access is the generated cloud SDKs
(HIP-1030)
Motivation
Every software team eventually needs to decouple deployment from release. You push code to production, but the new behavior is hidden behind a flag. You turn it on for 1% of users, watch metrics, then ramp to 100%. If something breaks, you kill the flag -- no rollback, no redeployment, no incident.
This is table stakes for web applications. But AI systems introduce a category of experimentation that traditional feature flag platforms were never designed for:
-
Model version rollouts: You have Zen-72B in production. Zen-120B is ready. You want to route 5% of inference traffic to the new model, compare quality scores, latency, and cost, then decide whether to promote. This is not a boolean flag -- it is a traffic split with multi-dimensional metric analysis.
-
Prompt template A/B testing: The same model with two different system prompts produces different outputs. Which prompt yields better user satisfaction? Better task completion? Lower token usage? You need to randomly assign users to prompt variants and track downstream metrics.
-
RAG strategy comparison: Retrieval-augmented generation has multiple knobs -- chunk size, overlap, embedding model, reranker, top-k. Comparing strategies requires running parallel pipelines and measuring answer quality. This is a multi-armed bandit problem, not a simple on/off switch.
-
Cost optimization experiments: Route 20% of traffic to a cheaper model. If quality metrics remain within the baseline, promote the cheaper model for that traffic segment.
General-purpose flag platforms treat flags as configuration switches for UI elements, and their evaluation rides a hosted tier — a CDN poll or a streaming connection — that can go stale. For AI inference routing, a stale flag is a request routed to a deprecated model or a cost tier over budget. Hence the design: a stateless, PostHog-compatible evaluator compiled into the cloud binary, definitions in each org's own encrypted SQLite, with no evaluation tier that can go stale.
Specification
The shipped surface
flags (manifest/apps.go:54) serves seven operations under /v1/flags
(apps/flags/routes.go:47-60): POST /v1/flags and POST /v1/flags/decide
are the SAME evaluate handler — one verdict function, two spellings for the
PostHog-shaped clients; GET /v1/flags/defs, GET|PUT|DELETE /v1/flags/defs/{key} manage definitions; GET /v1/flags/activity reads the
audit log; GET /v1/flags/health probes. Definitions live in per-(org,
project) encrypted SQLite — {DataDir}/orgs/{org}/projects/{project}/flags.db
via cloud.OrgDB (apps/flags/store.go:3-6) — and evaluation runs in-process
through the embedded evaluator github.com/hanzoai/flags/go, a pure function
of (definitions JSON, context JSON) answering in microseconds
(apps/flags/engine.go:1-13). No KV, no sync protocol, no network hop:
every pod evaluates from its own hot copy, which is what makes the p99 targets
below ordinary rather than aspirational.
The credential is the org's ordinary bearer per HIP-0026 — there is no hf_*
key family. The admin plane is not a second prefix: definition writes are
org-scoped on the same surface, and the platform's own switches are the
reserved platform store the SuperAdmin flips from the cockpit through the same
engine (apps/flags/flags.go:21-27).
Stated for HIP-0139 §6: the capability is free, said in those words
(plugin/flags/main.go:21 — Price: cloud.Free), and /v1/flags/ is on the
spend gate's never-refuse list (spend.go:453) because the kill switch must be
observable by an unpaid org. It owns the definition store above and no other.
It publishes no events on the bus, so a customer's webhooks (HIP-1310)
receive nothing from it, and it emits nothing to observability beyond the
request span. It is ga (HIP-0139 §8): flags is the mechanism stage-gating
itself rides, so it cannot sit behind a flag. Upstream: it embeds
github.com/hanzoai/flags/go, our own implementation of PostHog-compatible
evaluation semantics, pinned to the prior implementation's answers by a
621-case parity table (apps/flags/engine.go:11-13).
Flag Types
Hanzo Flags supports four flag value types, matching the OpenFeature specification:
| Type | Use Case | Example |
|---|---|---|
| Boolean | Kill switches, feature gates | new-dashboard: true/false |
| String | Multivariate experiments, model selection | inference-model: "zen-72b" / "zen-120b" |
| Number | Numeric configuration, thresholds | rate-limit-multiplier: 1.5 |
| Object | Complex configuration, prompt templates | rag-config: {"chunk_size": 512, "top_k": 5} |
Flag Definition Schema
The stored definition is the PostHog-compatible JSON the embedded evaluator
consumes (apps/flags/store.go:5), versioned and audited; the evaluator's
parity table, not this document, is the normative statement of its fields.
The JSON below illustrates the concepts a definition carries — variants,
targeting, rollout percentages, a fallthrough — in a reader-friendly shape:
{
"key": "inference-model-experiment",
"name": "Inference Model A/B Test",
"description": "Compare Zen-72B vs Zen-120B on production traffic",
"type": "string",
"defaultValue": "zen-72b",
"enabled": true,
"targeting": {
"rules": [
{
"name": "Internal dogfood",
"priority": 1,
"conditions": [
{"attribute": "org", "op": "in", "value": ["hanzo", "zoo"]}
],
"variant": "zen-120b"
},
{
"name": "Pro users 20% rollout",
"priority": 2,
"conditions": [
{"attribute": "plan", "op": "eq", "value": "pro"}
],
"rollout": {
"percentages": {"zen-72b": 80, "zen-120b": 20},
"seed": "inference-model-experiment"
}
}
],
"fallthrough": {
"variant": "zen-72b"
}
},
"variants": {
"zen-72b": {"value": "zen-72b"},
"zen-120b": {"value": "zen-120b"}
},
"metadata": {
"owner": "ml-team",
"jira": "ML-1234",
"experiment_id": "exp_model_comparison_2026Q1"
}
}Targeting Rules
Targeting determines which variant a user receives. Rules are evaluated top-to-bottom by priority; the first matching rule wins.
Condition Operators
| Operator | Description | Example |
|---|---|---|
eq | Equals | {"attribute": "plan", "op": "eq", "value": "pro"} |
neq | Not equals | {"attribute": "plan", "op": "neq", "value": "free"} |
in | In set | {"attribute": "org", "op": "in", "value": ["hanzo", "zoo"]} |
not_in | Not in set | {"attribute": "country", "op": "not_in", "value": ["CN", "RU"]} |
gt, gte, lt, lte | Numeric comparison | {"attribute": "account_age_days", "op": "gte", "value": 30} |
contains | String contains | {"attribute": "email", "op": "contains", "value": "@hanzo.ai"} |
regex | Regex match | {"attribute": "user_agent", "op": "regex", "value": "Mobile.*"} |
semver_gt, semver_lt | Semantic version | {"attribute": "app_version", "op": "semver_gt", "value": "2.1.0"} |
Targeting Dimensions
User Attributes:
- user_id / distinct_id # Stable user identifier
- email # Email-based targeting
- org / organization # Organization membership
- plan # Subscription tier (free, pro, enterprise)
- account_age_days # Days since signup
- country / region # GeoIP-derived (from request IP)
- language # Accept-Language header
Device Attributes:
- device_type # Desktop, Mobile, Tablet
- os # macOS, Windows, Linux, iOS, Android
- browser # Chrome, Firefox, Safari
- app_version # Semantic version of client app
AI-Specific Attributes:
- model_requested # Model the user requested
- token_budget # User's remaining token budget
- request_complexity # Estimated prompt complexity (token count)
- provider_preference # User's preferred AI providerPercentage Rollouts
Percentage-based rollouts use consistent hashing on a stable identifier (typically user_id + flag key). This ensures a user always receives the same variant for a given flag, even across multiple evaluations, multiple servers, and service restarts.
hash = murmur3(user_id + ":" + flag_key + ":" + seed)
bucket = hash % 10000 // 0-9999, giving 0.01% granularityThe seed is configurable per flag. Changing the seed reshuffles the assignment -- useful when you want to run a new experiment on the same flag key without the previous assignment biasing results.
Evaluation API
POST /v1/flags/decide
Evaluate flags for an identity. This is the primary hot-path endpoint —
served as both POST /v1/flags and POST /v1/flags/decide, one handler
(apps/flags/routes.go:52-55). It answers for EVERY flag in the caller's
definitions at once (the PostHog /decide shape), so there is no separate
batch endpoint to keep in step with it.
POST /v1/flags/decide HTTP/1.1
Host: api.hanzo.ai
Authorization: Bearer <the org's ordinary key, HIP-0026>
Content-Type: application/json
{
"distinct_id": "user_789",
"properties": {
"org": "hanzo",
"plan": "pro",
"country": "US"
}
}Response:
{
"key": "inference-model-experiment",
"value": "zen-120b",
"variant": "zen-120b",
"reason": "TARGETING_MATCH",
"rule_id": "internal-dogfood",
"metadata": {
"experiment_id": "exp_model_comparison_2026Q1"
}
}The response and reason vocabulary is the embedded evaluator's — PostHog-compatible, pinned by its parity table — and the JSON above is illustrative of the verdict's shape (state, variant, payload per flag), not a schema this HIP owns: the definitive shape is what the evaluator answers and the served document publishes (HIP-1030).
Performance Requirements
| Metric | Target |
|---|---|
| Evaluation latency (p50) | < 1ms |
| Evaluation latency (p99) | < 5ms |
| Batch evaluation (10 flags, p99) | < 10ms |
| Availability | 99.99% |
| Throughput | 100K evaluations/second per node |
These targets are achievable because flag evaluation is a pure in-memory operation: definitions are read from the org's own SQLite store into a hot in-process copy, and the evaluator is a pure function over them (apps/flags/engine.go:5-7). There is no KV tier and no sync hop to go stale.
Management surface
Definition management is org-scoped on the SAME prefix — there is no
/admin/v1 and no separate management key family:
GET /v1/flags/defs # list definitions
GET /v1/flags/defs/{key} # read one
PUT /v1/flags/defs/{key} # create / update (versioned)
DELETE /v1/flags/defs/{key} # delete
GET /v1/flags/activity # the audit log of definition changesEvery mutation to a flag definition is recorded append-only with the acting
user, timestamp and version (apps/flags/store.go), readable at
GET /v1/flags/activity — which is what answers "who changed the model
rollout percentage at 3am?". The platform's own operational switches are the
one cross-tenant surface: they live in the reserved platform store and only a
SuperAdmin writes them (apps/flags/flags.go:21-27).
The plane that composes this one
An A/B test is a caller of this primitive, not a second engine inside it: the
experiment registry, the analysis over its arms and the decision that locks a
winner are the experiments capability, HIP-1311. Create and decide there write
a flag definition here, which is why the arm a request is served and the arm a
report names are one value.
Integration with the serving path
Traffic splitting at the infrastructure layer — the gateway evaluating a flag
before the request reaches application code — is a design this section used
to specify in KrakenD configuration. No such gateway plugin ships; what
ships is in-process composition: any subsystem in the binary evaluates
through flags.Assign (the same deterministic rollout hash the HTTP surface
uses), which is how apps/experiments and apps/campaign split traffic
today with no network hop at all. A gateway-level split remains open design
and MUST, if built, evaluate through the same embedded engine rather than a
second one.
Architecture
One binary, one subsystem, no tiers. apps/flags holds the definitions in each
org's own encrypted SQLite file and evaluates them in-process through the
embedded evaluator. There is no standalone flags service, no SQL-to-KV sync, no
KV pub/sub channel, no Kafka exposure topic and no separate deployment — the
earlier revision of this HIP specified all four, and the shipped shape replaced
them with something strictly simpler: the store is beside the evaluator, so
there is nothing to sync and nothing to go stale. Deployment is the cloud image;
the plugin binary is plugin/flags.
The credential is the caller's ordinary org key (HIP-0026). There is no hf_*
key family: the evaluate routes take the same bearer as every other /v1
surface, and tenancy is the validated principal, never the key prefix.
SDK Usage
The client surface is the generated cloud SDKs and the hanzo CLI, both
projections of the served document (HIP-1030): the flags operations appear as
the Flags class/command group in every generated language. The hand-written @hanzoai/flags-js /
hanzoai-flags / flags-go packages and the OpenFeature provider wrappers
this section used to show are not published; if OpenFeature adapters are
wanted they wrap the generated client, they do not replace it.
Security
Authentication
Every request carries the org's ordinary credential (HIP-0026); there is no
flags-specific key family. Tenancy is the VALIDATED principal — principal.Org
and principal.Project — never a header a client can write.
Flag Access Control
Flags are scoped to (org, project), and the isolation is physical, not a
filter: each (org, project) pair is its own SQLite file
(apps/flags/store.go:3-6), so one tenant's evaluation cannot read another's
definitions even in the presence of a query defect. The platform-switch store
is the reserved platform namespace and only a SuperAdmin writes it.
What an attacker gets from the wrong implementation: flags are the policy primitive other planes compose (admission, stage gating per HIP-0139 §8.2), so a cross-tenant write here is not a cosmetic defect — it is another org's kill switches flipped, and a readable beta-flag roster is an existence oracle for capabilities a customer has not been shown.
Audit Trail
Every flag mutation (create, update, toggle, delete) is recorded with:
- Acting user (from IAM token or API key)
- Timestamp (UTC, millisecond precision)
- Full diff of the change (before/after JSON)
- IP address and user agent
The audit log is append-only and cannot be modified or deleted via the API. It is stored in SQL with a 2-year retention policy.
Monitoring
It exports no metrics of its own today: there is no flags_* metric family and
no sync or consumer lag to alert on, because there is no sync and no consumer. What a customer can read back under
/v1/o11y is the request span every route already gets; the definition
change history is GET /v1/flags/activity. Alerting on evaluation latency,
if wanted, rides the fleet's ordinary request telemetry rather than a
capability-local exporter.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | flags at its own prefix | 8 operations |
| CLI | hanzo flags … | 8 of 8 |
| SDK | FlagsApi in every published client | 8 methods |
| MCP | tool flags on https://api.hanzo.ai/v1/mcp | 8 operations, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/flags/defs, operation get_flags_defs:
hanzo flags defs listimport { Configuration, FlagsApi } from 'hanzoai';
const api = new FlagsApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getFlagsDefs();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import FlagsApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = FlagsApi(client).get_flags_defs()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.FlagsAPI.GetFlagsDefs(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, flags_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = flags_api::get_flags_defs(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.FlagsApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new FlagsApi(client).getFlagsDefs();curl https://api.hanzo.ai/v1/flags/defs \
-H "Authorization: Bearer $HANZO_API_KEY"The door reaches flags through the flags tool, which names its 8 operations with its own verbs — this one among them, under a name only the door declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_flag_activity"
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/flags/activity | Returns the caller's flag change log newest-first: every create, update and delete, with the actor and the time. |
POST /v1/flags/decide | Evaluate runs the caller's flag definitions for one identity and returns the flag verdict: which flags are on (or which variant), their payloads, and… |
GET /v1/flags/defs/{key} | Returns one flag definition by key, or 404 when the caller's store has none under that key. |
PUT /v1/flags/defs/{key} | Creates or replaces the flag definition at the path's key and returns the stored row. |
DELETE /v1/flags/defs/{key} | Removes one flag definition by key and records the deletion in the change log. |
GET /v1/flags/defs | Returns every flag definition in the caller's (org, project) store, by key, with its version and who last changed it. |
GET /v1/flags/health | Health reports that the flag engine is serving. |
POST /v1/flags | Evaluate runs the caller's flag definitions for one identity and returns the flag verdict: which flags are on (or which variant), their payloads, and… |
How is this guide?