Skip to content

Your flags live in the repo. Reviewed like code.

Declare your flags in a YAML file, read the diff in a pull request, apply it on merge. One file and one binary. No state bucket, no provider, no HCL.

GitHub GitLab Bitbucket Azure DevOps Jenkins Or a laptop
flagdash.yaml +1 −1
flags:
  search-v2:
    environments:
      production:
        enabled: true
-       rollout_percentage: 10
+       rollout_percentage: 25
        included_keys: [beta-tester-1]
One line. That is the whole change set.
pull request #418 · checks
$ flagdash plan
~ search-v2 production rollout_percentage: 10 25
1 change. Exit code 2.
$ flagdash apply # on merge
search-v2 production rollout_percentage → 25
Exit 0 for no changes, 2 for changes pending, 1 for an error. Any CI gates on that.
Declare Review Merge Apply

Start from what you already have

Nobody hand-writes a schema for a project that already exists. Export your current configuration, commit it, and the first plan should report no changes at all.

Export the project you have

Every flag, every environment, every targeting rule, written out as the document the platform would produce for the state it is in right now. The dashboard has the same button, with guided setup, at Dashboard → GitOps.

Commit it, and prove nothing moved

No changes. Your file matches FlagDash. Exit code 0. Now the file is a fact about production rather than a hopeful description of it.

Change it in a pull request, apply it on merge

Normally from CI on merge. It works from a laptop too, which matters the first week and during an incident.

Autocompletion and inline errors, in your editor

The document has a published JSON Schema. Add one comment at the top of the file and your editor validates operators, enum values and field names as you type, before CI ever sees them.

No state bucket. No provider. No HCL.

The usual route is a general-purpose infrastructure-as-code tool, which has to remember what it created. That memory lives somewhere, and protecting it becomes your job.

The state-file path
Before you define a single flag
  1. 1 A cloud storage bucket for the state file, which you now pay for and back up.
  2. 2 Workload identity federation so CI can reach it: trust policy, audience, service account, role.
  3. 3 State locking, so two pipelines cannot corrupt it.
  4. 4 A provider to pin, and a second configuration language to learn.
  5. 5 Five files that all have to agree.
A flag toggled by an operator is reverted on the next apply.
Ownership is whatever the state file says it is, so it can be wrong.
The FlagDash path
One YAML file and one binary
  1. 1 Put a management API key in your CI secrets.
  2. 2 flagdash gitops export > flagdash.yaml
  3. 3 Commit the file. Run plan on pull requests and apply on merge.

FlagDash already knows what it created. Ownership is a column on the row, so "what would change?" is a query. Nothing to lock, because there is no second copy of the truth.

A flag toggled by an operator is reported, never silently reverted.
Adopt one flag at a time. Anything not in the file stays fully editable.
0
state buckets to provision
1
file, or a flagdash.d/ directory
1
secret in CI

Works with your CI, whatever it is

There is no plugin to install and no marketplace app to authorise. The integration point is a binary and three exit codes, so anything that can run a command and hold a secret is already supported. All four recipes below do the same thing: plan on a pull request, apply on merge.

GitHub Actions .github/workflows/flagdash.yml
steps:
  - uses: actions/checkout@v4
  - run: npm install -g @flagdashio/cli
  - name: Plan
    if: github.event_name == 'pull_request'
    run: flagdash plan --format markdown | tee plan.md
    env:
      FLAGDASH_API_KEY: ${{ secrets.FLAGDASH_API_KEY }}
  - name: Comment the plan
    run: gh pr comment ${{ github.event.number }} --body-file plan.md
  - name: Apply
    if: github.ref == 'refs/heads/main'
    run: flagdash apply
--format markdown is there so the plan reads as a review comment. The diff shows up where the reviewer already is.
GitLab CI .gitlab-ci.yml
flagdash:plan:
  image: node:22
  rule: merge_requests
  script:
    - npm install -g @flagdashio/cli
    - flagdash plan --format markdown > plan.md
    - curl --form "body=<plan.md" "$CI_API_V4_URL/…/notes"
  variables:
    FLAGDASH_API_KEY: $FLAGDASH_API_KEY
flagdash:apply:
  image: node:22
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - flagdash apply
The merge request note is posted with GitLab's own API. No FlagDash-specific integration is involved.
Bitbucket Pipelines bitbucket-pipelines.yml
pipelines:
  pull-requests:
    '**':
      - step:
          image: node:22
          script:
            - npm install -g @flagdashio/cli
            - flagdash plan
  branches:
    main:
      - step:
          image: node:22
          script:
            - npm install -g @flagdashio/cli
            - flagdash apply
The step fails on a non-zero exit, which is exactly the gate you want: a pending change stops the pipeline until somebody looks.
Anything else Jenkins · Azure DevOps · cron · a laptop
# install, either way
npm install -g @flagdashio/cli
brew install flagdash/tap/flagdash
# plan exits 2 when there are changes
flagdash plan || [ $? -eq 2 ]
# apply on the default branch
flagdash apply
plan exits 2 for pending changes, which is a signal and not a failure. Pass --exit-zero when a step's success matters more than the signal.
0 No changes
2 Changes pending
1 Error
The 2am problem

Drift is reported. Never silently reverted.

Somebody hit a kill switch during an incident. The document still says the old value. What happens on the next merge is the whole question, and most tooling answers it by quietly undoing the fix.

the morning after
$ flagdash plan
! kill-switch production enabled: true false
changed outside the repository at 2026-08-30T02:14Z by ops@acme.com
$ flagdash apply
Error: drift_detected
These resources were changed outside the repository since the last sync.
Update the document to match, or re-run with --accept-drift to overwrite them.
The apply stops. It does not choose for you.

Why a state file cannot answer this

A tool that keeps its own state file compares two documents, so its only move on a conflict is to make one of them win, and the one that wins is the file. The operator's incident fix disappears on the next merge with nobody being told. Systems that sync in both directions resolve the same race first-come first-served, which has the same outcome and is harder to predict.

FlagDash holds the state, so a managed row is stamped when it changes outside a sync. That is not a comparison, it is a fact recorded at the moment it happened, with a timestamp and the person who did it. Having the fact is what makes refusing possible.

Two ways forward, both explicit

Keep what the operator did. Bring the file up to date:

Or overwrite it, on purpose:

There is no third option where it happens without you.

A managed flag still toggles in an emergency

Writes to a managed flag come back flag_managed, so the dashboard renders it read-only. The control is locked, not removed: there is a break-glass action behind it, because refusing a kill switch during an outage would make GitOps the reason the outage lasted longer. Taking it records who did it and when, and the next plan says so.

The repo owns the fields it mentions

Mention a field to own it. Omit it and it stays yours to change in the dashboard.

flagdash.d/search-v2.yaml
flags:
  search-v2:
    environments:
      production:
        rollout_percentage: 25   # the repo owns this
        # `enabled` is unmentioned, so
        # ops still own the kill switch

The rollout is enforced from the file

Moving 25% to 50% takes a pull request and a review, which is what a rollout deserves.

The kill switch stays an operator's to flip

No pull request, no merge queue, nobody to wake up. The field was never claimed.

Adopt one flag at a time

A flag becomes managed once it appears in your file and you apply. Anything not in the file is untouched, so you never surrender a project to get started.

One file

Small teams
flagdash.yaml

In the repository root. Everything in one place, which is the right call while everything fits on a screen.

One file per flag

Recommended
flagdash.d/*.yaml

Merged in filename order. Prefer this on any team with more than a couple of people: two people changing two flags never conflict, which a single file cannot promise.

"Who turned this on?" answers with a person and a commit

A CI key is not an answer. Every sync records the commit as the actor, so the audit log names the author and points at a diff you can go and read.

Audit log actor_type: gitops
gitops.sync_applied 3 flags updated, 1 segment created amara@acme.com · checkout-api@a41f9c3
flag.environment_updated search-v2 · production · 10 → 25 amara@acme.com · checkout-api@a41f9c3
flag.toggled kill-switch · production · break glass ops@acme.com · dashboard
One gitops.sync_applied entry with the summary, plus the ordinary per-resource entries every flag change produces. A gitops.sync_applied webhook event fires on the project too.

The pull request review is the approval

If the project requires approval for flag changes, a sync carries the repository, commit and author, and that is recorded as the approving evidence. There is deliberately no bypass flag: two approval systems that can each block the other produce a deadlock, and letting CI skip the platform's approval would make the platform's approval meaningless.

--prune can only reach what the repo owns

It deletes managed resources that have disappeared from your file. A flag created in the dashboard is not "missing from the file"; it was never in it. Off by default, and every deletion appears in the plan before it happens.

One secret, two scopes

A management API key with flags:write and segments:write. Read from FLAGDASH_API_KEY and FLAGDASH_PROJECT_ID, or passed as flags, or written as project: in the file.

You never write a rule id

Rules are matched across a diff by content, then by position, and the existing id is carried forward, because the id salts the rollout bucket, and a new one would silently reshuffle who is inside a partial rollout. Pin an id: only when you mean a fresh cohort.

Per-environment fields

enabled value rollout_percentage included_keys excluded_keys rules variations

Percentages are percentages. A 10% rollout is rollout_percentage: 10, not [10000, 90000] in thousandths. Bucketing is 1% granular and the file says so.

Rule operators

eq neq contains starts_with ends_with in not_in gt lt gte lte regex in_segment not_in_segment

A rule whose conditions match but whose rollout_percentage excludes the user falls through to the next rule. That is what makes "10% of EU, everyone else on the old path" two ordered rules.

Also in the document

segments: for project-scoped audiences, referenced by in_segment
variations: for weighted A/B, weights summing to 100
defaults: applied to any flag that omits them
tags, owner, lifecycle_type, archived

A suspended account can still run plan because reading what would change costs nothing. Only apply is blocked.

The obvious question

Why not just use the dashboard?

For a kill switch at 3am, you should. The dashboard is built for one person changing one thing right now, and nothing here takes that away.

These are the six things a dashboard structurally cannot do.

01

Ship the flag with the code

In the dashboard these are two acts in two systems. Deploy code that reads a flag before the flag exists and production quietly serves the default. In a file they are one commit, reviewed together.

02

Review before, not after

An audit log tells you what already reached customers. A pull request is somebody checking the change before it does. For "raise EU rollout to 50%", before beats after.

03

Change eight flags at once

A launch needs several flags to flip together. In a UI that is eight clicks, live one at a time, in whatever order you remember. Here it is one merge: all of them, or none.

04

Actually revert

You can toggle a boolean back. You cannot un-edit a four-condition targeting rule and be sure you restored last week's exact state. In git, the previous state is the previous commit.

05

Replicate a whole project

New region, new environment, disaster recovery, a customer-specific instance. Apply the file. The alternative is copying two hundred flags by hand and hoping.

06

A second pair of eyes

You typed 100, you meant 10. A reviewer catches that in a diff. Nobody reviews a dropdown.

You do not have to choose

Ownership is per field, not per flag. Put the things worth reviewing in the file and leave the switch out of it. The repository enforces the rollout and the targeting, while an operator still kills the feature from the dashboard without waiting for anyone.

flags:
  new-checkout:
    environments:
      production:
        rollout_percentage: 25   # the repo owns this, reviewed in a PR
        rules: [...]             # and this
        # `enabled` is not mentioned, so it stays
        # an operator's to flip, no PR required

Put your flags under review.

Export what you already have, commit it, and the next rollout change arrives as a diff somebody signed off on.

The file format, the CLI, the ownership model and drift are documented in full under CLI / TUI in the docs.