Feature flags from config
You don't always need a flag SaaS. Keep boolean flags in config, gate steps on them, and render a flags file your app reads at boot — flip a feature with one --var, no redeploy of the logic.
blog/feature-flags-configNot every feature flag needs a vendor, a dashboard, and a network call on the hot path. Plenty of flags are really just deploy-time config: "is the new checkout on in staging yet?" You want to flip them per environment, gate some setup work on them, and hand the app a file it can read — without wiring up a SaaS for three booleans.
That's a config problem, and it fits neatly in a workflow.
Flags are just vars
Define the flags in defaults: so they have a known baseline, then override per run or per environment. Gating a step on a flag is a plain if:, and rendering the flags file is the render function.
name: feature-flags
defaults:
new_checkout: "false"
dark_mode: "true"
beta_search: "false"
stage: staging
tasks:
# orchstep run flags --var new_checkout=true
flags:
desc: "Render the flag set and gate work on it"
steps:
# render produces text from a template. The output is the JSON the app loads.
- name: render_flags
func: render
args:
template: |
{
"new_checkout": {{ vars.new_checkout }},
"dark_mode": {{ vars.dark_mode }},
"beta_search": {{ vars.beta_search }}
}
outputs:
json: "{{ result.output }}"
- name: checkout_gate
if: '{{ eq vars.new_checkout "true" }}'
then:
- name: on
func: shell
do: echo "new checkout flow ENABLED for {{ vars.stage }}"
else:
- name: off
func: shell
do: echo "new checkout flow disabled for {{ vars.stage }}"
- name: search_gate
if: '{{ eq vars.beta_search "true" }}'
then:
- name: index
func: shell
do: echo "warming the beta search index for {{ vars.stage }}"
- name: write
func: shell
do: echo "wrote flags.json for {{ vars.stage }}"Two things are happening. The render step turns the flag set into the JSON your app loads at boot — give it an args.output_file to also write it to disk. And the if: gates do the setup a flag implies: turning on beta_search also warms its index, so the flag and its side effects can't drift apart.
Flip a flag
orchstep run flags --var new_checkout=trueThe checkout_gate takes its then: branch, the rendered JSON flips to true, and your standing config is untouched. To make a flag environment-specific instead of per-run, lift it into an environments: block and select with --env.
See which gates fire before you commit
The honest worry with flags is "wait, what does this combination actually turn on?" A dry-run answers it without running anything:
orchstep run flags --var new_checkout=true --var beta_search=true --dry-runYou see the resolved booleans and which branches are taken — the index warm-up included — before a single step executes.
What you gained
| Concern | Hard-coded if in app | OrchStep config flags |
|---|---|---|
| Flip a flag | edit code, redeploy | --var new_checkout=true |
| Per-environment value | env-var spaghetti | environments: override |
| Flag's side effects | scattered, easy to forget | gated in one if: block |
| The flags file | hand-maintained | render from one template |
| "What's on right now?" | grep the code | --dry-run shows it |
Where a real flag service still wins
This pattern is for flags you flip at deploy time. If you need per-user targeting, gradual percentage rollouts, or flipping a flag live without a deploy, reach for a real flag platform — that's what they're good at. For the long tail of boolean config that ships with the release, this is less moving parts.
Where to go next
- The render function — templates to text or files
- Variables & Outputs — scoping and
--var - Error Handling — conditionals, retry, catch
The project above is a runnable, echo-only demo — flip the flags with --var and watch the gates change.
curl -fsSL https://orchstep.dev/install.sh | sh