What maps to what
Most of Flagsmith's model has a direct wagle counterpart; the honest exceptions are identity-shaped data (wagle stores none) and nested segment rules (wagle's are flat on purpose). Read this table once and the rest of the runbook is mechanical.
| Flagsmith | wagle | Notes |
|---|---|---|
| Project | Project | Identified by an opaque UUID, never a name - discover it via ListProjects and pass it as project on every call. |
| Environment | environment | A free-string namespace created on first write. A flag's identity is project-wide; its serving state lives per env in environments[env]. |
| Feature (flag + remote-config value) | Flag with a value_type | A feature flag is the BOOL case of a typed config entry (BOOL / NUMBER / STRING / JSON). Variation values are always strings on the wire; booleans are "true"/"false". |
| Enabled state | environments[env].enabled | Disabled serves offVariationKey; enabled with no matching rule serves defaultVariationKey. |
| Multivariate options + % split | Rollout rule | Weights are basis points of a 10000-bucket space and must sum to exactly 10000 (a Flagsmith 30% becomes 3000). |
| Segment | Segment | Flat, ANDed conditions; any operator except IN_SEGMENT. One level deep, permanently - no nested segments. Scoped per (project, environment). |
| Segment override | Flag rule with IN_SEGMENT | A rule {"op": "OPERATOR_IN_SEGMENT", "values": ["<segment-key>"]} serving a variation. First matching rule wins. |
| Identity override | An explicit rule | No per-identity store exists. Write it as a visible rule, e.g. EQ on an attribute your app sends in the eval context. |
| Identities & traits | Context attributes | wagle stores nothing about your users: attributes ride each evaluate call and are gone after it. Nothing identity-shaped migrates, by design. |
- Keys are byte-sensitive: no case normalization, Checkout and checkout are two flags. Copy Flagsmith feature names verbatim.
- new is a reserved flag key (the console's create route). A Flagsmith feature named new must be renamed on the way in.
Get your config out of Flagsmith
Two routes, per Flagsmith's bulk import/export docs: the UI export (flags and values, but no segments) or the environment document their local-evaluation SDKs consume, which carries feature states and segment rules in one JSON file. Prefer the environment document if you use segments.
# Route A - the UI bulk export: Project Settings -> Export tab, pick the
# source environment, download JSON. Covers flags, values and multivariate
# weights. Segments are NOT included, and exports expire after two weeks
# (both per Flagsmith's own docs), so keep a local copy.
# Route B - the environment document: the JSON Flagsmith's local-evaluation
# SDKs consume. One environment's feature states AND segment rules, no
# identities/traits. Their CLI outputs it (authenticate with a server-side
# "ser." environment key; see the flagsmith-cli README):
npx flagsmith-cli environment document > flagsmith-production.json- Both routes are per-environment. Export each Flagsmith environment you keep; each becomes one wagle BatchWrite later.
- Neither route carries identities or traits - see the FAQ for why that data has no wagle destination anyway.
Reshape features into write ops
The target shape is a list of WriteOps: each op one flag or one segment, exactly as the admin API takes them. The mechanical 80% (on/off features, single-value config) is a loop; the script below does it and prints the 20% that needs your judgment instead of guessing at it.
// migrate.mjs - Flagsmith environment document -> wagle BatchWrite ops.
// Converts the mechanical part (on/off features, single-value config) and
// PRINTS what needs your judgment (multivariate splits, segment rules)
// instead of guessing. It is your script, not a wagle feature - adapt it.
import { readFileSync, writeFileSync } from "node:fs";
const ENV = "production"; // the wagle environment this batch lands in
const doc = JSON.parse(readFileSync(process.argv[2], "utf8"));
const ops = [];
const review = [];
for (const state of doc.feature_states ?? []) {
const key = state.feature.name;
const value = state.feature_state_value;
if ((state.multivariate_feature_state_values ?? []).length > 0) {
review.push(`${key}: multivariate split -> write a rollout rule by hand`);
continue;
}
const serving = { enabled: state.enabled, defaultVariationKey: "on", offVariationKey: "off" };
const flag =
value == null
? {
// pure on/off feature -> a BOOL flag
key,
valueType: "VALUE_TYPE_BOOL",
variations: [
{ key: "on", value: "true" },
{ key: "off", value: "false" },
],
environments: { [ENV]: serving },
}
: {
// remote-config value -> a typed flag; wagle values are ALWAYS strings
key,
valueType: typeof value === "number" ? "VALUE_TYPE_NUMBER" : "VALUE_TYPE_STRING",
variations: [
{ key: "on", value: String(value) },
{ key: "off", value: "" },
],
environments: { [ENV]: serving },
};
ops.push({ flag });
}
for (const segment of doc.project?.segments ?? []) {
review.push(`segment ${segment.name}: translate rules by hand (flat ANDed conditions)`);
}
writeFileSync("ops.json", JSON.stringify(ops, null, 2));
console.log(`${ops.length} ops -> ops.json`);
if (review.length) console.log(`needs review:\n- ${review.join("\n- ")}`);- Multivariate splits: Flagsmith allocates percentages out of 100, wagle rollout weights are basis points summing to exactly 10000. Multiply by 100 and make the weights add up, or the write is rejected.
- Flagsmith regex conditions must fit wagle's portable subset (the intersection of RE2 and JavaScript regex) - the price of byte-identical evaluation across SDKs. Rewrite what doesn't fit.
- One op per key per batch: dedupe before writing, later ops replace earlier ones.
One atomic BatchWrite per environment
BatchWrite applies every op all-or-nothing under a single config version bump: a bad op rejects the whole batch and nothing lands, and no SDK ever syncs a half-imported config. That atomicity is the reason this runbook is safe to run against a live project.
# One BatchWrite applies every op all-or-nothing under a SINGLE config
# version bump: an invalid op rejects the whole batch and nothing is written,
# and no SDK ever observes a half-imported config. Run it once per target
# environment. Example ops: a segment, then a flag that references it.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/BatchWrite \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{
"project": "baseline",
"environment": "production",
"ops": [
{"segment": {"key": "beta-testers", "conditions": [
{"attribute": "plan", "op": "OPERATOR_IN", "values": ["pro", "enterprise"]}
]}},
{"flag": {
"key": "new-checkout",
"valueType": "VALUE_TYPE_BOOL",
"variations": [{"key": "on", "value": "true"}, {"key": "off", "value": "false"}],
"environments": {"production": {
"enabled": true,
"defaultVariationKey": "off",
"offVariationKey": "off",
"rules": [
{"conditions": [{"op": "OPERATOR_IN_SEGMENT", "values": ["beta-testers"]}],
"variationKey": "on"},
{"conditions": [],
"rollout": {"entries": [
{"variationKey": "on", "weight": 2500},
{"variationKey": "off", "weight": 7500}
]}}
]
}}
}}
]
}'
# -> {"models":{...},"graphs":[...],"version":"1"}- The example project is the placeholder baseline; your real project is an opaque UUID - discover it via ListProjects.
- Ops apply in order: write a segment before the flag that references it. (A dangling IN_SEGMENT reference is legal and evaluates false until the segment exists - but why make yourself debug that.)
- The returned version is a uint64 serialized as a JSON string - that's protojson, not a bug.
Prove the migration before you cut over
Evaluate the imported flags with the same identities and traits Flagsmith served and compare answers. Run both products in parallel for a while if you like: wagle SDKs evaluate locally against a synced snapshot, so the double-read costs your app nothing on the hot path.
# Spot-check evaluations against what Flagsmith served for the same
# identity + traits. Reasons tell you WHY a variation was served.
curl -sS $WAGLE_URL/api.eval.v1.EvaluationService/Evaluate \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production","flagKey":"new-checkout",
"context":{"entityId":"user-42","attributes":{"plan":"pro"}}}'
# -> {"variationKey":"on","value":"true","reason":"REASON_TARGET_MATCH",...}
# The import itself is auditable: it landed as ONE config version, attributed
# to you, diffable per flag.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/GetHistory \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production"}'- Percentage rollouts reshuffle membership once (different bucketing hash) - a 25% stays 25%, but not the same 25%. Cut over load-bearing rollouts at 0% or 100%.
Export from wagle back to Flagsmith
Leaving must be as easy as arriving, or the arriving was a trap. The entire config of a (project, environment) is two list calls returning plain JSON - the same public API you use every day, no export gate, no tier. Reshape it for Flagsmith's import and you're out.
# The whole config of one (project, environment) is two list calls: flags
# (identity + per-env serving state + rules) and segments. That JSON is
# yours; there is nothing else to ask for and no export gate to unlock.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/ListFlags \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production"}'
# -> {"models":{"flags":[...]},"graphs":[...]}
curl -sS $WAGLE_URL/api.admin.v1.AdminService/ListSegments \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production"}'
# -> {"models":{"segments":[...]},"graphs":[...]}
# Going back to Flagsmith is this page in reverse: reshape into their bulk
# import format (Project Settings -> Import tab) or replay their Admin API.Or hand the migration to an agent
The admin surface is exposed to AI agents over MCP at /mcp (flag and segment CRUD included), so a capable agent can read your Flagsmith export and recreate it: connect a host with an agent credential (minted in Settings → Agents), then use the prompt below - it encodes this page's transform rules, so the agent applies the same mapping the script would, and tells you what it couldn't.
{
"mcpServers": {
"wagle-admin": {
"type": "http",
"url": "$WAGLE_URL/mcp",
"headers": { "Authorization": "Bearer <refresh-credential>" }
}
}
}# Register the endpoint once (Claude Code shown; any MCP host works):
claude mcp add --transport http wagle-admin \
$WAGLE_URL/mcp \
--header "Authorization: Bearer <refresh-credential>"
# Then hand it your export and this page's rules in one ask:
> Here is a Flagsmith environment document (attached:
flagsmith-production.json). Recreate it in my wagle project "baseline",
environment "production":
- segments first: flat ANDed conditions; skip anything with nested
any/all rule trees and tell me what you skipped and why;
- each feature becomes a flag: pure on/off -> a BOOL flag; a valued
feature -> a typed flag whose "on" variation carries the value
(values are strings);
- multivariate splits -> a rollout rule, weights in basis points
summing to exactly 10000;
- keep every key byte-identical, except rename a feature named "new"
(reserved key) and flag it to me.
When done, list everything you created, then evaluate new-checkout
for user-42 with plan=pro so I can compare against Flagsmith.- MCP tools write one entity per call, so an agent migration is N version bumps, not one atomic batch like BatchWrite. Fine for a dozen flags; script the batch for hundreds.
- The bearer is the agent's long-lived, revocable refresh credential - revoke the agent when the migration is done if you minted one just for this.
Migration questions, answered honestly
Does wagle have a built-in Flagsmith importer?
No. There is no one-click importer and this page does not pretend otherwise. What wagle has instead is a small admin surface that is plain JSON over HTTP, and one BatchWrite call that applies your whole exported config atomically under a single version bump - so the import is a short script, not a product feature.
Will my rollout percentages keep the same users after migrating?
No. Both products bucket deterministically, but with different hashes: wagle hashes "{project}:{environment}:{flagKey}:{entityId}" with xxHash64 modulo 10000. A 25% rollout stays a stable 25%, but which users fall inside it reshuffles once at migration. If a partial rollout is load-bearing, migrate that flag at 0% or 100%, or accept one reshuffle.
Do Flagsmith segments migrate automatically?
No - translate them by hand. A wagle segment is a flat list of ANDed conditions (any operator except IN_SEGMENT), one level deep permanently, scoped per (project, environment). Flagsmith's nested any/all rule trees and richer operators don't map mechanically; regex rules must also fit wagle's portable RE2-and-JS subset.
What happens to identities and traits?
They don't migrate, by design. wagle stores nothing about your users: the evaluation context (entity id + attributes) rides each call from your app and is never persisted. Flagsmith identity overrides become explicit targeting rules on attributes you send; server-stored traits become attributes your app supplies itself.
Can I get my flags back out of wagle later?
Yes. ListFlags and ListSegments return the entire (project, environment) config as JSON - identity, serving state, rules, everything. Leaving is the same runbook in reverse, and the export path is the same public API you use every day, not a gated feature.