Skip to main content

Day-to-day flag operations — editing, toggling, targeting rules, rollouts, overrides, variants, and environment promotion

Last updated October 10, 2026

Managing Flags

This guide covers everything you need to manage feature flags after creation: editing properties, toggling, adding targeting rules, configuring rollouts, setting per-user overrides, working with variants, and promoting flags across environments.

Editing Flag Properties

Via the Dashboard

Open a flag's detail panel and edit any mutable property inline. Editable fields include:

  • Name and description
  • Default value (must still match the flag's type)
  • Tags — add or remove labels
  • Enabled state
  • Variants, targeting rules, and overrides

The flag type (boolean, string, number, object) is immutable after creation.

Via the API

curl -X PATCH "https://flaggr.dev/api/flags/new-checkout-flow?serviceId=svc-abc123&environment=development" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "New Checkout Flow v2",
    "description": "Updated single-page checkout with express options",
    "tags": ["checkout", "q2-launch"]
  }'

Only include the fields you want to change. The response contains the full updated flag object.

Updatable fields

FieldNotes
nameMax 200 characters
descriptionMax 1000 characters
enabledBoolean
defaultValueMust match the flag type
tagsMax 20 tags, each max 50 characters
variantsReplaces the full variant list; weights are whole numbers from 0 to 100
targetingReplaces the full targeting rules list (a live rollout plan's rule stays); rolloutPercentage is a whole number from 0 to 100, and a condition group needs at least one condition
overridesReplaces the full overrides list
isPublicWhether the flag can be evaluated without authentication

What happens on update

Every update triggers the same pipeline as creation:

  1. Validation — Zod schema checks all fields
  2. Approval check — if the environment has requireApproval: true, returns HTTP 202 with a change request instead; a failed environment lookup refuses the update (503)
  3. Storage write — persists the update
  4. Version snapshot — records the new state for rollback
  5. Audit log — captures who changed what
  6. Cache invalidation — clears all related caches
  7. Real-time event — SSE/Pub-Sub clients receive an UPDATED event

Toggling Flags

Toggling is the most common flag operation. It changes the enabled state and, for boolean flags, also updates defaultValue.

Toggle behavior by type

Flag TypeToggle OnToggle Off
booleanenabled: true, defaultValue: trueenabled: false, defaultValue: false
stringenabled: true (defaultValue unchanged)enabled: false (defaultValue unchanged)
numberenabled: true (defaultValue unchanged)enabled: false (defaultValue unchanged)
objectenabled: true (defaultValue unchanged)enabled: false (defaultValue unchanged)

Boolean flags couple enabled and defaultValue together so that if (flag) works intuitively. Non-boolean flags only toggle enabled — their defaultValue stays the same.

Toggle via the API

# Toggle (flip current state)
curl -X POST "https://flaggr.dev/api/flags/new-checkout-flow/toggle?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>"
 
# Explicit set (idempotent — preferred for automation)
curl -X POST "https://flaggr.dev/api/flags/new-checkout-flow/toggle?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'

The explicit { "enabled": true } form is idempotent — calling it twice produces the same result. Use this in scripts and CI/CD to avoid accidental double-toggles.

Disabled flag behavior

When a flag is disabled, all evaluations return:

{ "value": <defaultValue>, "reason": "DISABLED" }

No targeting rules, overrides, or variants are evaluated. The DISABLED reason makes it clear in logs and traces why a specific value was returned.

Targeting Rules

Targeting rules let you return different values for different users based on their attributes.

Evaluation order

Rules are evaluated top-to-bottom. The first rule that matches determines the returned value:

  1. Disabled check — if flag is off, return defaultValue with reason DISABLED
  2. Overrides — exact identity match (highest priority first)
  3. Prerequisites — all prerequisite flags must pass
  4. Mutual exclusion — group constraints checked
  5. Experiments — running experiment variant assignment
  6. Targeting rules — first match wins (with schedule gating)
  7. Variants — weighted random assignment if no rule matched
  8. Default — defaultValue returned

Adding a targeting rule

curl -X PATCH "https://flaggr.dev/api/flags/new-checkout-flow?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "targeting": [
      {
        "id": "beta-users",
        "conditions": [
          { "property": "plan", "operator": "in", "value": ["pro", "enterprise"] }
        ],
        "value": true
      },
      {
        "id": "us-users",
        "conditions": [
          { "property": "country", "operator": "equals", "value": "US" }
        ],
        "rolloutPercentage": 25,
        "value": true
      }
    ]
  }'

This configures two rules:

  1. All Pro/Enterprise users get true
  2. 25% of US users get true (gradual rollout)

Condition operators

OperatorValue TypeDescriptionExample
equalsanyExact matchcountry == "US"
not_equalsanyNot equalenv != "test"
inarrayValue in listplan in ["pro", "enterprise"]
not_inarrayValue not in listrole not_in ["banned"]
containsstringSubstring matchemail contains "@company.com"
starts_withstringPrefix matchname starts_with "admin"
ends_withstringSuffix matchemail ends_with ".edu"
greater_thannumberNumeric comparisonage > 18
less_thannumberNumeric comparisonscore < 100
gtenumberGreater or equalversion >= 2
ltenumberLess or equalattempts <= 3
regexstringRegular expressionemail matches ^.*@company\.com$
beforestring (ISO date)Date comparisoncreatedAt before 2026-01-01
afterstring (ISO date)Date comparisonsignupDate after 2025-06-01
in_cohortstring (cohort ID)Cohort membershipUser is in cohort
not_in_cohortstring (cohort ID)Not in cohortUser is not in cohort

Condition logic (AND / OR)

By default, all conditions within a rule use AND logic — every condition must match. Change this with conditionOperator:

{
  "id": "mobile-or-tablet",
  "conditions": [
    { "property": "device", "operator": "equals", "value": "mobile" },
    { "property": "device", "operator": "equals", "value": "tablet" }
  ],
  "conditionOperator": "or",
  "value": true
}

Condition groups

For complex logic like (A AND B) OR (C AND D), use condition groups:

{
  "id": "complex-targeting",
  "groups": [
    {
      "conditions": [
        { "property": "country", "operator": "equals", "value": "US" },
        { "property": "plan", "operator": "equals", "value": "enterprise" }
      ]
    },
    {
      "conditions": [
        { "property": "country", "operator": "equals", "value": "UK" },
        { "property": "plan", "operator": "in", "value": ["pro", "enterprise"] }
      ]
    }
  ],
  "groupOperator": "or",
  "value": true
}

Conditions within a group use AND logic. Groups are combined with groupOperator (default: "or").

Scheduled rules

Rules can be gated by a time schedule so they only activate during specific windows:

{
  "id": "business-hours-only",
  "conditions": [
    { "property": "region", "operator": "equals", "value": "US" }
  ],
  "schedule": {
    "startDate": "2026-03-01T00:00:00Z",
    "endDate": "2026-06-01T00:00:00Z",
    "recurrence": {
      "type": "weekly",
      "daysOfWeek": [1, 2, 3, 4, 5],
      "startTime": "09:00",
      "endTime": "17:00"
    }
  },
  "value": true
}

Schedule fields:

  • startDate / endDate — overall date range (ISO 8601)
  • recurrence.type — "daily", "weekly", or "cron"
  • recurrence.daysOfWeek — [0-6] where 0 is Sunday (UTC)
  • recurrence.startTime / endTime — "HH:MM" in UTC

All times use UTC for deterministic behavior across serverless instances.

Percentage Rollouts

Gradual rollouts let you expose a flag to a percentage of users. Rollouts use deterministic hashing — the same user always falls in the same bucket.

How rollout hashing works

hash(flagKey + ":rollout:" + targetingKey) % 100

If the hash result is less than rolloutPercentage, the user is in the rollout. This means:

  • Increasing the percentage from 10% to 25% adds new users but never removes existing ones
  • The same user sees the same behavior across requests (sticky)
  • A targetingKey is required. Without one, a rule whose percentage is below 100 never includes the user, except in the @flaggr/sdk 0.4.0 script-tag bundle (sdk.js) evaluating locally (with updateMode: 'stream', bootstrap or remoteConfig): it puts every context without one in the same bucket, so such a rule includes all of them or none (0.5.0 leaves them out)

Configuring a rollout

Add rolloutPercentage to a targeting rule. The percentage applies to the users the rule's conditions match, and a rule with no conditions (an empty conditions array and no groups) matches everyone, so its percentage is the only gate. https://flaggr.dev evaluates rules that way, and so does @flaggr/sdk evaluating locally from 0.5.0, but a data plane from before Flaggr 0.5.0 (https://api.flaggr.dev until it is updated) and local evaluation before 0.5.0 match a rule with no conditions to nobody. To roll out to everyone on every version, give the rule a condition every user meets:

{
  "id": "gradual-release",
  "conditions": [
    { "property": "targetingKey", "operator": "not_equals", "value": "__flaggr_rollout_match_all__" }
  ],
  "rolloutPercentage": 10,
  "value": true
}

A real targetingKey never equals __flaggr_rollout_match_all__, and a context without one doesn't either, so this condition matches every user and the rollout percentage is the only gate. Rollout plans write the same condition when a stage is open to everyone, and the console shows it as Everyone. Once every evaluator you use matches a rule with no conditions to everyone, "conditions": [] does the same. In the console, Add rule → Match: Everyone makes a rule with no conditions; its percentage field then reads Percentage of users. Percentages are whole numbers from 0 to 100 (the API refuses others with 400). To give everyone the same value you need no rule at all: users no rule matches get the flag's defaultValue (or a weighted variant, when the flag has variants and the context has a targetingKey).

To combine targeting with rollout:

{
  "id": "us-gradual",
  "conditions": [
    { "property": "country", "operator": "equals", "value": "US" }
  ],
  "rolloutPercentage": 50,
  "value": true
}

This targets 50% of US users.

Ramping up a rollout

Increase rolloutPercentage gradually: 5% → 10% → 25% → 50% → 100%. At each step, monitor health metrics and error rates before proceeding.

Per-User Overrides

Overrides let you force a specific value for individual users, bypassing all targeting rules and rollouts.

Adding overrides

curl -X PATCH "https://flaggr.dev/api/flags/new-checkout-flow?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "overrides": [
      {
        "name": "QA testers",
        "description": "Always-on for QA team",
        "identifiers": ["user-qa-1", "user-qa-2", "user-qa-3"],
        "value": true,
        "priority": 10
      },
      {
        "name": "Bug report user",
        "description": "Disabled for user reporting layout issues",
        "identifiers": ["user-12345"],
        "value": false,
        "priority": 5
      }
    ]
  }'

You can also send overrides when you create the flag (POST /api/flags, or an item of POST /api/flags/bulk); they are stored and served exactly as when added with PATCH.

Override fields

FieldTypeDescription
namestringHuman-readable label (required, max 100 chars)
descriptionstringWhy this override exists (max 500 chars)
identifiersstring[]Matching targetingKey values (1-100 identifiers)
valueanyThe value to return (must match flag type)
prioritynumberHigher priority wins (0-1000, default 1)

Override evaluation

Overrides are checked before all targeting rules. They are sorted by priority (highest first) and matched against the evaluation context's targetingKey. The first matching override wins.

Common use cases

  • QA testing: Force a feature on/off for testers in production
  • VIP access: Give early access to specific accounts
  • Bug workaround: Disable a broken feature for affected users
  • Demo accounts: Ensure demo users see specific behavior

Working with Variants

Variants enable A/B testing by returning different values to different users with configurable weights.

Adding variants

curl -X PATCH "https://flaggr.dev/api/flags/pricing-layout?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "variants": [
      { "name": "control", "value": "grid", "weight": 50 },
      { "name": "treatment_a", "value": "comparison-table", "weight": 30 },
      { "name": "treatment_b", "value": "single-column", "weight": 20 }
    ]
  }'

How variant assignment works

When a flag has variants and a targetingKey is provided:

hash(flagKey + targetingKey) % 100

The hash falls into a weight bucket:

  • 0-49 → control (weight 50)
  • 50-79 → treatment_a (weight 30)
  • 80-99 → treatment_b (weight 20)

Assignment is sticky — the same user always gets the same variant. Weights are whole-number percentages (0 to 100) and should sum to 100: a user whose bucket is past the last weight gets the first variant.

Combining variants with targeting

A targeting rule can return a specific variant:

{
  "id": "internal-always-treatment",
  "conditions": [
    { "property": "email", "operator": "ends_with", "value": "@company.com" }
  ],
  "variant": "treatment_a",
  "value": "comparison-table"
}

This forces all internal users to see treatment_a, while external users get random variant assignment based on weights. The rule serves its own value, so it carries the variant's value; the console's Return Variant and MCP add_targeting_rule with variant fill it in for you.

Variant evaluation response

{
  "value": "comparison-table",
  "variant": "treatment_a",
  "reason": "VARIANT"
}

The variant field tells your code which variant was assigned, separate from the value.

Environment Promotion

Flags exist independently in each environment. After testing a flag configuration in development, you can promote it to staging or production.

Promote via the API

curl -X POST "https://flaggr.dev/api/projects/proj-123/environments/development/promote" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "targetEnvironment": "staging",
    "serviceId": "svc-abc123",
    "flagKeys": ["new-checkout-flow", "pricing-layout"]
  }'

Promotion copies the fields listed in fields (default: enabled, defaultValue, targetingRules and variants) from the source environment to the target. A flag that doesn't exist in the target yet is created there with the source's settings, including its enabled state. To keep an existing target flag's on/off state, leave enabled out of fields.

Rollout plans own their rollout-<planId> rule in their own environment, so promotion never copies the source's rollout rules and keeps the target flag's own. A flag that a live rollout plan manages in the target isn't promoted: it is listed in refused (code ROLLOUT_IN_PROGRESS, or BUSY when another change holds its lock) and the other flags still are; the call answers 409 when nothing was promoted.

Promotion never copies a default, variants or rules onto a target flag of another type (a string flag in staging and a boolean flag in production, say): that flag is listed in refused with code FLAG_TYPE_MISMATCH and the others are still promoted. Promote only enabled, which copies across types, or give the flag one type in both environments. A target flag that the evaluation API couldn't load after the copy is listed with code FLAG_NOT_SERVABLE. See Refused Flag Writes.

Promotion needs read access to the service in the source environment, and write access in the target unless a change request is opened (see below).

Typical promotion flow

development → staging → production
  1. Create and configure the flag in development
  2. Test thoroughly with targeting rules and overrides
  3. Promote to staging for QA validation
  4. Promote to production with a gradual rollout

Each environment is independent — changes in development don't affect staging or production.

Protected environments

When an environment has protectedEnvironment: true and requireApproval: true, no flag change writes to it directly. PATCH /api/flags/{key}, POST /api/flags/{key}/toggle and POST /api/flags/{key}/rollback/{version} return 202 Accepted with a change request; the change takes effect once a project admin approves and applies it. Reviewing and applying need admin rights on that service and environment (access overrides apply), and the request can only change flags of its own project.

Changes a change request can't carry are refused with 409 and code: "APPROVAL_REQUIRED": creating or deleting a flag; bulk create, update and delete items and import items (refused one by one while the rest of the batch goes ahead); RUM optimization; site guardrail mitigation (reported as a blocked mitigation); and bandit reallocation. The circuit breaker restores nothing there and reports approvalRequired: true. If the environment can't be looked up, the change is refused with 503 and code: "APPROVAL_CHECK_UNAVAILABLE"; retry. Callers who reach the shared demo or showcase only through its public floor can't open change requests (403). This is common for production environments.

Promoting into a protected environment opens a promotion change request (202). Callers who reach the project only through the shared demo/showcase can't open one (403, code APPROVAL_REQUIRED). If the environment check can't run, the promotion is refused with 503 APPROVAL_CHECK_UNAVAILABLE and nothing is written.

Bulk Operations

For managing multiple flags at once, use the bulk API endpoints.

Bulk create

curl -X POST "https://flaggr.dev/api/flags/bulk" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "flags": [
      { "key": "feature-a", "name": "Feature A", "type": "boolean", "defaultValue": false, "serviceId": "svc-abc123" },
      { "key": "feature-b", "name": "Feature B", "type": "boolean", "defaultValue": false, "serviceId": "svc-abc123" }
    ]
  }'

Bulk update

curl -X PATCH "https://flaggr.dev/api/flags/bulk" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "flags": [
      { "key": "feature-a", "serviceId": "svc-abc123", "updates": { "enabled": true } },
      { "key": "feature-b", "serviceId": "svc-abc123", "updates": { "enabled": true } }
    ]
  }'

Bulk operations support up to 100 flags per request.

Deleting Flags

Via the API

curl -X DELETE "https://flaggr.dev/api/flags/old-checkout-flow?serviceId=svc-abc123&environment=production" \
  -H "Authorization: Bearer <token>"

Deletion requires delete permission on the project (Owner or Admin role).

Before deleting

  1. Check impact — use the impact analysis endpoint to see current evaluation volume
  2. Disable first — disable the flag and wait for a release cycle to ensure no code depends on it
  3. Remove from code — clean up all evaluateFlag("old-checkout-flow", ...) calls
  4. Delete — remove the flag from each environment independently

Deletion is permanent — use version history to review previous configurations if needed.

Evaluation Reasons

Every flag evaluation returns a reason field explaining why a specific value was chosen:

ReasonDescription
DISABLEDFlag is off — returned defaultValue
OVERRIDEMatched a per-user override
PREREQUISITE_FAILEDA prerequisite flag didn't meet its expected value
MUTUAL_EXCLUSIONBlocked by a mutual exclusion group constraint
EXPERIMENTAssigned a variant by a running experiment
TARGETING_MATCHMatched a targeting rule
VARIANTAssigned a variant by weighted distribution (no targeting rule matched)
DEFAULTNo rules matched — returned defaultValue
NOT_FOUNDFlag doesn't exist in this environment
ERRORAn error occurred during evaluation

Use these reasons in logging and debugging to understand flag behavior.