There's now a safer way for your agent to yeet stuff into production on Railway: Feature flags.
If you haven't used them before, a flag is a value your app checks at runtime to decide which code path runs, which means turning a feature on or off no longer requires a deploy. Code lands in production dark, behind a flag that defaults to off, so the merge changes nothing for anyone.
From there, the release happens on your terms: turn the feature on for your team, then 10% of users, then everyone, and if anything looks wrong along the way, turn it back off in seconds instead of scrambling to redeploy.
Feature flags are now wired into the dashboard, the CLI, the SDK, and MCP, so your agents can drive them the same way you do: create a flag, gate a change behind it, and run the rollout with confidence.
What it looks like #
With the Railway CLI installed, run railway setup agent --remote
to set your agent up with two things: the Railway MCP server and Railway's agent skills, which cover the full feature flag workflow and stay updated as the platform evolves. Once that's in place, the shortest path to your first flag is a prompt:
Gate the new checkout flow behind a feature flag, off by default, and roll it out to 25% of Pro users.
The agent knows exactly what that implies. It wraps the new code path in an SDK read, runs railway flag
commands to create the flag with a safe default and attach the targeting rule, and opens a pull request. That PR is safe to merge on sight, because until the rollout rule says otherwise, the flag serves false
to everyone.
And if you're more of a UI enjoyer, everything the agent just did is sitting in your project, ready to inspect and tweak.
Creating a flag and adding a targeting rule in the dashboard (Settings → Feature Flags)Flags live in your project under Settings → Feature Flags. Creating one takes a name, a type (bool
, string
, number
, or json
), and a default value, and targeting rules read the way you'd say them out loud: when plan
equals enterprise
, serve true
, or serve true
for 25% of users. The dashboard, the CLI, the SDK, and your agents all read and write the same flags, so what you see in settings is exactly what your app resolves at runtime.
One rollout, start to finish #
The clearest way to show how this changes your shipping rhythm is to run one release end to end. Say an agent has rewritten your checkout flow, the change is reviewed, and you want it live without betting your whole user base on it.
Every flag has a default value, plus optional rules that override it for specific users. Start by creating the flag with a safe default. There's no separate create command, railway flag set
creates the flag if it doesn't exist and infers its type from the value:
railway flag set checkout-v2 false
In your app, put the new code path behind the flag. Install the railway
TypeScript SDK and call flags.init()
once at startup. After that, a read is one call:
import { flags } from "railway";
await flags.init();
if (flags.getBoolean("checkout-v2")) {
// new checkout
}
A plain on/off switch needs nothing more, and most flags are exactly that. When you want targeting, like "my team only" or "25% of users", pass context about who's asking, plus an optional fallback for when the flag can't be read:
const useNewCheckout = flags.getBoolean(
"checkout-v2",
{ key: user.id, email: user.email, plan: user.plan },
false,
);
The context is just the attributes you choose to pass. A rule like plan == "enterprise"
works because you passed plan
, not because Railway knows what a plan is. The only special attribute is key
, which identifies the user for percentage rollouts.
Flags aren't limited to booleans either. getString
, getNumber
, and getJson
work the same way, so you can change things like rate limits or model config at runtime without redeploying.
Now merge. The default is false
, so nothing changes for your users yet. Then the release starts, beginning with your own team:
railway flag set checkout-v2 true --rule-id team --when 'email contains "@yourco.com"'
Once checkout looks healthy for the people who built it, widen to a slice of real users with a percentage rollout. That's what bucket()
does in a rule: it spreads your users evenly across a range from 0 to 1, based on the key
your app passes in the context (the user's ID, in the snippet above). So bucket(key) < 0.25
means "the quarter of my users who land in the first 25% of the range":
railway flag set checkout-v2 true --rule-id rollout-25 --when 'bucket(key) < 0.25'
A user's spot in that range never changes. Raise the threshold from 10% to 25% and everyone who already had the new checkout keeps it, new users join, and nobody flips back and forth while you adjust.
Conditions combine with &&
and ||
too, so "Pro customers, but only a quarter of them" is a single rule: --when 'plan == "pro" && bucket(key) < 0.25'
(the docs cover the full expression syntax).
The expressions are also exactly the kind of thing your agent is good at. If you'd rather stay in business terms, ask for the outcome instead:
Turn checkout-v2 on for 25% of Pro users, and keep it on for the whole team.
An agent set up with the Railway MCP server and agent skills (the railway setup agent --remote
combo from earlier) translates that into the rules you just saw, and railway flag list
shows you what it wrote before any user sees it.
If a rule turns out to be wrong, remove it with railway flag unset checkout-v2 --rule-id rollout-25
and everyone it covered goes back to the default, no deploy involved. And once the flag has sat at 100% for a while, delete it and make the new code path permanent:
railway flag delete checkout-v2
Throughout all of this, reads never make a network call. init()
fetches your project's rulesets once, and from then on every read evaluates against an in-memory copy that the SDK keeps fresh in the background. A read costs about as much as a function call, so you can put one in a hot path without budgeting for it. Flags end up behaving like environment variables that update without a redeploy.
And when you're staring at a value wondering how it got there, swap the getter for its evaluate
variant. Here's what it returns for a user who isn't on your team but landed inside the 25% rollout:
const evaluation = flags.evaluateBoolean(
"checkout-v2",
{ key: user.id, email: user.email, plan: user.plan },
false,
);
// {
// value: true,
// reason: "SPLIT",
// trace: [
// { ruleId: "team", matched: false, value: null },
// { ruleId: "rollout-25", matched: true, value: true },
// ],
// }
The team rule didn't match, the rollout did, and "SPLIT"
means a percentage rollout made the call. Resolution is a pure function with no hidden state, so "why does this user see this value" has an exact, reproducible answer.
When rules disagree, production wins #
Most flag systems resolve overlapping rules with priorities: rule three beats rule seven, or whichever rule was written last wins. In our experience, that's how you end up serving a value nobody chose, and debugging it means reconstructing who wrote what in which order.
Railway instead evaluates every matching rule at read time. When the matching rules agree, you get that value, and when they disagree, or nothing matches, you get the flag's default. There are no priorities to reason about, which means there is nothing to mis-order.
That makes the default the most important value on the flag. It's what production looks like, and every rule is a temporary exception on top of it. Whatever goes wrong, the result is the same:
Two rules conflict for the same user? They see the default.
A rollout rule needs a
key
and the context doesn't have one? The default, never a random assignment. - Your app can't reach Railway at all? The fallback you wrote in code.
The fallback in that last case is different from the default: it only applies when the flag can't be read at all. When the flag exists and no rule matches, you get the default you configured. Either way, the worst case of any conflict, outage, or typo is that a user sees plain old production.
Where this goes #
Feature flags in this first version resolve to literal values, but the primitive underneath (a context resolving to a value at read time) extends well past config. There's a lot a rule could decide beyond a boolean, and we're exploring where to take it next.
Feature flags are available on every project today. Create one under Settings → Feature Flags, read the feature flags docs, and tell us what you build with them (or what's missing) on Central Station.