| name | unleash-flag |
| description | This skill should be used when the user asks to "add a feature flag", "gate a feature", "put this behind a flag", "add a workspace flag", "feature flag this", "make this configurable per workspace", "configure Unleash", or references "flags.json", "useWorkspaceFlag", "useFlag", "SyncFlags", or "controlled rollout" of a feature. |
Unleash Feature Flag
Workspace-scoped feature flags for the Ambient Code Platform — naming, gating, evaluation, and lifecycle management.
Checklist
Every new flag requires these steps. Skip none.
- Define the flag in
components/manifests/base/core/flags.json
- Gate the frontend using the correct hook for scope
- Update tests to mock the flag hook
- Verify type-check and tests pass
1. Define the Flag
Add to components/manifests/base/core/flags.json:
{
"name": "category.feature-name.enabled",
"description": "Human-readable purpose of this flag",
"tags": [
{
"type": "scope",
"value": "workspace"
}
]
}
Naming Convention
All new flags must use the category.feature-name.enabled format:
category.feature-name.enabled
| Segment | Rules | Examples |
|---|
category | One of the standard categories below | runner, model, integration |
feature-name | Lowercase, hyphen-separated, descriptive | gemini-cli, coderabbit |
.enabled | Required suffix for all new flags | — |
Standard categories:
| Category | When | Example |
|---|
integration | External service integrations | integration.coderabbit.enabled |
runner | Runner type availability | runner.gemini-cli.enabled |
model | Model availability (auto-generated by SyncFlags) | model.claude-sonnet-4-5.enabled |
feature | Platform capabilities | feature.scheduled-export.enabled |
Grandfathered flags (do not rename):
jira-write
ldap.autocomplete.enabled
scheduled-session.reuse.enabled
These predate the convention. New flags must not follow their patterns.
Rules:
- Unique across the entire system — never reuse a retired flag name
- Descriptive — someone unfamiliar should understand the scope from the name alone
- Dot-delimited with category prefix — enables grouping in workspace settings UI
- Lowercase with hyphens within segments (not underscores, not camelCase)
Workspace Settings Visibility
The scope:workspace tag is what makes a flag appear in the workspace settings UI. Without it, the flag exists in Unleash but workspace admins cannot toggle it.
Backend SyncFlags runs at startup: reads flags.json, creates flags in Unleash (type: release, disabled by default, 0% rollout), and tags them. Flags without EnabledByDefault: true start off.
To force a re-sync, restart the backend. To clear a stuck flag, archive + purge it in Unleash admin before restarting.
2. Gate the Frontend
Choosing the Right Hook
Default: useWorkspaceFlag — always prefer this hook. It respects ConfigMap overrides set by workspace admins.
import { useWorkspaceFlag } from "@/services/queries/use-feature-flags-admin";
const { enabled } = useWorkspaceFlag(projectName, "category.feature.enabled");
Exception: useFlag — only for the rare case where no project context is available (e.g., cluster-level admin pages like /integrations). It bypasses workspace overrides entirely and returns only the Unleash global default. Do not use it in project-scoped pages.
import { useFlag } from "@/lib/feature-flags";
const enabled = useFlag("category.feature.enabled");
| Hook | Scope | Evaluation |
|---|
useWorkspaceFlag | Project-scoped pages (default) | ConfigMap override > Unleash default |
useFlag | Cluster-scoped pages only (exception) | Unleash default only |
Gating Patterns
Conditionally render a component:
const { enabled: featureEnabled } = useWorkspaceFlag(projectName, "category.feature.enabled");
{featureEnabled && <FeatureComponent />}
Conditionally include in an array:
const items = [
{ key: "always-visible", name: "Always" },
...(featureEnabled
? [{ key: "gated", name: "Gated Feature" }]
: []),
];
Extract project name from route params (when not passed as prop):
import { useParams } from "next/navigation";
const params = useParams();
const projectName = params.name as string;
Backend Gating (when needed)
For full-stack gating (not always necessary — frontend-only is often sufficient):
if !FeatureEnabled("category.feature.enabled") {
c.JSON(http.StatusForbidden, gin.H{"error": "Feature not enabled"})
return
}
if !FeatureEnabledForRequest(c, "category.feature.enabled") {
c.JSON(http.StatusForbidden, gin.H{"error": "Feature not enabled"})
return
}
Backend Middleware Gating
For endpoints that should be entirely hidden behind a flag, create a middleware in routes.go that checks FeatureEnabledForRequest() and returns 404 when the flag is off:
flagged := router.Group("/api/v1/feature")
flagged.Use(func(c *gin.Context) {
if !handlers.FeatureEnabledForRequest(c, "category.feature.enabled") {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.Next()
})
{
flagged.GET("/resource", handlers.ListResource)
flagged.POST("/resource", handlers.CreateResource)
}
This returns 404 (not 403) when the flag is off, so the endpoint appears not to exist. Use this for features that shouldn't even be discoverable until enabled.
E2E Testing with Feature Flags
When writing E2E tests for flagged features, the Unleash admin token must be set as an environment variable. Never paste or export the token inline in a shell session — this risks exposing it in shell history, logs, or terminal recordings.
Obtain the token from Unleash UI > API Access or from deployment secrets, then set it using a non-echoing prompt or a secret manager:
read -rs CYPRESS_UNLEASH_ADMIN_TOKEN && export CYPRESS_UNLEASH_ADMIN_TOKEN
In CI, inject it via a secret reference (e.g. GitHub Actions secret, Vault, or sealed secret) — never hard-code or echo the value.
Note: When logging token-related errors, use len(token) to confirm presence without exposing the value. Never include the full token in error messages, API responses, or commit history.
Map it in e2e/cypress.config.ts:
env: { UNLEASH_ADMIN_TOKEN: process.env.CYPRESS_UNLEASH_ADMIN_TOKEN }
describe('Flagged Feature', () => {
before(() => {
cy.request({
method: 'POST',
url: 'http://localhost:4242/api/admin/projects/default/features/category.feature.enabled/environments/development/on',
headers: { Authorization: Cypress.env('UNLEASH_ADMIN_TOKEN') },
}).its('status').should('eq', 200);
});
after(() => {
cy.request({
method: 'POST',
url: 'http://localhost:4242/api/admin/projects/default/features/category.feature.enabled/environments/development/off',
headers: { Authorization: Cypress.env('UNLEASH_ADMIN_TOKEN') },
}).its('status').should('eq', 200);
});
it('renders when flag is enabled', () => {
});
});
3. Update Tests
Any component that now calls useWorkspaceFlag or useFlag needs its tests updated.
Mock useWorkspaceFlag:
vi.mock("@/services/queries/use-feature-flags-admin", () => ({
useWorkspaceFlag: () => ({ enabled: true }),
}));
Mock useParams (if newly added):
vi.mock("next/navigation", () => ({
useParams: () => ({ name: "test-project" }),
}));
Mock useFlag:
vi.mock("@/lib/feature-flags", () => ({
useFlag: () => true,
}));
Test both states: confirm the gated element renders when enabled, and is absent when disabled.
4. Verify
cd components/frontend && npx tsc --noEmit
cd components/frontend && npx vitest run
Both must pass with zero errors.
Evaluation Order
When a flag is evaluated at runtime, the platform checks in priority order:
- ConfigMap override (
feature-flag-overrides in workspace namespace) — set by workspace admin via settings UI
- Unleash default — global state from Unleash server
- Code default —
false (fail-closed)
This means a workspace admin can override the global Unleash state in either direction.
Lifecycle
| Phase | Action |
|---|
| Create | Add to flags.json, gate frontend, assign ownership |
| Rollout | Workspace admins enable per-workspace via settings UI |
| GA | Remove flag checks from code, remove from flags.json, create Jira for cleanup tracking |
| Cleanup | Archive flag in Unleash, remove stale ConfigMap overrides |
Treat flags as technical debt. When a feature is fully rolled out, remove the flag — don't leave it permanently enabled.
When a feature reaches GA, create a Jira issue (use /jira-log) to track cleanup in this order:
- Verify feature is fully rolled out (all workspaces enabled or no longer need it)
- Remove
useWorkspaceFlag / useFlag calls from code and deploy
- Remove the flag from
flags.json and deploy (SyncFlags stops recreating it)
- Remove any ConfigMap overrides in workspace namespaces
- Archive and purge the flag in Unleash
Exceptions for long-lived flags: kill switches for graceful degradation, and debug flags for expensive tracing.
Common Mistakes
| Mistake | Fix |
|---|
Using useFlag() in project-scoped page | Use useWorkspaceFlag(projectName, flagName) |
Missing scope:workspace tag | Flag won't appear in workspace settings UI |
| Forgetting to mock hook in tests | Tests crash with "Cannot read properties of null" |
| Reusing a retired flag name | Unleash conflict error on sync — archive + purge first |
| Checking flag in multiple places | Evaluate once, pass result down |
| No test for disabled state | Add test verifying gated element is hidden when flag is off |