mcpField
F
flag-targeting

flag-targeting

github.com/growthbookDevelopment2540 installsUpdated 4w ago
Repository

About flag-targeting

A skill for adding, editing, or removing targeting rules on existing GrowthBook feature flags. It supports force and percentage rollout rules with attribute conditions, saved groups, and prerequisites; changes remain drafts until published.

Key features

  1. 01Add, edit, and remove rules
  2. 02Support force and percentage rollout
  3. 03Combine conditions, saved groups, and prerequisites
  4. 04Stage changes as draft revisions

Use cases

  1. 01Serve a flag value to users matching attributes
  2. 02Roll out a feature to a percentage of users
  3. 03Target saved groups or depend on another flag
  4. 04Adjust an existing rule's scope or conditions

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/growthbook/skills --skill flag-targeting

After installing, type/flag-targetingin Claude Code to confirm it loaded.

Details

From the project README

flag-targeting

Add, edit, or remove targeting rules on an existing GrowthBook feature flag. Handles force (serve a specific value to matched users) and rollout (serve a value to a random percentage of users) rule types, with full support for conditions, saved groups, and rule-level prerequisites.

Every change goes through a draft revision and requires publishing. For publishing, use flag-publish — it handles approval-required and merge-conflict failure modes.

All API calls go through the bundled helper: ${CLAUDE_PLUGIN_ROOT}/scripts/gb-call. It needs GB_API_KEY set in env or written to ~/.config/growthbook/.env by /growthbook:gb-setup.

Required inputs

Collect before starting:

  • Flag ID — kebab-case key. Use flag-search to resolve from a description.
  • Action — add, edit, or remove. Infer from the user's request; confirm before mutating.

Per-action inputs

add
  • Rule type — force (specific users) or rollout (percentage). Infer from wording: "X% of users" → rollout; "for users matching Y" → force.
  • Value — what the rule serves when matched. Must match the flag's valueType, serialized as a string.
  • Scope — allEnvironments: true or environments: [<env-ids>]. Ask if not specified.
  • Conditions (optional) — saved group, attribute condition, or prerequisite on another flag (see conditions decision tree below).
  • For rollout rules — coverage (0–1) and hashAttribute (required when coverage < 1).
  • Description (optional but recommended).
edit
  • Rule ID — resolved interactively by showing a numbered list (step 3b). User picks by number.
  • Patch fields — only the fields the user wants to change.
remove
  • Rule ID — resolved the same way as edit.

Workflow

- [ ] 1. Fetch flag and current state
- [ ] 2. Confirm the action (add / edit / remove)
- [ ] 3a. Add path
- [ ] 3b. Edit path
- [ ] 3c. Remove path
- [ ] 4. Hand off to flag-publish
1. Fetch flag and current state
gb-call GET /api/v2/features/<flag-id>

Capture: valueType, environmentSettings keys (available env IDs), rules array (full list with IDs, types, scope, enabled state). If 404, halt: "no flag with id <flag-id>." Suggest flag-search.

2. Confirm the action

If the user's request is ambiguous, ask before mutating.

3a. Add path

New rules append to the bottom of the rules array. Rules evaluate top-to-bottom; first match wins. If the flag already has rules, surface this:

"The flag has <N> existing rule(s); this new rule will be evaluated last. If it needs priority over an existing rule, reorder via flag-rules after adding."

Pre-validate value against valueType — the API doesn't catch mismatches at write time:

  • boolean → must be "true" or "false"
  • number → must parse as a number
  • json → must be valid JSON
  • string → any non-empty string

Resolve scope. Confirm the environment IDs against environmentSettings. Set either allEnvironments: true or environments: [...] — never both.

Fetch available attributes — do this before asking the user to describe any condition. If the flag has a project, pass it as projectId to get only relevant attributes (org-wide + project-scoped); omit it for org-wide flags:

# Flag has a project:
gb-call GET '/api/v1/attributes?projectId=<flag-project-id>'

# Flag is org-wide (no project):
gb-call GET /api/v1/attributes

Surface the returned list to the user grouped by type, with notable metadata called out:

Available targeting attributes for this flag:
  id            (string, hashAttribute)
  country       (string, format: isoCountryCode)
  app_version   (string, format: version)
  plan          (enum: free|pro|enterprise)
  is_employee   (boolean)

If no attributes exist (or none are in scope), warn: "No targeting attributes are registered for this project. Add them under Settings → Attributes before targeting."

Resolve hashAttribute for rollout rules (required when coverage < 1): From the filtered attribute list, prefer hashAttribute: true entries. Default to id for "by user"; pick the company-scoped attribute for "by company."

A rule has three separate targeting properties that can be combined freely. Determine which ones apply, then build all relevant fields before posting.

condition — attribute-based targeting (MongoDB-style JSON string)

Targets users based on attributes passed into the SDK. Pick attributes from the filtered list fetched above. If the user's description doesn't map clearly to an available attribute, show the list and ask them to pick. Refuse any attribute name not in the filtered list — unregistered attributes silently never match at SDK evaluation time.

Serialize the condition object as a JSON string in the payload.

CategoryOperatorsMeaning
Equality$eq, $ne (or bare key/value for eq)=, ≠
Comparison$lt, $lte, $gt, $gte<, ≤, >, ≥
Semantic version$veq, $vne, $vlt, $vlte, $vgt, $vgteVersion-aware comparison — "1.0.10" > "1.0.9". Use on format: version attributes.
Set membership$in, $ninis any of, is none of (case-sensitive)
Set membership (case-insensitive)$ini, $niniis any of / none of — ignores case
String contains$includes, $notIncludesstring or array contains / does not contain value
Regex$regex, $notRegexmatches / does not match pattern (case-sensitive)
Regex (case-insensitive)$regexi, $notRegexisame, ignores case
Existence$exists, $notExistsis not NULL / is NULL
Emptiness$empty, $notEmptystring or array is empty / not empty
Boolean$true, $falseattribute is truthy / falsy
Type$typeJS type equals value (e.g. "string", "number")
Array$elemMatch, $all, $alli, $sizeelement matches condition; all values present ($alli = case-insensitive); length comparison
Saved group (raw)$inGroup, $notInGroupin / not in a saved group by ID — prefer the savedGroups rule field instead
Logical$or, $and, $nor, $nottop-level keys are ANDed; use these for OR / negation

Key rules:

  • Multiple top-level keys are ANDed: {"country": "US", "plan": "pro"} requires both.
  • String comparisons are case-sensitive by default — use $ini/$regexi variants when needed.
  • Conditions are evaluated client-side by the SDK — attribute values never reach GrowthBook servers.
  • Only use operators from this table. Unlisted MongoDB operators (e.g. $where, $expr) silently never match.

Examples:

{"country": "US"}
{"country": {"$ini": ["us", "ca", "gb"]}}
{"plan": {"$ne": "free"}}
{"appVersion": {"$vgte": "3.0.0"}}
{"age": {"$gte": 18, "$lt": 65}}
{"email": {"$regexi": "@acme\\.com$"}}
{"$or": [{"country": "US"}, {"beta": true}]}
{"tags": {"$includes": "power-user"}}
{"company": {"$exists": true}, "plan": {"$in": ["pro", "enterprise"]}}

savedGroups — saved group targeting (separate rule field)

Targets users who belong to a pre-defined saved group. Fetch available groups:

gb-call GET /api/v1/saved-groups

Build the field as an array of group references:

"savedGroups": [
  { "ids": ["<sg-id>"], "match": "all" }
]
  • ids — array of saved group IDs to reference
  • match: "all" — user must be in all listed groups; "any" — user must be in at least one

Use saved groups for named populations managed outside the flag ("beta testers", "internal users", "enterprise accounts"). Prefer this over hand-writing $inGroup in the condition string.


prerequisites — rule-level prerequisite targeting (separate rule field)

Gates this rule on the evaluated value of another feature flag. If the prerequisite condition fails for a user, this rule is skipped (the next rule in order is evaluated instead — distinct from feature-level prerequisites which skip the entire flag).

"prerequisites": [
  { "id": "<flag-id>", "condition": "{\"value\": true}" }
]

The condition string evaluates against { "value": <the prerequisite flag's evaluated result> }. value is the only valid top-level key.

The two conditions that cover 99% of cases:

GoalCondition
Boolean flag is on{"value": true}
Boolean flag is off{"value": false}
Non-boolean flag is live (returning any value){"value": {"$exists": true}}
Non-boolean flag is not live{"value": {"$exists": false}}

For anything more specific (e.g. string flag equals a particular variant), the full operator table from the condition section above applies — but ask the user to confirm before writing complex prerequisite conditions.


Combining all three:

All three properties are ANDed together. A rule with all three set fires only when the user is in the saved group AND the attribute condition matches AND all prerequisite flags pass:

{
  "condition": "{\"country\": \"US\"}",
  "savedGroups": [{ "ids": ["sg_beta"], "match": "all" }],
  "prerequisites": [{ "id": "new-checkout", "condition": "{\"value\": true}" }]
}

Ambiguous cases ("VIP customers", "enterprise users") — ask: is this a named group in GrowthBook (saved group), an attribute on the user record (condition), or both?

User saysField to use
"Turn it on for our beta testers"savedGroups
"Users in the US"condition on country attribute
"iOS users on version 5.2 or higher"condition: {"platform": "ios", "appVersion": {"$vgte": "5.2"}}
"Only when the new-checkout flag is on"prerequisites
"Enterprise users"Ask — saved group or {"plan": "enterprise"} condition?

Build and POST the payload:

{
  "rule": {
    "type": "force",
    "value": "<string>",
    "description": "<optional>",
    "enabled": true,
    "allEnvironments": false,
    "environments": ["production"],
    "condition": "<optional JSON string>",
    "savedGroups": [{ "ids": ["<sg-id>"], "match": "all" }],
    "prerequisites": [{ "id": "<flag-id>", "condition": "{\"value\": true}" }]
  }
}

For rollout: swap type: "rollout", add coverage and hashAttribute. Omit empty arrays/strings.

echo '<payload>' | gb-call POST /api/v2/features/<flag-id>/revisions/new/rules -

Capture revision.version.

3b. Edit path

Show the rules as a numbered list:

Rules on `<flag-id>`:
  1. [force]    all envs     value="true"    "Beta testers" (saved group)
  2. [rollout]  production   10%             hash=id
  3. [force]    staging      value="false"   "Kill switch"

User picks by number. Surface current values; ask which fields to change.

Empty-patch guard: if the proposed changes match current values verbatim, halt — "no changes to apply." A no-op draft burns rate-limit budget and can invalidate previously-granted approvals via resetReviewOnChange.

Rule-type behavior on edit:

  • Explicit type changes are server-rejected. To convert a force rule to an experiment-ref rule: remove and re-add (3c then 3a or flag-experiment).
  • force ↔ rollout auto-flips based on effective coverage. Patching coverage: 0.25 onto a force rule silently converts it to rollout (also requires hashAttribute). Report the type transition in the summary.

experiment-ref edit rules:

  • Server-rejected patches: value, coverage, controlValue. Halt early with explanation.
  • Warn-and-confirm patches: experimentId, variations. API allows them but causes silent flag/experiment drift. Require explicit confirmation: "Changing experimentId/variations directly can cause the flag rule and experiment to drift. The experiment is the source of truth. Are you sure?"
  • Safe to edit: enabled, condition, savedGroups, prerequisites, scope, description.

Scope subtlety: when changing scope, always send both allEnvironments and environments together. Sending only environments without allEnvironments causes the server to infer allEnvironments: false, silently narrowing scope.

echo '<patch>' | gb-call PUT /api/v2/features/<flag-id>/revisions/new/rules/<rule-id> -

Capture revision.version.

3c. Remove path

Show the numbered list. Confirm:

"Remove rule <N> (<type>, <scope>, <summary>) from <flag-id>? This goes into a draft and only takes effect after publishing."

gb-call DELETE /api/v2/features/<flag-id>/revisions/new/rules/<rule-id>

For experiment-ref removal: "The linked experiment is not affected by removing this rule."

Capture revision.version.

4. Hand off to flag-publish

After any mutation, ask: "Publish this change now, or leave it as a draft?"

Hand off to flag-publish. It handles:

  • Approval-required (400) — offer review flow, org-wide bypass, per-token bypass
  • Merge conflict (409) — show conflict fields, collect overwrite/discard decisions, rebase

Guardrails

  • Draft version threading. If a version number is already in context from a previous write skill in this session, use it explicitly (e.g. .../revisions/42/rules) instead of new. This keeps all changes in the same draft across chained skills. Fall back to new when starting fresh — it auto-creates or reuses the most recently updated open draft via resolveOrCreateRevision.
  • Rule ID is a UUID (fr_...), not a position number. Always resolve from the rules array — never guess.
  • force vs rollout is mostly cosmetic server-side. A force rule with coverage < 1 is effectively a rollout. Default to force unless the user says "percentage" or specifies coverage; the server auto-flips.
  • coverage < 1 requires hashAttribute. Validate before POSTing.
  • Scope: exactly one of allEnvironments: true or environments: [...]. Never both.
  • Always fetch attributes before asking for a condition. Use ?projectId=<id> when the flag is project-scoped so the server returns only relevant attributes. Surface the list upfront. Never accept an attribute name the user provides without confirming it's in the returned list — unregistered attributes silently never match at SDK evaluation time.
  • Conditions are JSON strings. Validate they're parseable. Prefer Saved Groups over hand-written conditions — they're reusable and managed.
  • valueType mismatch is a footgun. The v2 rule-add handler doesn't validate at write time. Always pre-validate client-side.
  • Refuse empty patches. A no-op revision update burns rate limit and can reset review approvals.
  • New rules append to the bottom. First match wins. Surface evaluation order concerns before posting.
  • Self-approval blocked. Don't attempt submit-review after request-review.
  • Environment toggles are handled by flag-toggle, not this skill. This skill does not touch env-level enable/disable.

Endpoints used

  • GET /api/v2/features/:id — fetch flag state, current rules, env list, valueType
  • GET /api/v1/attributes — fetch attributes; pass ?projectId=<id> when the flag is project-scoped; surface upfront before building any condition; pick hashAttribute for rollouts
  • GET /api/v1/saved-groups — resolve saved groups for targeting
  • POST /api/v2/features/:id/revisions/new/rules — add rule
  • PUT /api/v2/features/:id/revisions/new/rules/:ruleId — edit rule
  • DELETE /api/v2/features/:id/revisions/new/rules/:ruleId — remove rule

Handoffs

  • flag-toggle — for environment-level enable/disable (kill switch)
  • flag-rules — for reordering rules or routing to other rule types
  • flag-experiment — for adding experiment-ref or inline experiment rules
  • flag-ramp — to progressively increase coverage on a rollout rule over time
  • flag-monitoring — to add guardrail metric monitoring to a rollout
  • flag-prerequisites — for feature-level prerequisite gates
  • flag-search — to resolve a flag ID from a description
  • flag-publish — to publish the draft (handles approval and merge conflicts)

Comments

Markdown supported