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:
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:
description: Checkout redesignmetadata: jira: CHECKOUT-123bucketBy: userIdrules: - key: netherlands conditions: netherlands percentage: 100 - key: everyone conditions: "*" percentage: 25Features, 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:
# ...metadata: owner: checkout-team jira: CHECKOUT-123# ...metadata: owner: - billing-team - checkout-teamWith 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:
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:
| Field | Required | Value |
|---|---|---|
by | Yes | A timestamp with a timezone |
done | No | true, or the timestamp it was done |
description | No | What 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:
# ...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-456review 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:
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.byNothing 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:
# ...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-456done: 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:
| Status | When |
|---|---|
| Done | done is true or a timestamp |
| Overdue | Not done, and its moment has passed |
| Due today | Not done, and due later today |
| Upcoming | Not done, and due within the next 30 days |
| Later | Not 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:
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.cleanupMisspell the owner of checkout, and lint names the teams the schema allows:
features/checkout.yml => Error: Expected "checkout-team", "billing-team" or "payments-team". Path: metadata.ownerA misspelt key fails too, since declared properties close metadata:
features/checkout.yml => Error: Unknown metadata key "ownr". This project's metadataSchema does not declare it. Did you mean "owner"? Path: metadata.ownrLint 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:
npx featurevisor lint --now=2027-03-02T12:00:00Z=> 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 foundA 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:
npx featurevisor lint --fail-on=deadline_overdueRun 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:
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:
features/showCookieBanner.yml => Error: Missing metadata, which this project requires on features. Path: metadataAn 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:
npx featurevisor list --features --owner=checkout-teamFeatures • checkout • pricingFound 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":
npx featurevisor list \ --features \ --deadline=open \ --now=2027-02-15T12:00:00ZFeatures • checkout remove by 2027-03-01T09:00:00Z, upcoming • pricing review by 2027-02-01T09:00:00Z, overdueFound 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:00Zreads 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.
diffreports changes to an owner and to each deadline by its type. Promotions carry metadata with the definition, andconverttreats it as shared by every environment.

