Featurevisor

Definitions in depth

Metadata

Metadata records what a team knows about a definition, such as who owns it and when it should be removed. It is for people, so it never reaches a datafile and never changes what users get. This page defines what metadata may hold, gives checkout an owner and two deadlines, then has lint check them.

Define the schema

Start by declaring what your team records, in metadataSchema. Here a definition can name an owner from a list of teams, and a jira ticket:

featurevisor.config.js
module.exports = {
metadataSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
enum: ['checkout-team', 'billing-team', 'payments-team'],
},
jira: {
type: 'string',
pattern: '^[A-Z]+-[0-9]+$',
},
},
},
}

The schema takes the keywords of reusable schemas, and its root is an object. Declared properties close metadata, as they close an object in any schema, so lint rejects a key the schema does not declare. Add additionalProperties: {} beside them to allow other keys. A key holding a moment, such as launchedOn, takes format: date-time, which accepts a timestamp with a timezone.

owner is one of two keys Featurevisor reads itself, and deadlines is the other. The schema is optional. Without one, lint checks only the shape of those two keys, and accepts any other key.

Add metadata

Give a definition a metadata object, with keys the schema declares:

features/checkout.yml
description: Checkout redesign
metadata:
jira: CHECKOUT-123
bucketBy: userId
rules:
- key: netherlands
conditions: netherlands
percentage: 100
- key: everyone
conditions: "*"
percentage: 25

Features, global variables, segments, attributes, targets and reusable schemas take metadata. Its values can be text, numbers, true and false, null, lists and objects, nested as deep as you like. Test specs do not take it.

Datafiles leave metadata out, so adding it, changing it or removing it builds the same datafiles and the same revision. fmt places it after description and tags, and keeps its keys in the order you wrote them.

Name an owner

owner names who to ask about a definition. It is a name, or a list of names:

features/checkout.yml
# ...
metadata:
owner: checkout-team
jira: CHECKOUT-123
features/pricing.yml
# ...
metadata:
owner:
- billing-team
- checkout-team

With a list, each name must be one the schema allows. Catalog shows the owner on the definition's Overview and on its row in every list, and lets you list everything a team owns. So does list --owner.

An owner records who to ask. It does not decide who reviews a change, which is what a CODEOWNERS file does. Use both, and change them together, as Establishing feature ownership shows.

Deadlines

deadlines is the other key Featurevisor reads, beside owner. It lists what should happen to a definition, each by a moment and under a type such as review or remove.

Define deadline types

Declare your project's types under properties.deadlines in metadataSchema:

featurevisor.config.js
module.exports = {
metadataSchema: {
type: 'object',
properties: {
// ...
deadlines: {
type: 'object',
properties: {
review: {
description: 'Check the flag still earns its place',
},
remove: {
description: 'Remove the flag and the code that reads it',
properties: {
ticket: { type: 'string' },
},
required: ['ticket'],
},
},
},
},
},
}

Name types after what should happen. A type starts with a letter and has only letters, digits, _ and -. Catalog writes notifyCustomers as "Notify customers", and shows each type's description beside its deadlines.

Every deadline has these fields, whatever its type:

FieldRequiredValue
byYesA timestamp with a timezone
doneNotrue, or the timestamp it was done
descriptionNoWhat should happen, in words

A type can add fields of its own, such as ticket, and required makes them necessary. They can have any name but these three. Lint rejects any other field, so a type has no additionalProperties.

Once types are declared, lint allows only those. Two more keywords, beside properties, shape the list of types:

  • required: ['remove'] makes every definition with deadlines plan a removal.
  • additionalProperties: {} allows types you have not declared, alongside the ones you have.

Without declared types, any type is allowed.

Plan deadlines

Give checkout a deadline of each type the schema declares:

features/checkout.yml
# ...
metadata:
owner: checkout-team
jira: CHECKOUT-123
deadlines:
review:
by: "2027-01-15T09:00:00Z"
remove:
by: "2027-03-01T09:00:00Z"
description: Once every market is on the new checkout
ticket: CHECKOUT-456

review needs only by, and remove also carries the ticket its type requires.

Write timestamps in ISO 8601, as conditions do: "2027-03-01T09:00:00Z" in UTC, or "2027-03-01T10:00:00+01:00" with an offset. A deadline is due at that moment. Quote them, as fmt writes them, so other YAML tools read them as text too. A date alone names a different moment in every timezone, so lint refuses it:

Output
features/checkout.yml
=> Error: Expected a timestamp with a timezone, such as 2027-03-01T09:00:00Z. A date alone has no time or timezone.
Path: metadata.deadlines.remove.by

Nothing changes for users when a deadline passes. The flag stays as it is, and until someone acts on it, Catalog shows the deadline as overdue and lint warns about it without failing.

Mark a deadline done

Set done to the moment the work was done:

features/checkout.yml
# ...
metadata:
owner: checkout-team
jira: CHECKOUT-123
deadlines:
review:
by: "2027-01-15T09:00:00Z"
done: "2027-01-10T16:30:00Z"
remove:
by: "2027-03-01T09:00:00Z"
description: Once every market is on the new checkout
ticket: CHECKOUT-456

done: true works too, when the moment does not matter. To plan a review again, such as every quarter, move by to the next one and remove done.

Each deadline has one status, worked out at the moment you look:

StatusWhen
Donedone is true or a timestamp
OverdueNot done, and its moment has passed
Due todayNot done, and due later today
UpcomingNot done, and due within the next 30 days
LaterNot done, and due after that

A deadline type is only a name. Marking remove done does not archive the feature, and setting deprecated: true does not mark a deprecate deadline done.

Check it with lint

Lint checks metadata against the schema. Leave out ticket and add an undeclared cleanup deadline, and lint reports both:

Output
features/checkout.yml
=> Error: Missing "ticket", which "remove" deadlines require.
Path: metadata.deadlines.remove.ticket
=> Error: Unknown deadline type "cleanup". This project allows "review" and "remove".
Path: metadata.deadlines.cleanup

Misspell the owner of checkout, and lint names the teams the schema allows:

Output
features/checkout.yml
=> Error: Expected "checkout-team", "billing-team" or "payments-team".
Path: metadata.owner

A misspelt key fails too, since declared properties close metadata:

Output
features/checkout.yml
=> Error: Unknown metadata key "ownr". This project's metadataSchema does not declare it. Did you mean "owner"?
Path: metadata.ownr

Lint also warns about each open deadline that is overdue or due today. A warning never fails lint, so a deadline passing never blocks a release. --now takes a timestamp and checks deadlines as they stand at that moment, such as the day after checkout's removal was due:

Command
npx featurevisor lint --now=2027-03-02T12:00:00Z
Output
=> Warning: The "remove" deadline was due by 2027-03-01T09:00:00Z and is not done. Set `done` once the work is done, or move `by` later. (feature checkout: metadata.deadlines.remove)
✔ No lint errors found

A deadline due today warns with deadline_due_today, and an overdue one with deadline_overdue. Deadlines that are done, and archived definitions, never warn.

To fail lint on overdue deadlines, name the code with --fail-on. Each one then prints and counts as an error:

Command
npx featurevisor lint --fail-on=deadline_overdue

Run it in a scheduled job, and keep plain lint on pull requests, so a deadline passing never blocks unrelated work.

Require metadata

required applies only to definitions that have metadata. To require metadata itself, list the kinds of definition in requireMetadataIn:

featurevisor.config.js
module.exports = {
requireMetadataIn: ['features', 'variables'],
metadataSchema: {
type: 'object',
required: ['owner'],
// ...
},
}

Every feature and global variable now needs an owner. Archived definitions are exempt. Lint names each one that has no metadata:

Output
features/showCookieBanner.yml
=> Error: Missing metadata, which this project requires on features.
Path: metadata

An empty metadata: {} records nothing, so lint refuses it wherever it is written, and it never stands in for the metadata a project requires.

The kinds are features, variables, segments, attributes, targets and schemas. One schema applies to every kind, so a required deadline type applies to segments too, when they have deadlines.

Find it from the CLI

list filters by owner and by deadline. List what a team owns:

Command
npx featurevisor list --features --owner=checkout-team
Output
Features
• checkout
• pricing
Found 2 features.

--deadline takes a status or a type, in any case. Statuses are overdue, today, upcoming (today or within 30 days), later, open (not done) and done. --now fixes the moment statuses are worked out at, so a report reads the same on every run. Here pricing also plans a review by "2027-02-01T09:00:00Z":

Command
npx featurevisor list \
--features \
--deadline=open \
--now=2027-02-15T12:00:00Z
Output
Features
• checkout remove by 2027-03-01T09:00:00Z, upcoming
• pricing review by 2027-02-01T09:00:00Z, overdue
Found 2 features.

Repeat --deadline to describe one deadline more closely. --deadline=remove --deadline=upcoming finds removals that are coming up, not definitions with a removal and something else coming up. Repeat --owner to list what several teams own together.

Add --json, and each definition comes with deadlineStatuses, the status of every deadline it has. A scheduled CI job can post the overdue ones to your team's chat.

See it in Catalog

Catalog shows a definition's owner beside its other facts, and the rest of its metadata below them. Its deadlines come next, open ones first, above its description. Links in metadata open when they are http or https URLs.

Lists filter by owner:, deadline: and metadata:, such as metadata:jira=CHECKOUT-123, and search finds owners and metadata values. The home page lists overdue deadlines under Needs attention and what is coming up below it, and a Deadlines page lists every deadline in the project.

Details

  • Metadata is read as JSON would write it. A TOML date and time such as 2027-03-01T09:00:00Z reads as the text "2027-03-01T09:00:00.000Z", and a value with no JSON form, such as a function, fails lint. A TOML date, or a time without an offset, fails as a deadline, as in YAML.
  • Due today and the next 30 days count calendar days where the reader is: in Catalog, the browser's timezone, and in the CLI, the machine's. Overdue is the same moment everywhere.
  • Metadata is as visible as the rest of a definition, including in a published Catalog. Keep secrets, credentials and personal data out of it.
  • Changing only metadata keeps a saved test report current in Catalog, since metadata cannot change a result.
  • diff reports changes to an owner and to each deadline by its type. Promotions carry metadata with the definition, and convert treats it as shared by every environment.
Previous
Testing