Generated banner for Creating Experiments
Generated logo for Creating Experiments

Creating Experiments

PostHogOfficialPublished by PostHog

  • Skill
  • Workflow
  • Data and AI
  • MIT
  • No issues found

From the author

Guides agents through experiment creation: reading the project's setup with experiment-setup-context, defining the hypothesis, configuring rollout and bucketing, setting up analytics and running time, and reporting which choices are guesses. Delegates rollout decisions to configuring-experiment-rollout and metric setup to configuring-experiment-analytics. TRIGGER when: user asks to create a new experiment or A/B test, OR when you are about to call experiment-create. DO NOT TRIGGER when: user is updating an existing experiment, managing lifecycle, or only browsing experiments.
— the description by PostHog

AutoGineer summary

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

This skill provides a systematic, three-step process for creating new A/B test experiments. It emphasizes a draft-first approach, requiring agents to read project context via experiment-setup-context to inform configuration choices. The skill guides the agent through defining hypotheses, setting up rollout parameters, and planning analytics, while ensuring that critical decisions—such as bucketing and persistence—are confirmed with the user before the experiment is created. It integrates with other specialized skills for rollout and analytics configuration to ensure a consistent and data-backed setup.

What it does

  • Analyzes project setup and event data to inform experiment configuration.
  • Drafts experiment parameters including name, hypothesis, and feature flag keys.
  • Delegates complex rollout and metric configuration to specialized skills.
  • Reports experiment choices categorized by confidence level.
  • Provides a direct link to the experiment in the UI for final review.
  • Guides users through post-creation steps like implementation and launch.

When to use it

  • When a user requests the creation of a new A/B test or experiment.
  • When the agent is about to execute an experiment-create command.
  • When you need to ensure an experiment is configured based on existing project facts.

Advantages

  • Reduces manual configuration errors by leveraging existing project data.
  • Ensures consistent experiment setup through a standardized workflow.
  • Provides transparency by distinguishing between data-backed choices and user-defined guesses.
  • Keeps the creation process lightweight by deferring metric setup to post-creation updates.

Usage examples

“Create an A/B test for the pricing page to see if a new CTA increases signups.”
The agent reads the project setup, identifies the pricing page events, drafts an experiment with a 50/50 split, and presents a summary of the configuration for user approval.
“I want to run an experiment on the homepage.”
The agent analyzes the homepage traffic and event schema, then asks the user for the hypothesis and bucketing preferences before proceeding with the draft creation.

Install

  1. Download the skill as a ZIP:

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

    mkdir -p .agents/skills && unzip creating-experiments.zip -d .agents/skills

Documentation by PostHog

Creating experiments

This skill walks through creating a new A/B test experiment: read the project, then the 3-step flow, then report the draft.

Core principle: draft first, iterate on details

Create the experiment as a draft quickly, then iterate on metrics and configuration. The user gets a tangible draft immediately and can refine it. Choose settings from the project's facts rather than asking, and say which choices are guesses. Bucketing is the exception: the facts cannot decide it, so ask before you create the experiment.

Step 0: Read the project

The right bucketing, metric and running time depend on the project: who sees the page, which SDKs evaluate the flag, how often the metric event happens. Read that before configuring anything.

  1. From the request, infer the target (the event that marks someone reaching the change, usually $pageview for a page) and a candidate metric event. Confirm both exist with read-data-schema. Don't ask the user for event names you can find. For a page, read event_property_values for $host and $pathname on the target event as well. The response samples the values rather than listing them all, so read it for the shape the project records (a trailing slash, a www. prefix, the casing) rather than as proof that a value is absent. An exact filter has to carry that shape: /pricing matches nothing where every pageview says /pricing/, and example.com matches nothing where the host is www.example.com.

  2. If the experiment-setup-context tool is available, call it once with target_event and metric_event. For a web surface, add target_properties with an exact $host. Add an exact $pathname as well when the surface is one page. target_url_contains is a substring match on $current_url. A bare domain matches any host that contains it, notexample.com included, and a homepage path matches every page under it. Both overstate the traffic and the exposure rate. For a surface that spans several pages, keep the exact $host and put only the path fragment in target_url_contains. Add metric_properties in the same call when the candidate metric counts only some occurrences of its event. Pass previous_experiments_limit: 5. Five experiments are enough to read a precedent, and the default of 10 roughly doubles the response for no more signal. If the tool is not available, continue without it and treat every choice below as a best guess. Never call a tool you can't see.

    Each filter needs a type of event or person, a key, an operator and a value. The call rejects the flag_evaluates_to operator with a 400 that names it. For the homepage of one domain:

    {
      "target_event": "$pageview",
      "target_properties": [
        { "key": "$host", "type": "event", "operator": "exact", "value": ["www.example.com"] },
        { "key": "$pathname", "type": "event", "operator": "exact", "value": ["/"] }
      ],
      "metric_event": "your_conversion_event",
      "previous_experiments_limit": 5
    }
  3. Apply references/setup-decisions.md to the result. It maps each fact to a choice (bucketing, where the flag is evaluated, exposure, primary metric, running time, precedent) and to a tier for the summary.

  4. Carry those choices into steps 1 to 3.

Ask the user only when a choice is "not decided" and the user is in the conversation. If the user said they are away, or asked for a draft without questions, decide with the defaults and list the open points in the report.

The 3-step creation flow

Step 1: What are we testing?

Gather these before calling experiment-create:

  • Experiment name — descriptive, inferred from context when possible

  • Hypothesis — what you expect to happen (goes in description)

  • Feature flag key — kebab-case. Infer a new key unless the user names an existing flag. The flag is auto-created — do NOT create one separately. The one exception is device-id bucketing, which experiment-create cannot set (see the recipe in configuring-experiment-rollout).

  • Type — leave empty (will internally default to "product". The "web" value is reserved for no-code experiments configured visually with the PostHog toolbar in a browser; it cannot be meaningfully driven via MCP. If a user asks for a no-code/toolbar experiment, point them to the PostHog UI instead of creating one here.)

If the user gives enough context to infer these, don't ask — just proceed.

Step 2: Who sees what variant?

This is about rollout configuration.

Before asking any rollout question, load configuring-experiment-rollout. The disambiguation wording, recommendations, and post-answer branches live there — do not formulate rollout questions yourself, and do not assume an example you remember covers the user's path.

Key decision points (covered in detail by configuring-experiment-rollout):

  • Variant split (how many variants, what percentage each)

  • Overall rollout percentage (what % of all users enter the experiment)

  • Bucketing and whether to persist the flag across authentication steps — the user chooses, because the project's facts cannot decide it

If the user doesn't mention rollout specifics, use defaults: 50/50 control/test, 100% rollout.

Step 3: How to measure impact?

This is about analytics and metrics. Load the configuring-experiment-analytics skill for guidance. That skill's first step checks for an existing shared metric to reuse before building a new one — don't duplicate a metric the project already has set up.

Do NOT configure metrics on creation. Metrics are not passed to experiment-create — they are added afterwards via experiment-update. This keeps the creation call lightweight.

When the user specifies metrics upfront, acknowledge them and add them immediately after creation. When they don't, infer the primary metric from the hypothesis (step 0) and add it after creation; say it was inferred.

If step 0 produced a running-time estimate, pass it to experiment-create as running_time_calculation so the plan is stored on the experiment.

How to create

Call experiment-create with:

{
  "name": "Descriptive experiment name",
  "feature_flag_key": "kebab-case-key",
  "description": "Hypothesis: [what you expect to happen]",
  "feature_flag": {
    "filters": {
      "multivariate": {
        "variants": [
          { "key": "control", "name": "Control", "rollout_percentage": 50 },
          { "key": "test", "name": "Test", "rollout_percentage": 50 }
        ]
      },
      "groups": [{ "properties": [], "rollout_percentage": 100 }]
    }
  }
}

Flag config goes in the feature_flag object, in the flag's own filters shape (not the deprecated parameters keys). Two different percentages live in there, do NOT mix them up:

  • filters.multivariate.variants[].rollout_percentage is how users inside the experiment are split across variants (must sum to 100, recommended to have an even split).

  • filters.groups[0].rollout_percentage is the overall gate: what fraction of all users enter the experiment at all (0-100, defaults to 100).

Key details:

  • Minimum 2, maximum 20 variants. No specific variant key is required — the analysis baseline defaults to the variant keyed "control" when present, else the first variant (override with stats_config.baseline_variant_key). Convention: key the baseline "control" unless the user asks for specific keys.

  • filters.groups[0].rollout_percentage defaults to 100 if omitted.

  • Bucketing and ensure_experience_continuity: default to user-id bucketing and leave ensure_experience_continuity out, so the team's persistence default applies. Report the surface's identity mix from step 0 and what device-id bucketing or persistence would each need, then let the user choose before this call. Device-id bucketing needs the flag created first, so the choice cannot wait until the draft exists. The identity mix does not decide it: see "Bucketing and persistence across login" in configuring-experiment-rollout.

  • Stats follow the team's defaults (method, confidence level). Only set stats_config if the user asks for a different method.

After creation

  1. Report the draft in three groups, so the user can review it quickly:

    • Set with confidence: the choice and the fact behind it ("linked the shared metric 'Signups': it counts the signup event per person, as you asked").

    • Best guess, please check: the choice, the fact, and what would change it ("a funnel metric, because your last four experiments measured conversion that way; say so if you want a count per user instead").

    • Not decided: what is missing and how to decide it ("the flag is also evaluated on your server with local evaluation; check the server and browser use the same distinct ID").

    Also say what you could not read: a missing tool, or a section whose status was not ok.

  2. Always show the experiment URL. The experiment-create response includes _posthogUrl — always display this link so the user can view and configure the experiment in the UI.

  3. Remind the user to implement the feature flag in code. Link to the experiment page and say "implement the flag as shown here" — the experiment detail page shows implementation snippets for the user's SDK.

  4. Guide through metrics if not yet configured — load the configuring-experiment-analytics skill.

  5. Launch only when the user asks for it — creation ends at a draft. A launch turns the feature flag on for real users, so never call experiment-launch unprompted.

  • configuring-experiment-rollout — variant splits, rollout percentage, and who sees the test

  • configuring-experiment-analytics — exposure criteria and primary/secondary metrics

  • managing-experiment-lifecycle — launch, pause, ship, and end once the experiment exists

Files

references/setup-decisions.md

Setup decisions from project facts

Use this file after experiment-setup-context returned. Each section maps the tool's facts to one configuration choice and to a tier for the summary:

  • Confident: the facts decide it.

  • Best guess: a reasonable default that the facts support but do not prove. Say what would change it.

  • Not decided: the facts cannot decide it. Say what is missing and how the user can decide.

Every section has the shape {status, data}. The field paths below are inside data: target_surface.anonymous_share means target_surface.data.anonymous_share. A [] in a path means a list: previous_experiments.experiments[].primary_metric_types is a field on each row. Write every dotted path from its section name, never from below it: previous_experiments.summary.using_custom_exposure, not summary.using_custom_exposure. A test resolves the full paths against the response and cannot check a shorthand. A bare field name is fine in a sentence whose section is already named. A section whose status is not ok gives no facts. Choices that depend on it drop to "best guess" at most, and the summary says which section was missing.

Bucketing and persistence

Read these facts:

  • target_surface.anonymous_share: among the distinct IDs that sent the target event and say whether they are identified, the share that were anonymous. Every SDK's events count, not only the web SDK's. A high share usually means logged-out visitors. It is null only when no target event carried $is_identified at all.

  • target_surface.device_id_share: the share of target events that carry a device ID, across every SDK that sent them. Null when there were no target events.

  • target_surface.libs[]: target_surface.libs[].anonymous_share and target_surface.libs[].device_id_share per SDK that reached the surface, plus a target_surface.libs[].category of web, mobile, server or other. A row's device_id_share is always a number, because the row exists only where that SDK sent target events.

  • sdk_profile.libs[]: per SDK, sdk_profile.libs[].device_id_share and sdk_profile.libs[].locally_evaluated_share on flag calls, plus a sdk_profile.libs[].category read the same way. It covers the whole project, not one page, so use target_surface.libs to pick the rows that apply to this page. It is empty when the project sent no multivariate flag calls in the last 7 days.

  • sdk_profile.libs_on_any_event[]: which SDKs the project sends any event from. Set only when sdk_profile.libs is empty.

Report the mix and leave the choice to the user. Tier: not decided. These facts give the identity mix on the surface, not whether the same individuals cross identification on it, so they cannot decide bucketing. A surface with an even mix may be two separate populations that never cross, and a surface that is almost all identified may still route every one of those people through an anonymous first pageview. Say what the mix is, name the options the surface can support under the caveats below, and say what each one would need. Ask before you call experiment-create. Device-id bucketing needs the flag to exist first, so the choice cannot wait until the draft is made.

Say it plainly when the project has used neither persistence nor device-id bucketing (previous_experiments.summary.using_persistence and previous_experiments.summary.using_device_id_bucketing both 0), because that is the case where the user has no in-house precedent to reason from. Report it as context, not as a fault.

What each option needs
  • User-id bucketing is the default. A person's variant follows their distinct ID, so it can change when they are identified.

  • Device-id bucketing needs a device ID on every flag call: a call without one gets no variant. Read target_surface.device_id_share, and sdk_profile.libs[].device_id_share on the rows whose lib appears in target_surface.libs. Both near 1 is the signal that the surface can carry it. A server SDK must forward the browser's device ID, and local evaluation works when it does. The web SDK sends the device ID on flag requests from posthog-js 1.307.1; an older version puts one on events and still gets no variant, so name the version as something to check. experiment-create cannot set it, so use the device-id recipe in configuring-experiment-rollout.

  • Persistence (ensure_experience_continuity: true) cannot work where an SDK evaluates the flag locally, because a local evaluation never consults the override store. Read sdk_profile.libs[].locally_evaluated_share on the rows whose lib appears in target_surface.libs. It also needs person profiles for anonymous users, no bootstrapping, and $anon_distinct_id on server flag calls. The tool cannot see those three, so list them for the user to check.

Both of those reads match sdk_profile.libs rows to target_surface.libs, so an SDK the match misses is unchecked rather than safe. A target_surface.libs[] row with no sdk_profile.libs row of the same lib is unchecked, and so is every row when sdk_profile.libs is empty, which is what an empty profile means: the project sent no multivariate flag call in the last 7 days, the normal state before its first experiment. target_surface.libs holds at most 5 SDKs and reports the cap in target_surface.libs_truncated, so an unseen SDK can reach the surface when that flag is true. In each of those cases say the option is unchecked, and name the SDK or the cap you could not read. An unread server SDK is the one that breaks both options, because it may send flag calls without a device ID and may evaluate them locally.

Read the shares rather than the platform, and read them in one direction only. Both count events and never the flag request: target_surface.device_id_share reads target events, and a sdk_profile.libs row reads flag-call events. A row near 1 says a device ID is present in that traffic. A row near 0 says only that those events carried none, which leaves the option unchecked rather than unavailable: a server SDK can forward the browser's device ID on the flag request while its own events carry no $device_id. So never rule device-id bucketing out from a share. Say it is unchecked, and name what would settle it, which is whether the SDK puts a device ID on the flag request. sdk_profile.libs_on_any_event[] names the platforms the project sends from, and that is all it does. It reads no flag call, so it cannot say whether a device ID reaches one or whether an SDK evaluates locally. Do not decide bucketing from it.

Device-id bucketing and persistence cannot be combined, and the API rejects the pair. When ensure_experience_continuity is omitted, experiment-create applies the team's default (team_defaults.flags_persistence_default). create-feature-flag leaves persistence off unless you set it, and experiment-create rejects a feature_flag object for a flag that already exists. So pass ensure_experience_continuity: false in the call that sets device-id bucketing on a flag experiment-create already made, because that flag carries the team default and the pair is refused. When team_defaults.flags_persistence_default is true and a matching sdk_profile.libs row evaluates the flag locally, say so: leaving the field out gives the flag a persistence setting that cannot work there. Set it to false only if the user asks, because the team chose that default.

Never change bucketing or persistence on a flag that is already live.

Where the flag is evaluated

If sdk_profile.evaluated_on_server_and_web is true, or target_surface.libs lists a server SDK next to web:

  • The server and the browser must use the same identity and see the same flag value. Otherwise one person gets two variants.

  • A custom exposure on a browser event is only safe when the browser holds the server's flag value at that moment, because the exposure reads $feature/<flag> from that event.

  • With device-id bucketing, the server must forward the browser's device ID on every flag call. A call without one gets no variant.

Tier: not decided. The facts show the risk, not whether the code handles it. Tell the user what to check.

Exposure

Keep the team's default exposure event (team_defaults.default_exposure_event). Use a custom exposure only when the user names the moment the person sees the change and the flag is evaluated well before it: a prefetch, a server render, or a flag read across the whole app while the change sits on one surface. Tier: best guess.

Who counts

Keep the test-account filter on and say so. team_defaults.new_experiments_filter_test_accounts says what a new experiment does when its own exposure criteria say nothing, and it does not follow the project's insight default. If team_defaults.test_account_filter_count is 0, say the project has no test-account filters, so the setting changes nothing. Tier: best guess.

Primary metric

  1. Pick the event that expresses the hypothesis. Confirm it exists with read-data-schema.

  2. Check shared_metrics.metrics[] for one to reuse instead of building an inline metric.

    • shared_metrics.metrics[].matches_metric_event set to true says the event appears in the metric somewhere. shared_metrics.metrics[].metric_event_roles says where: funnel_step and funnel_final_step for a funnel, mean_source for a mean, ratio_numerator or ratio_denominator for a ratio, retention_start or retention_completion for a retention metric.

    • A named role that cannot carry the hypothesis rejects the metric without another call. A metric that only starts a retention window from the event does not answer "did more people convert on it".

    • An empty shared_metrics.metrics[].metric_event_roles with a true match is not a rejection. The metric does count the event, but it is stored in a shape the role reader does not parse, which its shared_metrics.metrics[].metric_type shows as null or as a type not listed above. Retrieve it and read the query.

    • When the role fits, load the metric with experiment-saved-metrics-retrieve and check its metric_type and math against what the user asked for. The role alone does not prove those, so the tier stays best guess until you have read the query.

    • If it matches, link it after creation through experiment-update with saved_metrics_ids. Tier: confident. When the user asked for no questions, link it and report it rather than asking first.

    • When no listed metric fits and shared_metrics.metric_event_match_truncated is true, the match read only the newest shared metrics, so an older metric that counts the event shows matches_metric_event false. Call experiment-saved-metrics-list with event set to the metric event before you build an inline metric. That filter reads every shared metric.

    • If it doesn't, build an inline metric.

  3. Otherwise pick a shape from the metric templates in configuring-experiment-analytics (references/metric-templates.md). Tier: best guess.

If the metric counts only some occurrences of its event, for example a purchase with one payment method, pass those filters as metric_properties on the setup-context call. Use the same filters on the metric you build. Without them, candidate_metric reports a baseline for every occurrence of the event, and the running time below is too optimistic. candidate_metric.metric_properties echoes the filters the tool read.

Every conversion window carries a unit (conversion_window_unit). A window without a unit is ignored.

Follow the project's metric shape

Read previous_experiments.experiments[].primary_metric_types. It carries one entry per primary metric, so an experiment with two primary metrics of different types carries both. A row counts only when its list is not empty and every entry in it is the same type, and that type is the row's shape. An empty row ran no primary metric, and a mixed row made two different choices, so neither votes. Count the rows that qualify. Fewer than three is not a precedent: say the project has too few experiments to read one. From three qualifying rows up, propose the type held by at least 80% of them - 3 of 3, 4 of 4, 4 of 5 - unless the request asks for something else. Say you followed precedent and out of how many experiments. A project that measures every test as a funnel gets a funnel.

Then read previous_experiments.experiments[].primary_metric_events. When the event you picked already appears on a row, say which experiment measured it and as what, reading primary_metric_types on the same row. Tier: best guess.

Feasibility and running time

When candidate_metric.status is ok and it has baseline stats:

  1. Call experiment-calculate-running-time:

    • funnel metric: metric_type: "funnel", baseline_stats from candidate_metric.funnel_baseline_stats

    • count per user: metric_type: "mean_count", baseline_stats from candidate_metric.mean_count_baseline_stats

    • minimum_detectable_effect: team_defaults.minimum_detectable_effect, else team_defaults.product_default_minimum_detectable_effect

    • exposure_rate_per_day: target_surface.exposures_per_day_estimate, times the rollout share when the experiment includes less than 100% of traffic

    • number_of_variants when there are more than two variants

  2. Pass the result to experiment-create as running_time_calculation, so the plan is stored on the experiment:

    • minimum_detectable_effect: the value you sent (the calculator does not return it)

    • recommended_sample_size: the calculator's recommended_sample_size

    • recommended_running_time: the calculator's recommended_running_time_days

  3. If the running time is over about 8 weeks, say so plainly. Propose a more frequent event earlier in the journey as primary and keep the rare event as secondary.

The estimate assumes people are exposed on the target surface. If the flag is evaluated more widely (across the whole app, or on a page before the one that changes), the exposed population is larger and converts less. The baseline and the running time are then too optimistic. Say so, and consider a custom exposure on the surface event. This is the one case where a custom exposure is right in a project that has never used one, and "Precedent" says so too. previous_experiments.experiments[].outcome.control_baseline_value shows what that dilution looked like: a control baseline far below the rate the surface itself converts at is an experiment that was exposed too widely.

Check how the call scoped the target surface before you trust these numbers. target_surface.target_properties and target_surface.target_url_contains echo the filters the tool read. target_url_contains is a substring match on $current_url, so a bare domain matches any host that contains it, and a homepage path matches every page under it. Both overstate the page's traffic and the exposure rate. If the echo came back wider than the surface under test, the numbers here are too optimistic. Correct the scope and call the tool once more: an exact $host and an exact $pathname in target_properties for one page, or an exact $host with only the path fragment in target_url_contains for a wider surface. Read the numbers from the second response. A candidate_metric.persons_reached of 0 straight after an exact filter usually means the value is not the one the project records, such as /pricing against /pricing/. Read the shape back with read-data-schema (event_property_values, a sample of the values) and correct the filter, rather than widening it.

previous_experiments.summary says how the project's earlier experiments fared for size. When previous_experiments.summary.launched_with_zero_analyzed_exposures or launched_with_under_100_analyzed_exposures is a large share of launched, the project has been launching experiments that could not measure anything. Say so, and treat the running time as the number to get right rather than a formality.

Read the counts in this order
  1. candidate_metric.persons_reached is 0: no one sent the target event in the window under target_properties and target_url_contains. Fix the target before reading the two counts below. Only people who reached the target can convert, so candidate_metric.persons_converted is 0 whatever the metric does, and candidate_metric.event_volume counts the metric event without the target and says nothing about the pair.

  2. candidate_metric.event_volume is 0: the metric event never occurred in the window under metric_properties. Check the event name with read-data-schema, then check the filters. A candidate_metric.conversion_rate of 0 says nothing until this is above 0.

  3. candidate_metric.event_volume is above 0 but candidate_metric.persons_converted is 0: the event happens, but never after the target event. Either the metric measures something people do elsewhere in the product, or the target is wrong. candidate_metric.unique_persons says how many people send the event at all, which separates a rare event from a misplaced one.

Tier: confident on the arithmetic, best guess on the inputs (the baseline is an estimate over candidate_metric.window_days).

Precedent

Read previous_experiments.summary. Its using_* counts cover every listed experiment, drafts included, so read them as what the project sets up, not what it launches. When previous_experiments.experiments is empty, the project is creating its first experiment. There is no precedent, and nothing in this section applies. Say that rather than reporting an absent precedent as agreement.

If most listed experiments use one setting, follow it unless the facts above contradict it, and say you followed precedent. Tier: best guess. Three limits:

  • Bucketing and persistence sit outside this rule. "Bucketing and persistence" leaves both to the user, so a count of earlier experiments does not settle them either. Report the precedent as context and still ask.

  • Do not add configuration the project has never used, unless a fact above calls for it. A custom exposure and an activation event each narrow what counts as an exposure, and a project whose previous_experiments.summary.using_custom_exposure and using_activation are 0 has given you no reason to narrow it.

  • One fact above overrides a count of 0, and only this one: a flag evaluated more widely than the surface under test, for a custom exposure. That is the case "Feasibility and running time" describes, and it is the right first use of a custom exposure in a project that has never had one.

Read previous_experiments.experiments[].serving_single_variant before you trust previous_experiments.summary.using_uneven_split. Shipping a variant rewrites the flag to serve that variant to everyone it matches, so an ended experiment's flag no longer carries the split it ran with. The summary leaves those experiments out of using_uneven_split, and previous_experiments.experiments[].split_even is null on any row where serving_single_variant is set.

previous_experiments.summary.using_exposure_property_filters counts experiments whose exposure is narrowed by properties on whichever event it counts. A narrowed default event is not a custom exposure, so using_custom_exposure does not count it, but it is still a deliberate choice about what an exposure means. Read previous_experiments.experiments[].exposure_property_filters: an experiment that counted exposure only where $pathname was /pricing is the precedent for a new test on that page.

For a holdout, look at previous_experiments.experiments[].has_holdout.

Statistics

Leave stats_config out, so the experiment follows the team's defaults (stats_method, confidence_level, CUPED, sequential testing). Set it only when the user asks for a different method. Tier: confident.

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.

Security

No issues found by our static scan· No issues found

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

Published by
PostHog
Publisher
PostHog (Official)
Source
PostHog/skills · skills/omnibus/creating-experiments · 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 creating-experiments.zip