Generated banner for Instrument Feature Flags
Generated logo for Instrument Feature Flags

Instrument Feature Flags

PostHogOfficialCreated by PostHog

  • Skill
  • Tool integration
  • Data and AI
  • MIT
  • Low risk

From the author

Add PostHog feature flags to gate new functionality. Use after implementing features or reviewing PRs to ensure safe rollouts with feature flag controls. Also handles initial PostHog SDK setup if not yet installed.
— the description by PostHog

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to instrumenting PostHog feature flags within your codebase. It handles the end-to-end process, including detecting your platform, setting up the PostHog SDK if necessary, configuring environment variables, and implementing the code required to gate features. It emphasizes best practices such as using environment variables for configuration, preferring server-side evaluation to prevent UI flicker, and ensuring minimal, non-intrusive code changes.

What it does

  • Detects your project's platform and language automatically.
  • Initializes the PostHog SDK if it is not already present in the project.
  • Configures required environment variables securely without hardcoding.
  • Provides platform-specific code patterns for implementing feature flag checks.
  • Guides the integration of boolean and multivariate feature flags.
  • Ensures PostHog configuration does not break application boot or runtime stability.

When to use it

  • When you need to gate new functionality for controlled releases.
  • After implementing a new feature that requires a safe rollout strategy.
  • When reviewing pull requests that introduce features needing toggle controls.
  • When you need to set up PostHog instrumentation in a new or existing project.

Advantages

  • Supports a wide range of languages, frameworks, and platforms.
  • Promotes safe, production-ready instrumentation patterns.
  • Prevents common issues like UI flicker through server-side evaluation.
  • Maintains clean code by avoiding hardcoded configuration keys.

Usage examples

“Add a PostHog feature flag to gate the new checkout button in my React app.”
The agent will analyze your React project, verify PostHog setup, and provide the code snippet using posthog.isFeatureEnabled to conditionally render the button.
“I need to instrument a new feature flag for my Node.js backend.”
The agent will detect the Node.js environment, ensure the SDK is initialized, and guide you through adding the flag check logic to your server-side code.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/posthog/instrument-feature-flags/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip instrument-feature-flags.zip -d .agents/skills

Documentation by PostHog

Add PostHog feature flags

Use this skill to add PostHog feature flags that gate new or changed functionality. Use it after implementing features or reviewing PRs to ensure safe rollouts with feature flag controls. If PostHog is not yet installed, this skill also covers initial SDK setup. Supports any platform or language.

Supported platforms: React, Next.js, React Native, Web (JavaScript), Node.js, Python, PHP, Ruby, Go, Java, Rust, .NET, Elixir, Android, iOS, Flutter, and the REST API.

Instructions

Follow these steps IN ORDER:

STEP 1: Analyze the codebase and detect the platform.

Look for dependency files (package.json, pubspec.yaml, Podfile, Package.swift, requirements.txt, go.mod, Gemfile, composer.json, mix.exs, etc.) to determine the language and framework.

Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb, go.sum, pubspec.lock, Podfile.lock, Package.resolved, mix.lock) to determine the package manager.

  • Check for existing PostHog setup (SDK initialization, env vars, etc.). If PostHog is already installed and initialized, skip to STEP 3.

STEP 2: Research instrumentation. (Skip if PostHog is already set up.) 2.1. Find the reference file below that matches the detected platform — it is the source of truth for SDK initialization, flag evaluation methods, and framework-specific patterns. Read it now. 2.2. If no reference matches, fall back to your general knowledge and web search. Use posthog.com/docs as the primary search source.

STEP 3: Create or find the feature flag.

  • Check if a PostHog MCP server is connected. If available, use its tools to search for an existing feature flag the user wants to instrument, or create a new one.

  • If no MCP server is available, instruct the user to create the flag in the PostHog dashboard.

STEP 4: Plan release conditions.

  • Determine the rollout strategy (percentage rollout, user targeting, group targeting, etc.).

  • Plan how the feature flag will gate the new functionality in code.

STEP 5: Instrument the feature.

  • Add the feature flag code following the platform-specific reference patterns.

  • Use server-side evaluation when possible to avoid UI flicker.

  • Do not alter the fundamental architecture of existing files. Make additions minimal and targeted.

  • You must read a file immediately before attempting to write it.

STEP 6: Set up environment variables.

  • Check if the project already has PostHog environment variables configured (e.g. in .env, .env.local, or framework-specific env files). If valid values already exist, skip this step.

  • If the PostHog project token is missing, use the PostHog MCP server's projects-get tool to retrieve the project's api_token. If multiple projects are returned, ask the user which project to use. If the MCP server is not connected or not authenticated, ask the user for their PostHog project token instead.

  • For the PostHog host URL: check the projects-get MCP response for a region field — US maps to https://us.i.posthog.com, EU maps to https://eu.i.posthog.com. If the region is not available from the MCP response or from existing project configuration, ask the user: "Are you on PostHog US Cloud or EU Cloud?" Do not assume US Cloud.

  • Write these values to the appropriate env file using the framework's naming convention.

  • Reference these environment variables in code instead of hardcoding them.

Reference files

  • references/react.md - React feature flags installation

  • references/react-native.md - React native feature flags installation

  • references/web.md - Web feature flags installation

  • references/nodejs.md - Node.js feature flags installation

  • references/python.md - Python feature flags installation

  • references/django.md - Django

  • references/flask.md - Flask

  • references/php.md - Php feature flags installation

  • references/laravel.md - Laravel

  • references/ruby.md - Ruby feature flags installation

  • references/ruby-on-rails.md - Ruby on rails

  • references/go.md - Go feature flags installation

  • references/java.md - Java feature flags installation

  • references/rust.md - Rust feature flags installation

  • references/dotnet.md - .net feature flags installation

  • references/dotnet.md - .net

  • references/elixir.md - Elixir feature flags installation

  • references/android.md - Android feature flags installation

  • references/ios.md - Ios feature flags installation

  • references/usage.md - Ios SDK usage

  • references/flutter.md - Flutter feature flags installation

  • references/api.md - API feature flags installation

  • references/next-js.md - Next.js

  • references/adding-feature-flag-code.md - Adding feature flag code

  • references/best-practices.md - Best practices for production-ready flags

  • references/COMMANDMENTS.md - Framework-specific rules the integration must follow

Each platform reference contains SDK-specific installation, flag evaluation, and code examples. Find the one matching the user's stack. If unlisted, use the API reference as a fallback.

Key principles

  • Environment variables: Always use environment variables for PostHog keys. Never hardcode them.

  • Minimal changes: Add feature flag code alongside existing logic. Don't replace or restructure existing code.

  • Boolean flags first: Default to boolean flag checks unless the user specifically asks for multivariate flags.

  • Server-side when possible: Prefer server-side flag evaluation to avoid UI flicker.

Files

references/best-practices.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Best practices for production-ready flags

Canonical URL: https://posthog.com/docs/feature-flags/best-practices

On this page

  • Checklist

  • Flags are pure functions

  • Unexpected results are almost always input problems

  • Resolve identity before evaluating flags

  • Evaluation architecture

  • Flag hygiene

  • Further reading

Checklist

  • Call identify() before evaluating flags – the hash uses the wrong ID otherwise. This is the most common input problem.

  • Evaluate flags server-side with local evaluation – explicit inputs, your data right there, no workarounds.

  • Bootstrap client-side flags – client-side evaluation is async. Bootstrap to eliminate the gap.

  • Handle undefined explicitly – it means "not evaluated yet," not false.

  • Evaluate once, record the result – a flag is a one-time signal. Re-evaluate only on meaningful state changes.

  • Evaluate where the data lives – if the data is on your server, evaluate there.

  • Choose evaluation context deliberately – "server and client" is the default for compatibility, not because it's the right choice for your flag.

  • Clean up flags that have done their job – a flag at 100% is done. Remove it or archive it.

  • Disable client-side evaluation for server-side flags – don't let the client SDK re-evaluate what your server already decided.

  • Use a reverse proxy – prevent ad blockers from disabling your flags.

  • Call your flag in as few places as possible – wrap in a single function if used in multiple places.

  • Name flags clearly – descriptive names, types, positive language.

  • Roll out progressively – start small, monitor, then increase.

The mental model: Flags are pure functions – same flag key + same distinct ID = same result. Always. Unexpected results are almost always input problems – if the result changed, an input changed.


Flags are pure functions

A flag hashes two things – the flag key and the distinct ID – and returns a deterministic result. Same inputs, same output. Every time.

hash("my-experiment", "user-123") → 0.31 → always 0.31

On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: same flag key + same distinct ID = same result.

Technically

"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on flag_key + distinct_id. Some features like experience continuity add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output.

How the hash works

PostHog uses SHA-1:

hash_key = "{flag_key}.{distinct_id}"
position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE  → float in [0, 1]
in_rollout = position <= rollout_percentage / 100

For variants, a second hash with salt "variant" maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags.

If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns false.

Unexpected results are almost always input problems

If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays.

So when a flag returns something you didn't expect, the flag is fine, the problem is in the inputs passed to the flag. Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem.

If you keep running into flag issues and they're not incidents, the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it.

We're here to help with that – this guide, PostHog AI, and professional services all exist for exactly this. But the starting point is always the same: look at the inputs.

When something goes wrong, in order of likelihood:

  1. Input problems (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – bootstrapping, property overrides, server-side evaluation.

  2. Output problems. The flag returned the right value but your code misread it – undefined treated as false, no handling for the loading gap, evaluating repeatedly instead of recording the result.

  3. Actual incidents. Check status.posthog.com. If nothing there, it's #1 or #2. And even here: with server-side local evaluation, the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close.

Resolve identity before evaluating flags

Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person.

If you call identify() after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After identify(), the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed.

Call identify() before any flag evaluation in auth flows. If you can't guarantee that timing, bootstrap with the stable ID at init so the distinct ID is correct from the first millisecond. See keeping flag evaluations stable for the full picture.

SPA-specific timing. In single-page applications, identify() and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the distinct_id synchronously when identify() runs, but if capture() was called first in the same execution frame, that event uses the anonymous ID. The fix: call identify() before the navigation that mounts post-auth components – in Vue, in beforeEach before next(); in React, before navigate(), not in a useEffect inside the target route.

Don't rely on flag persistence to fix identity gaps

If you've enabled experience continuity (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it.

That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of known bugs where values can still change after identify(). It also means no support for local evaluation and slower flag responses.

The better fix is to make persistence unnecessary. Use device bucketing for single-device consistency, or design your identity flow so the distinct ID never changes. If you need experience continuity today, treat it as a migration path toward proper identity resolution, not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely.

Evaluation architecture

How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time.

Evaluate once, not continuously

A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes.

Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs.

  • Feature rollouts – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer.

  • Experiments – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to.

Evaluate where the data lives

If you target a flag on plan_type: "pro", your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data.

If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data.

If you must evaluate client-side, use setPersonPropertiesForFlags() to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser.

Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on.

Server-side local evaluation is where the pure function model is fully legible:

  • All inputs are explicit. You pass the distinct ID and properties directly. When something's wrong, you log what you passed.

  • Your data is right there. User plan, account type, permissions – it's in your database at request time. No syncing, no fetching.

  • No workarounds needed. Client-side evaluation often requires setPersonPropertiesForFlags(), onFeatureFlags(), and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap.

Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap.

Have the value before you need it

Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns undefined, not false.

Bootstrap is the fix. Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker.

If you can't bootstrap, use onFeatureFlags() to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay.

undefined is not "flag is off" nor false

posthog.getFeatureFlag() returns undefined before flags load. That means "not evaluated yet," not "flag is off."

// Returns undefined before flags load – not false
if (posthog.getFeatureFlag('my-experiment') === 'test') {
  // Never runs during the loading gap
}

Handle it with bootstrap (preferred) or onFeatureFlags() (adds a loading state). You can check the current identity with posthog.get_distinct_id().

The "not loaded yet" return value varies across SDKs – some return undefined/nil/None, others return false or a defaultValue you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of getFeatureFlag() and isFeatureEnabled() when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the Feature Flags API to query flag definitions directly.

Flag hygiene

Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient.

Choose a flag type intentionally

Every flag in PostHog is configured as client-side, server-side, or both via evaluation contexts. New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation.

If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server.

Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed.

Clean up flags that have done their job

A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable /flags requests, keep appearing in SDK payloads, and add clutter to your codebase.

Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See cleaning up stale flags for the full workflow and cutting costs for more on reducing your bill.

An idea worth considering: design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's true, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user /flags requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to false. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup.

Disable client-side evaluation for server-side flags

If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did.

This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size.

Use a reverse proxy

Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a reverse proxy so requests go through your own domain. PostHog offers a free managed reverse proxy, or you can run your own.

Call your flag in as few places as possible

The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function:

function useBetaFeature() {
    return posthog.isFeatureEnabled('beta-feature')
}
Name flags clearly

Good naming makes flags easier to understand and maintain:

  • Use descriptive names. is_v2_billing_dashboard_enabled is clearer than is_dashboard_enabled.

  • Use name types. Suffix with the purpose: new-billing-experiment, new-billing-release.

  • Reflect the return type. is_premium_user for a boolean, selected_theme for a string.

  • Use positive language for booleans. is_premium_user instead of is_not_premium_user – avoids double negatives.

Roll out progressively

Start at 5-10% of users, monitor metrics, then gradually increase. This is a phased rollout. At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone.

Use dependencies for complex rollouts

Feature flag dependencies let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies.

Be careful with "Latest" person properties

PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like $current_url) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., first_landing_page via $set_once) and target that instead.

Reducing your bill

Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our dedicated guide to cutting costs for estimating and reducing your feature flag bill.

Further reading

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
PostHog
Publisher
PostHog (Official)
Source
PostHog/skills · skills/omnibus/instrument-feature-flags · commit a5d30e7
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by PostHog or PostHog.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download instrument-feature-flags.zip