Headlessly
Experimentation

FeatureFlag

Progressive rollouts, targeting rules, and kill switches for controlled feature releases.

Schema

import { Noun } from 'digital-objects'

export const FeatureFlag = Noun('FeatureFlag', {
  key: 'string!##',
  name: 'string!',
  description: 'string',
  organization: '-> Organization',
  experiment: '-> Experiment',
  type: 'Boolean | String | Number | JSON',
  defaultValue: 'string',
  variants: 'json',
  targetingRules: 'json',
  status: 'Draft | Active | Enabled | Disabled | Paused | RolledOut | Archived',
  rolloutPercentage: 'number',
  evaluations: 'number',
  lastEvaluatedAt: 'datetime',
  rollout: 'RolledOut',
  enable: 'Enabled',
  disable: 'Disabled',
})

Fields

FieldTypeRequiredDescription
keystring (unique, indexed)YesMachine-readable flag key (e.g. onboarding-v2, dark-mode)
namestringYesHuman-readable display name
descriptionstringNoWhat this flag controls and why it exists
organization-> OrganizationNoOrganization that owns this flag
experiment-> ExperimentNoExperiment this flag is associated with
typeenumNoValue type: Boolean, String, Number, or JSON
defaultValuestringNoDefault value when no targeting rules match
variantsjsonNoArray of possible values for multivariate flags
targetingRulesjsonNoRules that determine which users see which variant
statusenumNoDraft, Active, Enabled, Disabled, Paused, RolledOut, or Archived
rolloutPercentagenumberNoPercentage of traffic receiving the feature (0-100)
evaluationsnumberNoTotal number of flag evaluations
lastEvaluatedAtdatetimeNoTimestamp of the most recent evaluation

Relationships

FieldDirectionTargetDescription
organization->OrganizationThe organization that owns this flag
experiment->ExperimentThe experiment this flag is part of (if any)

Verbs

VerbEventDescription
createCreatedCreate a new feature flag
updateUpdatedUpdate flag configuration
deleteDeletedDelete a feature flag
rolloutRolledOutSet rollout to 100% and mark as fully rolled out
enableEnabledEnable the flag for evaluation
disableDisabledDisable the flag, returning default value for all evaluations

Verb Lifecycle

import { FeatureFlag } from '@headlessly/experiments'

// BEFORE hook -- validate before enabling
FeatureFlag.enabling(flag => {
  if (!flag.defaultValue) {
    throw new Error('Default value must be set before enabling')
  }
})

// Execute -- enable the flag
await FeatureFlag.enable('featureFlag_jK8sTxJv')

// AFTER hook -- track the change
FeatureFlag.enabled((flag, $) => {
  $.Event.create({
    type: 'feature_flag.enabled',
    data: {
      key: flag.key,
      rolloutPercentage: flag.rolloutPercentage,
    },
  })
})

Status State Machine

Draft --> Active --> Enabled --> RolledOut --> Archived
                       |
                       v
                    Disabled --> Archived
                       |
                       v
                     Paused --> Enabled
  • Draft: Flag created but not yet configured for use
  • Active: Configured and ready for evaluation
  • Enabled: Actively being evaluated against targeting rules
  • Disabled: Temporarily turned off, all evaluations return the default value
  • Paused: Evaluation suspended, typically during an incident
  • RolledOut: Fully rolled out to 100% of traffic, experiment concluded
  • Archived: Historical record, no longer evaluated

Cross-Domain Patterns

FeatureFlag is the control mechanism that gates features across every domain:

  • Experimentation: Flags belong to Experiments. When an experiment concludes, the winning flag variant is rolled out to 100%.
  • Analytics: Every flag evaluation emits an Event. Conversion metrics can be segmented by flag variant.
  • Platform: Workflow steps can be gated by feature flags. Agents check flags before executing actions.
  • Marketing: Campaign features can be progressively rolled out via flags. Form variants use flags for A/B testing.
  • CRM: Contact-level flag targeting enables personalized feature access based on organization tier, lead score, or stage.
import { FeatureFlag } from '@headlessly/experiments'

// When a flag is fully rolled out, clean up
FeatureFlag.rolledOut((flag, $) => {
  // Conclude the parent experiment if one exists
  if (flag.experiment) {
    $.Experiment.conclude(flag.experiment)
  }

  // Log the rollout as a metric
  $.Metric.create({
    name: `Flag rolled out: ${flag.key}`,
    type: 'Milestone',
    value: flag.evaluations,
  })
})

// Kill switch pattern -- disable immediately on incident
FeatureFlag.disabled((flag, $) => {
  $.Event.create({
    type: 'feature_flag.killed',
    data: {
      key: flag.key,
      evaluationsAtKill: flag.evaluations,
    },
  })
})

Query Examples

SDK

import { FeatureFlag } from '@headlessly/experiments'

// Find all enabled flags
const enabled = await FeatureFlag.find({ status: 'Enabled' })

// Get a specific flag
const flag = await FeatureFlag.get('featureFlag_jK8sTxJv')

// Create a progressive rollout flag
await FeatureFlag.create({
  key: 'new-dashboard',
  name: 'New Dashboard UI',
  type: 'Boolean',
  defaultValue: 'false',
  rolloutPercentage: 10,
  targetingRules: [
    { attribute: 'org.tier', operator: 'in', values: ['Enterprise', 'Business'] },
  ],
})

// Gradually increase rollout
await FeatureFlag.update('featureFlag_jK8sTxJv', { rolloutPercentage: 50 })

// Full rollout
await FeatureFlag.rollout('featureFlag_jK8sTxJv')

MCP

headless.ly/mcp#search
{
  "type": "FeatureFlag",
  "filter": { "status": "Enabled" },
  "sort": { "evaluations": "desc" },
  "limit": 50
}
headless.ly/mcp#fetch
{ "type": "FeatureFlag", "id": "featureFlag_jK8sTxJv" }
headless.ly/mcp#do
const flags = await $.FeatureFlag.find({ status: 'Enabled' })
await $.FeatureFlag.disable('featureFlag_jK8sTxJv')

REST

# List enabled flags
curl https://experiment.headless.ly/~acme/feature-flags?status=Enabled

# Get a specific flag
curl https://experiment.headless.ly/~acme/feature-flags/featureFlag_jK8sTxJv

# Create a feature flag
curl -X POST https://experiment.headless.ly/~acme/feature-flags \
  -H 'Content-Type: application/json' \
  -d '{"key": "new-dashboard", "name": "New Dashboard UI", "type": "Boolean", "defaultValue": "false"}'

# Enable a flag
curl -X POST https://experiment.headless.ly/~acme/feature-flags/featureFlag_jK8sTxJv/enable

# Disable a flag (kill switch)
curl -X POST https://experiment.headless.ly/~acme/feature-flags/featureFlag_jK8sTxJv/disable

# Full rollout
curl -X POST https://experiment.headless.ly/~acme/feature-flags/featureFlag_jK8sTxJv/rollout

On this page