Define your configuration
Attributes
Attributes declare the context your application passes in, such as a user's country or plan. Conditions in segments, force entries and variable overrides compare against them, and bucketBy uses them to keep each user's result stable.
Featurevisor can only target what your application passes in. Before planning a rollout by plan, region or app version, check that the attribute is declared here and that your application sends it.
Create an attribute#
Each attribute is a YAML file in attributes/. The file name is its key, so this file declares country:
description: Countrytype: stringdescription is required. Define either type or oneOf, not both. A few naming rules keep every key usable in every SDK:
- Attribute files sit directly in
attributes/, not in subdirectories. See Namespaces. - The key cannot contain dots, even when
namespaceCharacteris/. For nested context, declare an object attribute instead. - Attribute and property names cannot be names that JavaScript objects inherit, such as
constructorortoString. The JavaScript SDK would find a value your context never supplied.
Why declare attributes#
Attributes never reach your datafiles. They exist while you author, so mistakes are caught before a change ships. Give plan the values it can take:
description: Subscription plantype: stringenum: - free - paid - enterpriseNow suppose a segment misspells one of them:
description: Accounts on the paid planconditions: - attribute: plan operator: equals value: piadnpx featurevisor lintsegments/paidAccounts.yml => Error: Value does not match the schema of attribute "plan". Path: conditions.0.valueLinting checks every condition and bucketBy against the attributes you declare. A misspelled attribute gets a suggestion:
=> Error: Unknown attribute "plann". Did you mean "plan"?An operator that does not suit the type fails too. greaterThan on plan reports that it can only be used with integer or number attributes.
Code generation turns the same declarations into types for your application's context:
// attributes.tsexport type PlanAttribute = "free" | "paid" | "enterprise";// context.tsexport interface Context { country?: CountryAttribute; deviceId?: DeviceIdAttribute; plan?: PlanAttribute; userId?: UserIdAttribute; [key: AttributeKey]: AttributeValue;}Types#
Set type to one of the following values:
| Type | Accepted values | Example context entry |
|---|---|---|
boolean | true or false | betaTester: true |
string | Text | country: 'nl' |
integer | Whole numbers | age: 30 |
number | Numbers, including decimals | rating: 4.5 |
array | Arrays of strings | permissions: ['read', 'write'] |
object | Flat objects | account: { locale: 'nl-NL' } |
A string can also declare a format for timestamps and versions.
boolean#
Use boolean for a yes or no fact, such as whether the user joined a beta programme:
description: Joined the beta programmetype: booleanCompare it with equals and true or false.
string#
Use string for text, such as a country code. This is country from the scaffold:
description: Countrytype: stringStrings accept the most operators, from equals and in to startsWith and matches. Add enum to allow only listed values, as plan does above.
integer#
Use integer for whole numbers, such as the user's age:
description: Age in yearstype: integerCompare it with greaterThan, lessThanOrEquals and the other number operators.
number#
Use number for numbers that can have decimals, such as the average rating a user gives:
description: Average rating given by the usertype: numberBoth 4 and 4.5 are numbers. The number operators work on them as they do on integers.
array#
Use array for an array of strings. To limit its values, describe each item with items:
description: Permissionstype: arrayitems: type: string enum: - read - write - adminArray attributes only hold strings. items is required as soon as an array declares enum, const, minItems, maxItems or uniqueItems.
object#
Use object for a flat object, and list its properties:
description: Account detailstype: objectproperties: locale: type: string seats: type: integerrequired: - localeProperties are optional unless listed in required. Without required, every property is optional. required only lists keys declared in properties, so an object without properties has no required keys.
Object attributes must stay flat. Their properties cannot be objects. In segment conditions, refer to a property with a dotted path, such as account.locale or account.seats.
Formats#
A string attribute can declare a format. Each format changes what lint accepts and which operators it allows. Only these two exist, and any other format is a lint error:
| Format | Accepted values | Example context entry |
|---|---|---|
date-time | ISO 8601 timestamps with a timezone | signupDate: '2025-01-15T12:30:00Z' |
semver | Versions the version operators compare | version: '5.5.0' |
date-time#
Use format: date-time for a timestamp with a timezone, such as 2025-01-15T12:30:00Z or 2025-01-15T13:30:00+01:00:
description: Signup datetype: stringformat: date-timeCompare timestamps with before and after. The only other permitted operators are exists, notExists, isNull and notNull. Equality, string matching and emptiness operators are rejected, including on timestamp properties inside objects, because one instant has many spellings.
Every enum entry and const must be a portable timestamp. Date only strings and epoch numbers are not timestamps. Code generation uses Date | string for an unconstrained timestamp attribute, since context can hold a native date. See native date inputs for other SDKs.
To compare a timestamp as text too, for example with equals, declare it as a plain string without a format. before and after still work on plain strings, but lint stops checking that values are timestamps. Do not combine both in oneOf. A valid timestamp matches both branches, and oneOf needs exactly one.
semver#
Use format: semver for a version string such as 5.5.0:
description: Version number of the apptype: stringformat: semverCompare versions with the semantic version operators:
description: App version 5.5.0 or laterconditions: - attribute: version operator: semverGreaterThanOrEquals value: '5.5.0'The format accepts every version these operators can compare, not only strict semantic versions. 5.5, v5.5.0 and 5.x are valid, while 5.5,0 is not. Lint checks enum, const and condition values against it, so a version that could never match fails before it ships. The version operators also work on plain strings.
Quote versions in YAML. Unquoted, 5.5 is read as a number. Pass the version as a string in runtime context too, for example { version: '5.5.0' }.
oneOf#
Use oneOf instead of a top level type when an attribute must match exactly one of several schemas. It needs at least two:
description: External identifier supplied as text or an integeroneOf: - type: string - type: integerConstrain values#
These properties narrow what an attribute accepts. Linting checks condition values against all of them. Code generation turns enum and const into exact types and makes required properties non-optional.
| Properties | Applies to | Purpose |
|---|---|---|
enum, const | Any type | Allow only listed values, or a single value |
minLength, maxLength, pattern | string | Limit length or match a regular expression |
format | string | Accept only timestamps or versions |
minimum, maximum | integer and number | Limit the numeric range |
items, minItems, maxItems, uniqueItems | array | Describe items and limit their count |
properties, required, additionalProperties | object | Declare keys and which must be present |
enum and const values must match type and format, and required can only list keys from properties. Lengths and item counts are whole numbers of zero or more. Schemas describes each property in detail.
Context#
An SDK evaluates against the context your application passes. Its keys are attribute keys, and object attributes are passed as objects:
const context = { userId: '8f2a', country: 'nl', plan: 'paid', account: { locale: 'nl-NL', seats: 12, },}f.isEnabled('checkout', context)- Keys that no condition or
bucketByuses have no effect. - When an attribute is missing from the context, most operators return
false.notEqualsand the presence operators are the exceptions listed in Operators. - SDKs do not check context types at runtime, because attributes are not in datafiles. In TypeScript, use the generated
Contexttype above.
Lint warns about test contexts that do not match any declared attribute type or constraint. Warnings identify the received value and its context path. CLI evaluate gives the same nonblocking warnings for supplied context. Neither changes the evaluation result, and SDKs do not emit these warnings. See Context warnings.
Archiving#
Set archived: true on an attribute you no longer use:
archived: truedescription: Device IDtype: stringAn archived attribute is left out of generated code. Linting stops an active segment, feature or global variable from still using it, including in bucketBy, so find and remove its references first. Archived definitions that use it do not count. Keep the file rather than deleting it, so the key is not reused with another meaning. You can list archived attributes afterwards:
npx featurevisor find-usage --attribute=deviceIdnpx featurevisor list --attributes --archivedPromotable#
In a project that uses sets, an attribute can protect its existing destination definition from later promotions by setting promotable: false at the top level:
description: Countrypromotable: falsetype: stringIf the destination attribute exists, it remains unchanged when either the source or destination attribute has this field. A missing destination attribute is still created and retains promotable: false. The field does not apply to nested attribute schema properties.
Related#
- Segments, which build audiences from attributes
- Condition operators
- Context in the JavaScript SDK
- Code generation

