---
name: feature-flag-workflow
description: Run feature flags end to end without building up flag debt: classify the flag, name it well, roll it out on a safe ladder, flip it as a kill switch, and retire it on schedule. Use when adding a flag, reviewing one, ramping a release, or cleaning up dead flags.
---

# Feature flag workflow

## Purpose

Feature flags decouple deploying code from releasing it. Done sloppily they
become dead conditionals, surprise resurrections, and branches nobody dares
touch. This skill is the full discipline: classify, name, launch, ramp, and
retire every flag on purpose. The core rule: a release flag is an unpaid loan,
Total Cost = Cost of Feature + Cost of Flag Cleanup. Ship the cleanup the same
way you ship the feature.

## Workflow

### 1. Classify the flag before creating it

Use Martin Fowler's taxonomy (https://martinfowler.com/articles/feature-toggles.html).
The type decides the lifespan, and the lifespan is what rots when misclassified.

| Type | Purpose | Lifespan | Example |
|---|---|---|---|
| `release` | Ship dark code, roll out gradually | Days to weeks. Retire after full launch | `release.checkout.v2-flow` |
| `experiment` | A/B test a cohort | Hours to weeks. Retire when the test concludes | `experiment.onboarding.simplified-flow` |
| `ops` | Kill switch, circuit breaker, degrade mode | Long-lived on purpose | `ops.payments.stripe-integration` |
| `permission` | Gate by plan, role, or tenant | Lives with the product | `permission.reporting.advanced-analytics` |

The classic error mode is treating a release toggle as a permission toggle:
"we never turned it off, so now it's permanent." Flag debt starts there.

### 2. Name it so an operator can find and flip it at 3am

Convention: `<type>.<domain>.<feature>`, all lowercase, dots or dashes as
separators. Examples: `release.invoicing.pdf-export`, `ops.search.circuit-breaker`.
Never reuse a flag name, even after retirement. A recycled name makes history
unreadable and old config ambiguous.

### 3. Create it born-off, owned, and expiring

Every new flag gets four things at creation:

- **Safe default**: off is the safe value. Code always reads with an explicit
  default, so a flag-service outage degrades to the safe path, never a crash.
- **Owner**: the team or person who flips it and deletes it.
- **Expiry date**: only for `release` and `experiment` flags. A flag with no
  expiry date is a defect waiting to happen.
- **Cleanup ticket**: opened the same day the flag is created, scheduled a
  fixed period after launch (commonly 2 weeks). "Done" on the feature PR means
  the cleanup ticket exists.

### 4. Roll out on a ladder, not a cliff

Never go 0 to 100 in one flip. The standard ladder, each step held while
metrics settle:

```
internal users -> 1% -> 10% -> 50% -> 100% (bake briefly) -> retire
```

Rules for each ramp:

- **Sticky bucketing**: the same user must land in the same bucket every
  request, usually by hashing a stable user or account id. Non-sticky rollouts
  make metrics meaningless and users see a flickering product.
- **Watch both kinds of signal**: errors and latency at 1%, conversion and
  business metrics at 10-50%.
- **Automatic brakes**: tie the rollout to alerting so an error or latency
  spike drops the percentage back to zero without waiting for a human.
- **Evaluate once per request**: read the flag once and pass the result
  through. Re-checking mid-flow can return different values if targeting rules
  change underneath you.

### 5. Treat flips as production changes

A flag flip is a production config change and gets treated like one: log who
changed it, when, old value, new value. Audit trail is non-negotiable once
flags control anything beyond cosmetics.

Two flip paths, used deliberately:

- **Dashboard flip**: find the flag, change the value. Live in seconds, no
  redeploy. This is the kill-switch path. During an incident, this is the one.
- **Config-as-code flip**: edit the declaration, open a PR, deploy. Slower but
  versioned and reviewed. Use when the rule change itself should be recorded.

Rule of thumb: need it live now -> dashboard; want it recorded -> config.

**Kill switches** (`ops` flags) are the permanent exception to "flags are
temporary". Every feature with external dependencies (third-party APIs, fragile
services) gets one. A kill switch removed because nobody used it last quarter
is a future outage.

### 6. Retire it on schedule

Detection rules. A flag is dead when any of these hold:

- `release` flag at 100% rollout for more than ~14 days
- `experiment` flag past its end date
- `release` flag older than ~30 days with no removal ticket on file

Staged retirement (borrowed from teams that paid down real flag debt):

1. Confirm the intended final value in every environment.
2. Freeze targeting changes; remove unexpected overrides.
3. Make the final behavior unconditional in code and deploy it.
4. Verify logs, metrics, and client behavior for a short observation period.
5. Remove the flag, the dead branch, stale tests, and the dashboard entry in
   the same release.

Before deleting, verify the flag is truly dead. A flag that looks inactive in
the control plane may still be evaluated by an old client, a scheduled job, or
a fallback path. Also scan for dynamic evaluation (string interpolation,
reflection) before declaring a flag unused. Flag removal is a design decision:
do not remove flags you cannot fully trace.

## Output Contract

When asked to run this skill, produce:

1. **Flag card** for each new flag: name (per convention), type, owner,
   expiry date, safe default, and the cleanup-ticket reference. Use
   `bin/flag_inventory.py --card <name> --type <type> --owner <team> --expiry <YYYY-MM-DD>`
   to generate a consistent card.
2. **Rollout plan**: the ladder steps with hold durations, the signals watched
   at each step, and who is allowed to flip.
3. **Retirement plan**: detection criteria for this flag plus the staged
   removal steps above.
4. For reviews: `bin/flag_inventory.py --scan <repo-path>` output showing every
   flag read found in code, naming-convention violations, and candidates for
   retirement.

## Operating Rules

- Off is the safe default. Always.
- No permanent flags except `ops` and `permission` types.
- Never reuse a flag name.
- Config toggles are not feature flags. `max_connections` is a permanent
  operational setting; it is not a flag and gets no expiry date.
- Environment checks (`if Rails.env.production?`) and permission checks
  (`if user.admin?`) are not feature flags either.
- A flag that appears unused may be evaluated dynamically. Check string
  interpolation and reflection before calling it dead.
- Never gate security-critical behavior behind a rollout percentage.
- Do not recommend removing a flag you cannot fully trace.
- Keep the count of live release flags on a team dashboard, alongside open
  bugs. If the number only grows, the discipline is broken.
