Featurevisor v4.0 is here!
Featurevisor v4.0 lets your features, segments and variables refer to each other through one conditions grammar, and lets you test those relationships before a datafile reaches your applications.
A feature rarely stands alone. File upload in chat needs chat itself. The payment methods in a checkout depend on the market and on the plan. In v3 some of these relationships had their own fields and the rest lived in application code. V4 puts them in your definitions, where they are reviewed, linted and tested together.
This post follows one small project through the release: liveChat and chatFileUpload, a maintenanceMode switch, the checkout feature with its settings, and four users, 8f2a, c4e1, 3f5d and 9b30.
What's new?#
- Unified conditions select and gate everywhere with attribute, segment, feature and variable leaves, combined with
and,orandnot. - Accumulating overrides build one global variable value in layers.
- Composed global variables collect flags, variations and variables into one object, or alias another variable.
- Scenarios and checks test values and relationships across several definitions.
- Presence and emptiness operators tell missing, null and empty values apart, and
includesAnymatches against a list. - Groups are removed, with two patterns that replace them.
Gate a feature on configuration#
chatFileUpload should appear only where liveChat does, and never during maintenance. Both requirements go in one conditions list at the root of the feature:
description: File upload inside chatbucketBy: userIdconditions: - feature: liveChat - variable: maintenanceMode operator: equals value: falserules: - key: everyone conditions: '*' percentage: 100Every item in the list must match. liveChat is on for everyone, so all four users get file upload while maintenanceMode is false. Switch it to true and file upload goes for every user, without touching the feature.
In v3, requiredFeatures could ask for liveChat, but nothing could read the maintenance switch, and rules picked their audience through a separate segments field. V4 replaces both with conditions wherever you select or gate: rules, force entries, variable overrides and the roots of features and global variables. If your team prefers v3's stricter shape, where rules only name segments, allowedConditions keeps it.
A condition has four kinds of leaf. Attribute leaves read the context. Bare strings are segment keys, such as netherlands. Feature leaves check a flag or a variation, and variable leaves compare a value, optionally at a nested path. Segments accept the same leaves, so an audience can depend on a feature too. The conditions guide covers every form.
Build a value in layers#
Dutch users can pay with iDEAL, and paying accounts can pay by invoice. A Dutch paying account should get both. Add continue: true to the first override so the next matching override builds on its result:
description: Checkout settingstype: objectproperties: currency: type: string paymentMethods: type: array items: type: stringdefaultValue: currency: EUR paymentMethods: [card]overrides: - key: netherlands conditions: netherlands continue: true mutate: paymentMethods:appendUnique: ideal - key: paying conditions: payingAccounts mutate: paymentMethods:appendUnique: invoiceThe four users get these payment methods:
| User | Context | paymentMethods |
|---|---|---|
8f2a | Dutch, free plan | [card, ideal] |
c4e1 | Dutch, paid plan | [card, ideal, invoice] |
3f5d | German, free plan | [card] |
9b30 | German, free plan | [card] |
Without continue: true, the first match wins and c4e1 stops at [card, ideal]. A paid account outside the Netherlands gets [card, invoice], because only paying matches.
appendUnique is one of the mutations that change part of a value instead of repeating all of it. Overrides can also nest, so a child refines its parent's value for a narrower audience. See nested overrides.
Hand your app one object#
The checkout page reads its flag, its pricing variation and these settings together. Collect them into one global variable with compose:
description: Values for the checkout viewcompose: enabled: { feature: checkout } pricing: { feature: pricing, variation: true } settings: { variable: checkoutSettings }Each property reads its source through the normal getter, with that source's own targeting, bucketing and conditions. Read the result with the global variable getter you already use:
const context = { userId: 'c4e1', country: 'nl', plan: 'paid',}const view = f.getVariableObject('checkoutView', context)c4e1 gets { enabled: true, pricing: 'treatment', settings: { currency: 'EUR', paymentMethods: ['card', 'ideal', 'invoice'] } }. 9b30 gets enabled: false and pricing: 'control', with paymentMethods: ['card']. A false flag stays in the object and does not hide the properties next to it.
A string reference makes an alias instead. compose: checkoutSettings returns the same value under another key, which keeps old consumers working while you rename a variable. Compositions and aliases can have their own root conditions and overrides.
Every property is evaluated on each read. Composing an experiment can expose users to it even when the page never shows that property, so read the composition guidance before composing experiments. Older SDKs read a composition as a variable without a default and return null, so upgrade every consumer before publishing one.
Test how it fits together#
The checkout relies on three things agreeing for c4e1: the new checkout is on, the account counts as paying, and invoice is offered. No single definition owns that, so write a scenario:
description: Paying Dutch accounts can pay by invoicecontext: userId: c4e1 country: nl plan: paidchecks: - feature: checkout enabled: true - segment: payingAccounts matches: true - variable: checkoutSettings path: paymentMethods operator: includes value: invoiceRun it with the rest of your tests, or on its own:
npx featurevisor test --entity-type=scenarioChecks compare variable values, feature results and segment matches, either with a literal or with another evaluated value. They work inside feature and variable specs too. Give a check a key and description to make its failure easy to find. The native SDK runners run the same specs through the Go, Python, PHP, Ruby, Java, Swift, Rust and Elixir SDKs.
A feature spec can test the maintenance gate on its own. stickyVariables fixes the switch for one assertion, as setStickyVariables() would in your application:
feature: chatFileUploadassertions: - key: maintenance description: Maintenance switches off file upload at: 50 context: userId: 8f2a stickyVariables: maintenanceMode: true expectedToBeEnabled: falsestickyFeatures fixes feature results the same way. Every kind of spec uses these two names.
When a value surprises you, ask for the reason:
npx featurevisor evaluate \ --variable=checkoutSettings \ --context='{"userId":"c4e1","country":"nl","plan":"paid"}' \ --explainThe explanation names every override that changed the value:
Explanation: Global variable overrides supplied the value.Last override: payingApplied overrides: netherlands; payingCatalog shows the same relationships while you browse. Each definition lists what uses it and what it depends on, directly and through other definitions, next to its tests.
Catch mistakes before they ship#
A relationship can break in a way no single file shows. Lint checks the whole graph:
- A cycle across features, segments and variables is an error, including one that passes through a segment.
- A chain of more than 256 dependency evaluations is an error, because every SDK fails it. Lint warns above 32. See cycles and depth.
- A regular expression that can take exponential time on short input is an error.
- A bare string is always a segment key, so writing
- liveChatinstead of- feature: liveChatfails. - A reference to an archived definition is an error, unless the definition that refers to it is archived too. Archived definitions are left out of datafiles, so the reference would stop matching, and inside
notit would match everyone. - A reference to a deprecated feature or variable is a warning, so you can move off it before it is archived.
That last mistake reads like this:
features/chatFileUpload.yml => Error: Unknown segment "liveChat". "liveChat" is a feature: use { feature: liveChat }. Path: conditions.0Before you archive liveChat, find what still needs it:
npx featurevisor find-usage --feature=liveChatIt lists chatFileUpload. Lint rejects archiving liveChat until that condition is removed or chatFileUpload is archived too. See issue codes for everything lint reports.
Same answer in every language#
Everything above works in the JavaScript SDK and in the Go, Python, PHP, Ruby, Java, Swift, Rust and Elixir SDKs, with an OpenFeature provider for each. Supported SDK versions lists the first release of each with v4 support.
The SDKs share one public runtime contract. It defines how conditions, compositions and dependency failures evaluate, so a Go service and a Swift app give a user the same result. A cycle or a chain that is too deep fails the whole call with an error, never a partial value, and reports one evaluation_error diagnostic for that call.
V4 SDKs also read v3 datafiles. Upgrade your applications first, while the project still publishes v3 datafiles, then upgrade the project.
Groups are removed#
Groups were meant to keep features in different slots from sharing users, but they could not guarantee it. Each feature was bucketed on its own, so experiments meant to be exclusive could overlap, and a positive rule percentage inside a slot could be ignored. V4 removes them.
Two patterns replace them, both built on conditions:
- Priority exclusion: a feature requires another to be off, so no user gets both.
- Shared variation allocation: one feature assigns each user a cohort, and each experiment requires its own cohort.
Convert every Group on the latest v3 CLI, before upgrading SDKs. Converting can change who is in each experiment, even at the same percentages, and an old rule key can keep its slot allocation. Follow Groups removed step by step, and schedule the conversion between experiments.
Upgrade to v4#
V4 is a breaking release, including for applications that do not adopt the new configuration features. Before upgrading:
- Ensure the project CLI can run on Node.js 24 or later.
- Plan the required conditions migration. Replace selectors and gates with
conditionswhen upgrading the CLI. There is no automatic conversion. - Replace every Group as described above.
- Replace the
double,dateandsemvertypes. Authoring types follow JSON Schema, so fractional numbers arenumber, and timestamps and versions are strings withformat: date-timeorformat: semver. - Rename
stickytostickyFeaturesin feature specs, and remove references to archived definitions. - Review the other changes when you upgrade the CLI, such as attribute directories, custom parsers and generated variable types, and the JavaScript SDK changes, such as
getVariableNumber()replacinggetVariableDouble().
Follow the v4 migration guide in this order:
- Update existing consumers to the latest v3 JavaScript SDK or a compatible native SDK release that understands
requiredFeatures. - Replace legacy feature
requireddefinitions withrequiredFeatures, then rebuild and publish with the latest v3 CLI. Include bundled, cached and on-demand datafiles. - Upgrade every application SDK and integration while continuing to serve the prepared v3 datafiles. Follow the migration guide for each language.
- Upgrade the project CLI and migrate selectors and gates to
conditions. Run lint, project tests and application tests before deployment. - Publish v4 datafiles only after every consumer is upgraded, including consumers of bundled, cached, persisted and on demand copies.
SDK package versions are independent across languages. Use the supported SDK versions and the language migration guides to choose the right release for each application.
Keep configuration in your workflow#
V4 builds on the idea behind the first stable release: configuration belongs in your repository, reviewed and tested before it reaches your applications. With conditions, compositions and scenarios, that review now covers how your configuration fits together, not only each file on its own.
Explore the documentation, follow the roadmap, and share your feedback on GitHub.

