Featurevisor

Migration guides

Migrating from v3 to v4


Upgrade your Featurevisor project and applications from v3 to v4. The createFeaturevisor(), isEnabled(), getVariation() and getVariable() calls stay the same. V4 SDKs read prepared v3 datafiles, so upgrade consumers before changing authoring and publishing v4 output.

V4 removes Groups and deprecated SDK APIs, and uses unified conditions for every selector and gate, and adds accumulating overrides and richer tests. Logical and, or and not groups remain supported. See the v4 release announcement for an overview.

Other language SDKs

Native SDK versions are independent of the CLI version. Use the guide for your application:

Application languageMigration guide
GoGo SDK
PythonPython SDK
PHPPHP SDK
RubyRuby SDK
Java or KotlinJava SDK
SwiftSwift SDK
RustRust SDK
ElixirElixir SDK

At a glance

WhoRequired change
Project maintainersPrepare v3 datafiles, then upgrade the CLI
Application maintainersReplace removed SDK APIs and deploy upgraded consumers
OpenFeature usersUpgrade provider and SDK together and test targeting keys
CI maintainersUpgrade native runners with the v4 CLI and refresh test case reruns

Migration order

  1. Update existing consumers to the latest v3 JavaScript SDK, or a native release that reads requiredFeatures.
  2. Convert Groups and legacy requirements, then prepare and deploy every datafile and cached copy with the latest v3 CLI.
  3. Replace removed application APIs and deploy upgraded SDKs while keeping the prepared datafiles.
  4. Upgrade the project CLI and migrate all selectors and gates to conditions. Review mutation ordering and run lint and tests.
  5. Publish v4 datafiles only after every consumer is ready, including consumers of cached and bundled copies.

Save the prepared datafiles and application versions as your rollback point.

Prepare every datafile before upgrading SDKs

V4 SDKs silently ignore legacy feature required and ranges fields, which can bypass prerequisites, change cohorts or leave enabled features without a variation. Convert Groups and legacy requirements, then rebuild and publish with the latest v3 CLI first, including bundled fallbacks, persisted and cached copies and datafiles loaded on demand.

Prepare while still on v3

Update JavaScript applications, including React or Vue integrations in use, to their latest v3 packages:

Command
npm install @featurevisor/sdk@3

In the Featurevisor project:

Command
npm install --save-dev @featurevisor/cli@3

Rename feature required to requiredFeatures and object entry key to feature, preserving variation and enabled constraints:

Before
required:
- key: checkout
variation: treatment
After, still on v3
requiredFeatures:
- feature: checkout
variation: treatment

For strings, required: [checkout] becomes requiredFeatures: [checkout]. If both properties exist, retain the intended requiredFeatures declaration without creating duplicate keys. Leave object schema required lists unchanged.

Convert every Group now, before upgrading SDKs. Follow Groups removed: use priority exclusion or a shared allocation feature with flat list requirements. Do not use v4 nested requirement expressions during this preparation. Review audience changes and schedule conversion between experiments, as existing cohorts are not guaranteed.

Commands
npx featurevisor lint
npx featurevisor test
npx featurevisor build

Deploy every environment, set and target. Inspect the generated JSON each application loads: feature prerequisites must use requiredFeatures, with feature in object entries, and features must no longer contain legacy ranges. Keep the range fields under traffic[].allocation. Publish converted copies to all caches, bundled fallbacks, persisted storage and on-demand sources before any v4 SDK evaluates them. Replace old copies or ensure upgraded applications discard them before evaluation.

Upgrade SDK consumers

After preparation, install the v4 SDK:

Command
npm install @featurevisor/sdk@4

With React or Vue, upgrade the integration in the same install:

Command
npm install @featurevisor/sdk@4 @featurevisor/react@4
Command
npm install @featurevisor/sdk@4 @featurevisor/vue@4

Upgrade OpenFeature providers with the SDK, including any instance passed into a provider. Node.js providers require @openfeature/server-sdk ^1.23.0. Browser providers require @openfeature/web-sdk ^1.10.0. Resolve peer dependency conflicts rather than bypassing them.

Through the actual OpenFeature client, test omitted, empty and inherited targeting keys. In JavaScript, targetingKey: "" replaces the configured targeting field. Omitting the key preserves it. Native client behaviour differs: follow the language guide and OpenFeature reference.

Removed JavaScript sticky options and module before and after callbacks are silently ignored. Verify replacement callbacks run and sticky values apply on parent and child instances. A successful evaluation alone does not prove the migration is complete.

Upgrade the project

Use Node.js 24 or later for the CLI and core on developer machines and CI. This is not a browser SDK requirement.

If you use a custom parser, add its writing function before running the upgraded CLI.

Commands
npm install --save-dev @featurevisor/cli@4
npx featurevisor lint
npx featurevisor lint --v3-mutation-order
npx featurevisor test
npx featurevisor build

Regenerate code with generate-code where used. Compare important enabled and disabled results, variations and variable values using the same application contexts before and after migration.

Groups removed Breaking

CLI error codeWhen it occursRecommended action
removed_groupsA command uses a removed group option.Remove the option and complete the migration below.

Groups did not work reliably. Their features were bucketed independently, so supposedly exclusive experiments could overlap. A positive rule percentage inside a slot could also be ignored. V4 removes the Groups entity completely. Variation allocation ranges and logical and, or and not expressions stay.

Prepare the replacement while still on the latest v3 CLI and compatible v3 SDKs:

  1. Choose priority exclusion when one feature should give way to another, or shared variation allocation for experiment cohorts.
  2. For shared allocation, create a feature at 100% with one variation per cohort and an explicit holdout for empty slots. Add flat list requiredFeatures with a variation constraint to each experiment. For priority, require the higher priority feature with enabled: false in a flat list.
  3. Review the experiment rollout as a percentage within its new cohort. Whichever CLI you convert with, rename the rule keys on every formerly grouped feature to fresh keys, for example everyone to allocated, before rebuilding. The saved state writer does not record Group slots. Keeping an old rule key retains its slot based variation allocation, so enabled users can receive no variation even at 100%. Lint and build do not report this. Fresh rule keys reset those allocations. Preserve state for unrelated features. Do not delete the entire state file. Conversion can change audience size and membership. There is no guarantee of preserving old cohorts, even with the same nominal percentages.
  4. Delete Group definitions, including those in sets, and remove groupsDirectoryPath. Keep prerequisites exposed and review the exposure warning before using expose: false.
  5. Lint, test and rebuild with the v3 CLI. Complete the variation and allocation checks. Compare real user IDs, enabled results and variations. Publish every converted environment, set and target, including all cached, persisted, bundled and on demand copies, before upgrading SDKs.
ConsumerDatafileBehaviour
Latest compatible v3 SDKConverted flat requirement listsSupported
v3 SDKNested and, or or not requirementsUnsupported. Features can stay off
v4 SDKConverted requirementsSupported
v4 SDKLegacy feature rangesSilently ignored, with no compatibility handling or warning

The v4 CLI rejects groupsDirectoryPath and nonhidden files in groups/ or a selected sets/<set>/groups/. Build and lint also detect custom datasources returning keys from legacy listGroups(). Convert all sets before promotion or other workflows, not only the currently selected set.

Remove Groups APIs from custom adapters: listGroups, groupExists, readGroup, writeGroup and deleteGroup. The list --groups option, group counts in info, Group diff entries and Catalog group views are gone. See Datasource and Plugins.

Verifying converted allocations

For a converted secondExperiment with equally weighted control and treatment variations and a 100% rollout, add this v3 compatible feature spec:

Excerpt: converted experiment variation check
tests/features/secondExperiment.spec.yml
feature: secondExperiment
assertions:
- at: 10
sticky:
experimentAllocation:
enabled: true
variation: second
expectedToBeEnabled: true
expectedVariation: control

The sticky allocation selects the cohort without forcing the experiment, and at: 10 checks a low bucket that an old later slot can leave empty. When you later upgrade the CLI, rename sticky to stickyFeatures, as described in sticky values in feature specs. Adapt the expected variation, context and bucket to each converted rule, keeping the bucket within its rollout. For priority exclusion, make the prerequisite sticky with enabled: false instead.

Then check each converted rule's traffic[].allocation[].range in the generated JSON (0 to 100,000 scale):

  • The allocation as a whole starts at 0 and covers the rollout through its percentage multiplied by 1,000.
  • Range interiors do not overlap. Adjacent ranges may share an endpoint, such as [0, 50000] and [50000, 100000]. The first match wins there.

Unified conditions Breaking

The v4 CLI accepts only conditions for selectors and gates. Replace rule, force and override segments, and move requiredFeatures and unreleased requiredVariables into that expression. There is no compatibility output or automatic conversion. V4 SDKs normalize released v3 datafiles once when loaded, without changing caller objects. V4 datafile entities are normalized lazily on first read. Mixed new and legacy fields are ANDed. Unreleased datafile requiredVariables is not read.

Run npx featurevisor lint before building, locally and in CI. Removed authoring fields are lint errors, but build does not run full lint or add a removed field validation gate. Skipping lint can silently lose an old gate or selector. Dependency cycles and other unsafe dependency graphs still fail building independently. The existing guard for legacy feature required remains, but is not a substitute for lint.

Upgrade SDKs before publishing v4 datafiles

V4 builds emit schemaVersion: "4". V3 JavaScript SDKs read traffic.segments, which v4 datafiles no longer contain. No ordinary rules match, so flags are off unless another evaluation path, such as force or sticky state, supplies a result.

In v3 JavaScript SDKs, force entries and variable overrides interpret v4 conditions as attribute conditions. Feature, variable and segment leaves evaluate to false. A not group around such a failed expression can therefore match everyone. Force can still win, and variable overrides can return unintended values. This is not safe failure under NOT. Other SDKs can fail differently, as described under operator restrictions.

The schema version is informational and does not reject old SDK reads. Upgrade every consumer before publishing, including applications using bundled, cached, persisted and on demand datafiles. Sticky or force results are not a compatibility safeguard. An SDK rollback also requires restoring compatible v3 datafiles everywhere those SDKs can read, including cached and bundled copies.

Rules

Before
features/checkout.yml
# ...
rules:
- key: netherlands
segments: netherlands
percentage: 100
After
features/checkout.yml
# ...
rules:
- key: netherlands
conditions:
- netherlands
percentage: 100

Force entries

Before
features/checkout.yml
# ...
force:
- segments: netherlands
enabled: true
After
features/checkout.yml
# ...
force:
- conditions:
- netherlands
enabled: true

Feature variable overrides

Combine previously separate selectors into one list. Keep their intended order, since dependency getters and callbacks run in authored order.

Before
features/buyButton.yml
# ...
rules:
- key: everyone
segments: '*'
percentage: 100
variableOverrides:
label:
- segments: netherlands
requiredFeatures: checkout
value: Nu kopen
After
features/buyButton.yml
# ...
rules:
- key: everyone
conditions: '*'
percentage: 100
variableOverrides:
label:
- conditions:
- netherlands
- feature: checkout
value: Nu kopen

Global variable overrides

Before
variables/supportEmail.yml
# ...
overrides:
- key: checkout
segments: netherlands
requiredFeatures: checkout
value: checkout-[email protected]
After
variables/supportEmail.yml
# ...
overrides:
- key: checkout
conditions:
- netherlands
- feature: checkout
value: checkout-[email protected]

Every override now needs conditions, including nested overrides. Use conditions: '*' where an override should always match. Children also require every ancestor to match.

Root gates

The same replacement applies at feature and global variable roots:

Before
features/chatFileUpload.yml
# ...
requiredFeatures:
- liveChat
requiredVariables:
variable: maintenanceMode
operator: equals
value: false
After
features/chatFileUpload.yml
# ...
conditions:
- feature: liveChat
- variable: maintenanceMode
operator: equals
value: false

requiredVariables never shipped in v3. Its example above applies only to projects using that unreleased authoring. Existing object feature entries keep their shape. Wrap feature key strings in { feature: <key> }.

Compositions can have root conditions and optional disabledValue, checked against the inferred schema. A whole variable sticky value comes first. An unmet gate returns disabledValue or null, without reading sources or running overrides. Compositions have no defaultValue or useDefaultWhenDisabled.

Strings are segments

A bare string always references a segment. conditions: [checkout] never means a feature prerequisite. Write conditions: [{ feature: checkout }] instead. Segments accept all four leaf kinds and can reference other segments, but lint rejects dependency cycles. Empty lists and groups are invalid and evaluate to false. '*' is valid only as the whole expression. See Conditions for the complete grammar.

Keep the v3 shape

V4 accepts every kind of condition in every location. A rule can compare attributes directly, a segment can name other segments, and an override can read a global variable. If your team prefers the stricter shape of v3, where audiences live in segments and rules only name them, keep it with allowedConditions:

featurevisor.config.js
module.exports = {
allowedConditions: {
segments: ['attribute'],
features: ['feature'],
rules: ['segment'],
force: ['attribute', 'segment'],
featureVariableOverrides: ['attribute', 'segment', 'feature'],
variables: ['feature'],
variableOverrides: ['attribute', 'segment', 'feature'],
},
}

Each location lists what v3 allowed there:

  • Segments compare attributes only.
  • Feature and global variable roots only require other features, as requiredFeatures did.
  • Rules only name segments.
  • Force entries compare attributes or name segments.
  • Variable overrides compare attributes, name segments or require features.

A project converted as this guide describes passes lint unchanged. Lint then reports anything else with disallowed_condition. When your team wants a capability v4 adds, relax one location at a time. Removing a location from allowedConditions accepts every kind there.

Changes when you upgrade the CLI

Attribute and schema types Breaking

CLI error codeWhen it occursRecommended action
removed_schema_typeAn attribute, schema or variable uses a removed type.Use the replacement the message names.

Types now follow JSON Schema. double became number, and timestamps and versions are strings with a format:

v3v4
type: doubletype: number
type: datetype: string with format: date-time
type: semvertype: string with format: semver

This applies to attributes, their properties, reusable schemas, global variables and feature variablesSchema. There is no automatic conversion. Edit the files lint reports:

Before
description: App version
type: semver
After
description: App version
type: string
format: semver

Attribute changes do not change datafiles, since attributes are not part of them. Number variables are written as type: number in v4 datafiles. V4 SDKs read both number and the earlier double. JavaScript, Python, Ruby, PHP and Elixir read them with getVariableNumber() or its spelling in that language, and their getVariableDouble() was removed. The Java, Swift, Go, Rust, Dart and .NET SDKs keep getVariableDouble, named after the double they return.

The formats also tighten lint:

  • format: semver accepts every version the version operators compare, such as 5.5 or v5.5.0. Values they cannot compare, in enum, const or conditions, are now errors.
  • date and email are not formats. date-time and semver are the only ones.
  • minLength, maxLength, minItems and maxItems must be whole numbers of zero or more.
  • An object attribute's required lists keys from its properties. An object without properties cannot declare required.

Date attribute comparisons Breaking

Timestamp attributes, declared with format: date-time, now accept only before, after, exists, notExists, isNull and notNull. This applies to object paths, schemas whose branches all use that format and global variables with it. Equality compared representations rather than instants. Replace it with a strict date comparison appropriate to your intent, not an equivalent equality test:

Before
conditions:
- attribute: signupDate
operator: equals
value: '2026-01-01T00:00:00Z'
After, accounts created from 2026 onward
conditions:
- attribute: signupDate
operator: after
value: '2025-12-31T23:59:59.999Z'

Replace empty or notEmpty with a presence or null check where that is your intent. Presence does not prove a timestamp is valid. Every date enum entry and const must also be a portable timestamp, including in nested schemas. Remove placeholder values such as soon.

Variables declare timestamps the same way, with format: date-time, and their conditions follow the same operator rules. Declare a plain string to also compare a timestamp as text. Plain string date comparisons remain valid, and generated variable types stay string. The format adds no datafile field or SDK validation.

Lint and evaluate now warn about supplied contexts that do not match declared attribute types or constraints. Warnings do not change results or exit status. Test runners do not validate context, and build does not run lint or add a date operand guard. Run lint separately before testing and building.

Native date inputs also follow the portable year range and offset limits where the type retains that information. Values outside those limits no longer match before or after. Check native date values when upgrading other SDKs, including Ruby's removal of plain Date support.

Invalid saved allocations Breaking

The builder now rejects invalid saved variation allocations instead of reusing them. An invalid_saved_allocation error names the feature and rule. JSON error details include stateFile when the allocation file is known. Restore valid allocation state, or deliberately rename the rule if you want a new allocation. Do not remove unrelated state. See State files.

String membership conditions Breaking

Conditions using in or notIn require a declared string attribute, including a string property reached through a dot separated path. Numeric and other attribute types are rejected by linting. Replace numeric in with an or group of equals conditions. Replace numeric notIn with not containing that or group to preserve the negation. This restriction does not change variable requirement operators or runtime evaluation of existing datafiles.

Unknown evaluation keys Breaking

evaluate exits with code 1 for an unknown project feature or global variable. JSON errors use unknown_feature or unknown_variable. A known entity excluded from a target is still evaluated through the SDK so its absence can be explained.

Selecting an environment when building

build --environment=<name> now builds only that environment. V3 ignored the option and built every environment. Unknown names fail, as does --environment in a project without environments. Check CI scripts that pass the option. A selected build advances the shared revision but leaves other environments' existing datafiles unchanged. Run npx featurevisor build without the selector before publishing every environment together.

Custom parsers must support writing Breaking

Custom parsers must provide a nonempty extension and synchronous parse and stringify functions. V4 validates them when loading configuration, so a parser that only reads files now stops every CLI command with Invalid custom parser: stringify must be a function. Built in YAML and JSON parsers need no changes.

If you followed the previous TOML example, replace the parse only toml dependency with smol-toml:

Commands
npm uninstall toml
npm install --save-dev smol-toml

Keep the rest of your project configuration unchanged:

Before
featurevisor.config.js
module.exports = {
parser: {
extension: 'toml',
parse: (content) => require('toml').parse(content),
},
}
After
featurevisor.config.js
module.exports = {
parser: {
extension: 'toml',
parse: (content) => require('smol-toml').parse(content),
stringify: (content) => require('smol-toml').stringify(content),
},
}

stringify is used when writing definitions, including promotions. Run lint, tests and a promotion preview before applying changes. Serialization may change formatting or comments. Review the Git diff. See Custom parsers for the full contract.

Generated variable types Breaking

Regenerate your TypeScript files with npx featurevisor generate-code --language=typescript --out-dir=./src and add --react if you use the generated React hooks. Update imports from your generated modules:

Previous generated typeV4
GlobalVariablesVariables
GlobalVariableKeyVariableKey
GlobalVariableType<K>VariableType<K>
Deprecated VariableKey<F> for a featureFeatureVariableKey<F>
Deprecated VariableType<F, V> for a featureFeatureVariableType<F, V>

The generated VariableKey and VariableType<K> now describe global variables only. Feature variables retain their explicit FeatureVariable names. The SDK's own VariableKey remains a string key, and its VariableType describes schema kinds. Those SDK types are distinct from your generated project types. Calls to getVariable() and useVariable() stay the same.

Variable key collisions Breaking

If your project allows features and global variables to share keys, rename the configuration property. Its default remains false:

Before
module.exports = {
allowFeatureAndGlobalVariableKeyCollisions: true,
}
After
module.exports = {
allowFeatureAndVariableKeyCollisions: true,
}

New project defaults

init now creates flat rules without environments and an all target that includes every active feature and global variable without requiring tags. Existing project configuration and targets are not rewritten. Use init --example=with-environments for staging and production, or follow Switch an existing project if you want to change an existing project's shape.

Remove environments: [] if present. Configured environment names must be unique and nonempty, without surrounding whitespace. Commands now reject --set when the project does not enable sets, so remove any unused set selector from shared scripts rather than relying on it being ignored.

Promotion previews

Before applying promotions, review the preview again. Promotion now lints the proposed merged destination, not only the source and destination separately. Reordering global variable overrides is a conflict because order changes evaluation. Source policy follows source order within movable slots. Protected and destination only overrides retain their positions. Destination policy keeps the existing order, and fail policy rejects a reordering conflict.

Attribute directories Breaking

Attribute definitions must sit directly in the configured attributes directory, including inside each Set. Nested files such as attributes/geo/country.yml and dotted names such as attributes/geo.country.yml fail lint with invalid_attribute_namespace, regardless of namespaceCharacter. The configured file extension is allowed. Builds and other commands that read attributes reject these files even without a separate lint step.

Move or rename a definition to attributes/country.yml and update references and application context if it is an independent context key. If it describes nested context, declare attributes/geo.yml as an object attribute with a country property and use attribute: geo.country. Other entities still support namespaces.

Archived references Breaking

Archived definitions are left out of every datafile. A reference to an archived segment silently never matched, and not around it matched everyone. Lint already rejected references to archived features and global variables. It now rejects them for every kind, with archived_reference:

  • Active features, global variables and segments cannot reference an archived feature, global variable, segment or attribute, in conditions, bucketBy or compose.
  • Specs and scenarios cannot test or check an archived definition, or set a sticky value for one.
  • Targets cannot name an archived feature or global variable by its exact key. Patterns with * may still match one.

Archived definitions may still reference each other, so retiring several definitions together needs no particular order. Lint checks an archived definition for its shape only and does not resolve its references, so a definition that only archived ones use can be changed or deleted. Run npx featurevisor lint, then remove each reported reference or unarchive what it points to. Delete specs for archived definitions, and keep the archived files so their keys are not reused.

Sticky values in feature specs Breaking

Feature assertions named their sticky features sticky, while segment and variable assertions and scenarios used stickyFeatures. Feature assertions now use stickyFeatures too, in assertions and children. Lint reports sticky with removed_test_field, and featurevisor test refuses to run a spec that still uses it, in every SDK's runner.

Before
feature: checkout
assertions:
- at: 50
sticky:
pricing:
enabled: true
expectedToBeEnabled: true
After
feature: checkout
assertions:
- at: 50
stickyFeatures:
pricing:
enabled: true
expectedToBeEnabled: true

Feature assertions also accept stickyVariables now, so a feature gated on a global variable can be tested in its own spec. Every child owns its sticky values and does not inherit its parent's. Some earlier native runners passed a parent's sticky values to children that set none, so results for those children can change.

Consistent variable mutations Breaking

Feature variable mutation maps now follow authored order rather than sorting paths by length. When a property should refine a parent assignment, put the parent assignment first. Malformed paths and unsafe property names are rejected.

Run npx featurevisor lint --v3-mutation-order once during migration. It shows affected paths and the v3 and v4 resolved values. Confirm the intended results with tests and adjust order where needed. Normal lint and build commands do not run this comparison.

Ordinary and accumulating overrides share mutation behaviour, including insertion around array indices and complete item replacement. See Mutations for appendUnique, removeValue and examples.

Optional object properties Breaking

Only properties listed in a schema's required array must be present. A declared property outside that list is optional, including when required is omitted. Earlier lint behaviour could require every declared property. If your application needs a property, list it explicitly:

Schema excerpt
type: object
properties:
title: { type: string }
required: [title]

Without required: [title], an object can omit title, and a mutation can remove it. Code generation reflects the optional property. Review application assumptions and schemas when upgrading.

Evaluation details and tests

Update detailed evaluation expectations using the field changes. Lint rejects the removed required and required_features_unmet reasons in expectedEvaluation and expectedEvaluations. Expect conditions_unmet instead, as described in root condition reason.

Lint now rejects expectedVariation on a feature without variations, including expectedVariation: null. Use expectedToBeEnabled for those features instead. Keys in expectedVariables must exist in the feature's variablesSchema. Correct misspelled keys or remove expectations for undeclared variables. These rules also apply to child assertions and matrices. SDK missing value behaviour is unchanged.

Review version comparisons

V4 aligns version comparisons across the SDKs. Before deploying upgraded consumers, test application versions used in conditions, variable requirements and test checks.

ComparisonEarlier behaviour in affected SDKsV4 behaviour
1 or 1.0 compared with 1.0.0Go and Python treated them differentlyEqual
1.0.0-1a compared with 1.0.0-1bJavaScript and Elixir could treat them as equal1a sorts before 1b
1.2.3.4 compared with 1.2.3.5Rust ignored the fourth component.4 sorts before .5
1.0.0-0 compared with 1.0.0PHP could treat them as equalThe prerelease sorts first
1.0.0-alpha compared with 1.0.0-alpha.0Some SDKs treated missing prerelease identifiers as zeroThe shorter prerelease sorts first
Unicode digits or a version embedded in multiple linesSome native parsers accepted themInvalid version

Numeric identifiers now compare exactly without integer overflow or floating point rounding. A prerelease identifier containing letters remains text, even when it starts with a digit. Wildcards apply only to core components. See version comparisons for the complete behaviour.

Keep version dependent targeting unchanged during the SDK rollout unless you have verified its results on both generations. These corrections also affect existing datafiles, so upgrading the CLI later does not defer them.

Review date and regex comparisons

Date strings require a valid Gregorian timestamp, timezone and year from 0001 to 9999. Fractions accept one to nine digits and truncate to milliseconds. Offsets cannot exceed 14:00. The normalized UTC instant must stay in range. Audit values previously accepted through JavaScript date normalization, including invalid dates, leap seconds and hour 24. See date operators.

Case sensitive \d, \D, \w and \W use ASCII classes across SDKs. This does not change \s into an ASCII only class. Unicode case folding with i, anchors and advanced patterns can differ between engines. Test representative inputs in every consuming SDK. See regular expressions.

Test spec keys

Public test keys now omit a terminal .spec: tests/variables/checkoutPolicy.spec.yml becomes variables.checkoutPolicy, without changing the filename. Rename files whose keys collide and refresh saved rerun commands and Catalog links. See test reports.

Adopt new syntax safely

Deploy the v4 JavaScript SDK or the corresponding native major release to every consumer before publishing the capabilities in this table. Include offline applications and indirect dependencies.

SyntaxRisk for an older SDK
continue or precomputeMutations: falseIgnores runtime overrides and returns a default or incomplete value
Unified conditionsDoes not understand conditions. Rule based evaluation can stop matching or return unintended values
Composed global variables and overridesReads a missing default and returns null or an application fallback
isNull, notNull, empty, notEmptyTreats an unknown operator as false, which can become a match under not
includesAny, notIncludesAnyTreats an unknown operator as false, which can become a match under not

V4 builds emit schemaVersion: "4". The v4 SDK reads prepared v3 datafiles, but a v3 SDK does not reject v4 merely because that string changed. Upgrade SDKs first. The v4 CLI does not generate v3 compatible datafiles.

Adopt composition

Follow the same rollout for compose: upgrade every runtime consumer to a v4 SDK, including applications using cached and bundled datafiles, then publish definitions. Targets do not translate compositions for older consumers.

Use compose: supportEmail to alias a global variable's evaluated value, or compose: { support: { variable: supportEmail } } to collect it under an object property. The string is a reference, not a literal result. Aliases inherit the source type and preserve null. Both forms accept overrides after reading their sources. Existing SDK getters are unchanged, so choose the getter for the result type.

Do not add a fake default, type or schema to a composed definition. Root conditions and optional schema checked disabledValue are allowed. The gate runs before sources, and an unmet gate skips sources and overrides. Without disabledValue, it returns null. Compositions have no useDefaultWhenDisabled. Optional composed overrides run after assembly. Regenerate TypeScript code and test composed objects before deploying. Review tracking modules that skip every dependencyOf read: skip dependencyPurpose: 'condition' when tracking consumed values, and handle 'composition' reads as actual use.

Before grouping experiments, review the composition exposure guidance.

Review the advisory composition warnings before publishing. Generated variable and variation members remain nullable, because getters can still return null after an error or module change.

Restrict new operators during rollout

Keep the array list operators blocked until every consuming SDK is upgraded to its compatible release:

featurevisor.config.js
module.exports = {
disallowOperators: ['includesAny', 'notIncludesAny'],
}

Older SDKs can treat these unknown operators as false. Inside a not group, that result can become true and admit users you meant to exclude. Do not rely on an unknown operator failing safely under negation.

Unsupported operators and leaf kinds do not have one portable outcome across older SDKs. Depending on the SDK and selector encoding, they can return false, fail an evaluation or reject a datafile. Valid output from a newer CLI is not necessarily supported by an older SDK. Upgrade every consumer before adopting new syntax, including applications reading cached, persisted, bundled and on demand copies.

If consumers also lack the presence and emptiness operators, block those alongside the two list operators:

featurevisor.config.js
module.exports = {
disallowOperators: [
'isNull',
'notNull',
'empty',
'notEmpty',
'includesAny',
'notIncludesAny',
],
}

Lint and build reject these operators in attribute and variable conditions and tests. Keep disallowOperators restrictions until every consumer supports the blocked operators. This setting blocks named operators only. It does not block new leaf kinds or translate unified conditions, compositions or runtime overrides for older SDKs. Keep unsupported syntax out of shared datafiles rather than relying on an older SDK to reject it. See configuration.

Checks and scenarios require the v4 CLI and matching native test runners. They do not add runtime datafile constraints.

Mixed version deployments

Keep serving prepared v3 datafiles throughout the SDK rollout. If older applications must remain, retain a separately maintained v3 project and CLI output at a different URL. Do not publish a v4 datafile to any of those consumers:

ConsumersExample URLBuild input
Older applications/configuration/v3/production.jsonPrepared v3 definitions and CLI
Upgraded applications/configuration/v4/production.jsonV4 definitions and CLI

Keep the old URL compatible, with separate caches and controlled build inputs. Never redirect it to the new URL or overwrite both with one v4 build. Apply urgent configuration corrections to both.

Ship a compatible bundled fallback with each application. Targets select entities and dependencies. They do not translate new syntax for old SDKs.

Rolling back

Before publishing any v4 datafile, prepared v3 datafiles remain your rollback snapshot during the SDK rollout. An SDK downgrade alone is unsafe after publishing v4 datafiles. Restore compatible v3 datafiles to every affected URL and replace or clear persisted, cached and bundled copies before rolling SDKs back. Confirm the rollback application uses only compatible copies. Otherwise keep upgraded SDKs and restore a known good v4 datafile.

JavaScript SDK usage

Number getter Breaking

Variables that hold numbers have the type number, so the typed getter is now getVariableNumber(). getVariableDouble() was removed. Rename calls to getVariableNumber(), which returns the same value, including for double variables in datafiles built before v4.

After
const context = {
country: 'de',
}
const threshold = f.getVariableNumber('freeShippingThreshold', context)

Datafile update result

setDatafile now returns true when it accepts the update, including unchanged content, and false when it rejects the update or the instance is closed. The default remains merging. Pass true as the second argument to replace the datafile.

const accepted = f.setDatafile(datafile, true)
if (!accepted) {
// Keep using the previous datafile.
}

Existing calls that ignore the result can stay as they are. Review wrappers that declare the old return type, especially when migrating other language SDKs.

Module callbacks Breaking

Module before and after callbacks are removed. Use beforeEvaluation and afterEvaluation instead.

The beforeEvaluation and afterEvaluation callbacks cover both feature evaluations and global variable evaluations. To preserve the behaviour of a callback written only for features, check for featureKey:

Before
your-app/index.js
const f = createFeaturevisor({
datafile,
modules: [
{
before(options) {
return {
...options,
context: { ...options.context, country: 'nl' },
}
},
after(evaluation) {
return evaluation
},
},
],
})
After
your-app/index.js
const f = createFeaturevisor({
datafile,
modules: [
{
beforeEvaluation(options) {
if (!('featureKey' in options)) return options
return {
...options,
context: { ...options.context, country: 'nl' },
}
},
afterEvaluation(evaluation) {
return evaluation
},
},
],
})

Apply the same guard to afterEvaluation if its logic is specific to features. Omit the guard when the callback should also handle global variables. Caller defaults are applied before afterEvaluation runs.

If modules already use both callback families, preserve their ordering when combining them. In v3, all before callbacks run before all beforeEvaluation callbacks, while all afterEvaluation callbacks run before all after callbacks. V4 runs only beforeEvaluation and afterEvaluation, in module order. Simply renaming fields or reordering modules may not preserve those two phases. For interdependent transformations, compose the old callback bodies explicitly in their previous phase order, retaining guards for feature only logic. Check the resulting evaluations before deploying.

Module setup, bucketKey, bucketValue, and close are unchanged.

See Modules.

Sticky feature options and methods Breaking

The sticky option and setSticky() method are removed. Use stickyFeatures and setStickyFeatures() on both parent and child instances:

Before
your-app/index.js
const f = createFeaturevisor({
datafile,
sticky: { checkout: { enabled: true } },
})
f.setSticky({ checkout: { enabled: false } })
const child = f.spawn(context, {
sticky: { checkout: { enabled: true } },
})
After
your-app/index.js
const f = createFeaturevisor({
datafile,
stickyFeatures: { checkout: { enabled: true } },
})
f.setStickyFeatures({ checkout: { enabled: false } })
const child = f.spawn(context, {
stickyFeatures: { checkout: { enabled: true } },
})

Use stickyVariables and setStickyVariables() for global variables. Variables inside a feature remain under that feature's variables property in stickyFeatures.

Passing true as the second setter argument still replaces the corresponding sticky state. Otherwise, it merges. For example, f.setStickyFeatures({}, true) clears sticky features without clearing sticky global variables.

Sticky events Breaking

The combined sticky_set event is removed. Subscribe to the specific events your application needs:

Before
your-app/index.js
const unsubscribe = f.on('sticky_set', (details) => {
refreshFeatures(details.features)
refreshVariables(details.variables || [])
})
After
your-app/index.js
const offFeatures = f.on('sticky_features_set', (details) => {
refreshFeatures(details.features)
})
const offVariables = f.on('sticky_variables_set', (details) => {
refreshVariables(details.variables)
})
const unsubscribe = () => {
offFeatures()
offVariables()
}

Feature events carry { features, replaced }. Global variable events carry { variables, replaced }. The combined compatibility event is no longer emitted. Diagnostics keep their existing sticky_features_set and sticky_variables_set codes, but their details now follow these specific shapes without unrelated empty compatibility arrays.

Evaluation snapshots Breaking

getAllEvaluations() is removed. Use getFeatureEvaluations() with the same arguments on either a parent or a child instance:

Before
const features = f.getAllEvaluations(context)
After
const features = f.getFeatureEvaluations(context)

Use the existing f.getVariableEvaluations(context) separately for global variable snapshots.

Detailed evaluation fields Breaking

Evaluation.required and Evaluation.sticky are removed:

Previous fieldV4
evaluation.requiredevaluation.conditions
evaluation.stickyevaluation.stickyFeature for feature details, when present

Check evaluation.reason === 'sticky' to identify a sticky result. Do not use the presence of stickyFeature as a general sticky check: not every sticky evaluation carries it. Sticky global variables expose their result in variableValue.

Root condition reason Breaking

An unmet root gate now reports conditions_unmet for feature flags and global variables in every SDK. V3 feature flag evaluations used required, and global variable evaluations used required_features_unmet. Update reason comparisons in callbacks, analytics, logs and test expectations. The failed root expression is exposed as evaluation.conditions, not evaluation.requiredFeatures or the unreleased evaluation.requiredVariables. Variation and feature variable evaluations keep their existing disabled reasons and fallbacks.

Before
const evaluation = f.evaluateFlag('checkout', context)
const unmetFeatureRequirement = evaluation.reason === 'required'
After
const evaluation = f.evaluateFlag('checkout', context)
const unmetFeatureRequirement = evaluation.reason === 'conditions_unmet'

Deprecation diagnostic code Breaking

A deprecated global variable now reports deprecated_variable, the code a deprecated feature variable already used, in every SDK. It reported variable_deprecated before, except in Go. Update diagnostic handlers that match the old code. A feature variable's diagnostic details include featureKey, and a global variable's do not.

Dependency purpose Breaking

Feature and variable condition reads carry dependencyPurpose: 'condition', replacing the unreleased 'requirement' purpose. Direct application reads have no dependency purpose. Keep 'composition' reads as consumed values when tracking exposures, and skip eligibility reads tagged 'condition'. Leaves run in authored order with short circuiting, so put cheap attribute leaves first.

Array getter item types Breaking

Without an explicit or inferred item type, getVariableArray() now returns unknown[] | null, rather than assuming every item is a string:

Before
const labels = f.getVariableArray('checkout', 'labels')
// string[] | null
After
const labels = f.getVariableArray<string>('checkout', 'labels')
// string[] | null

The same applies to global variables and child instances. Generics remain optional. They describe the expected item type for TypeScript, but do not validate each item at runtime. The getter still checks that the evaluated value is an array.

Removed types Breaking

Update imports from @featurevisor/sdk or @featurevisor/types where applicable:

Removed typeReplacement
RequiredFeatureCondition for one feature leaf, or Conditions for an expression
RequiredWithVariationFeatureCondition, using feature instead of key
StickySetEventDetailsStickyFeaturesSetEventDetails or StickyVariablesSetEventDetails
GlobalVariableKeyVariableKey

These changes do not remove Featurevisor, FeaturevisorOptions, FeaturevisorChildInstance or the generic variation and variable APIs.

Run TypeScript checks and application behaviour tests. Untyped JavaScript, wrappers and type assertions can hide removed options and callbacks.

What's new

After migration, adopt these features as needed:

FeatureReference
Unified attribute, segment, feature and variable conditionsConditions
Null, empty and nonempty comparisonsOperators
Accumulating global overrides and nested continuationGlobal variables
Global variable aliases, composed objects and overridesCompose values
Array mutations and value replacementMutations
Ordered mutation lists, including repeated pathsMutations
Runtime mutations without continuationPrecompute mutations
Variable comparisons, feature expectations and segment checksChecks
Standalone tests across entitiesScenario testing
Lint warnings for references to deprecated definitionsDeprecated references

Test combinations of matching overrides, nested matches and complete value replacements when adopting accumulation.

Migration checklist

Required for the migration

  • Deploy prepared v3 datafiles, including cached and bundled copies.
  • Replace removed SDK APIs and test callbacks, sticky state and child instances, including getVariableDouble(), now getVariableNumber() in JavaScript, Python, Ruby, PHP and Elixir.
  • Replace reason comparisons from required and required_features_unmet to conditions_unmet, including analytics and test expectations.
  • Replace diagnostic handlers matching variable_deprecated with deprecated_variable.
  • Rename sticky to stickyFeatures in feature specs, including children.
  • Remove references to archived definitions from active definitions and specs, as lint reports with archived_reference.
  • Test failed prerequisites, disabled variations and application fallbacks.
  • Upgrade OpenFeature providers with their SDKs and verify targeting keys.
  • Add stringify to any custom parser before running the v4 CLI.
  • Run the CLI on Node.js 24 or later, including in CI.
  • Rename allowFeatureAndGlobalVariableKeyCollisions to allowFeatureAndVariableKeyCollisions if configured.
  • Check CI scripts that pass --environment, including projects without environments and deployments that need every environment.
  • Upgrade the CLI and native runners, migrate all selectors and gates to conditions, and regenerate code where used. Deploy every SDK consumer before publishing schema version "4" datafiles.
  • Run npx featurevisor lint --v3-mutation-order once and test any changed values.
  • Replace numeric in conditions with or groups of equals. For notIn, wrap that group in not.
  • Update scripts that expect evaluate to succeed for unknown feature or global variable keys.
  • Fix test specs that use expectedVariation on features without variations or unknown keys in expectedVariables. Run lint before test.
  • Move attribute definitions out of subdirectories and remove dots from their names, or model nested context with an object attribute.
  • Resolve invalid_saved_allocation errors before publishing datafiles.
  • Replace double, date and semver types with number or a string format, and fix the values lint then reports.
  • Review date comparisons and regular expressions.
  • Refresh saved test reruns and keep the prepared rollback snapshot.

Only if adopting optional features

For applications on v2, follow the v3 migration guide first.

Previous
Establishing ownership
Next
v3