Skip to content

Flagsmith Feature Flags Configuration

This guide explains how to configure and use feature flags in the ERP-Unlocked platform using Flagsmith.

ERP-Unlocked uses Flagsmith for feature flag management. This enables:

  • Gradual rollouts: Roll out features to a percentage of users
  • Targeting: Enable features for specific organizations, users, or segments
  • A/B testing: Test different feature variants
  • Kill switches: Quickly disable features in production
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Flagsmith Server β”‚
β”‚ (Self-hosted or Flagsmith Cloud) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ @repo/feature-flags (TS) β”‚ β”‚ packages/feature-flags-py β”‚
β”‚ β”œβ”€β”€ server.ts (SSR/API) β”‚ β”‚ (Python Flagsmith client) β”‚
β”‚ β”œβ”€β”€ client.ts (Browser) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ └── hooks.tsx (React) β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό
β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Python Workers & Pipelines β”‚
β”‚ Webapp / TS Temporal Worker β”‚ β”‚ (temporal-worker, dagster) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Terminal window
# Flagsmith environment key (server-side key)
FLAGSMITH_ENVIRONMENT_KEY=ser.xxxxx
# Flagsmith API URL (self-hosted or cloud)
FLAGSMITH_API_URL=https://flagsmith.yourdomain.com/api/v1
Terminal window
# Enable local evaluation (caches flags, reduces API calls)
FLAGSMITH_LOCAL_EVALUATION=true
# Cache refresh interval in seconds (default: 60)
FLAGSMITH_REFRESH_INTERVAL=60
---
import { getFeatureFlag } from '@repo/feature-flags';
// Check if feature is enabled for current user
const isNewDashboardEnabled = await getFeatureFlag('new_dashboard', {
identifier: Astro.locals.user?.id,
traits: {
organizationId: Astro.locals.organization?.id,
email: Astro.locals.user?.email,
}
});
---
{isNewDashboardEnabled ? (
<NewDashboard />
) : (
<LegacyDashboard />
)}
import { getFeatureFlag } from '@repo/feature-flags';
export const GET: APIRoute = async ({ locals }) => {
const useNewAlgorithm = await getFeatureFlag('new_matching_algorithm', {
identifier: locals.user?.id,
traits: {
organizationId: locals.organization?.id,
},
});
if (useNewAlgorithm) {
// Use new implementation
} else {
// Use existing implementation
}
};
import { useFeatureFlag } from '@repo/feature-flags/hooks';
export function OrderEditor() {
const showAdvancedOptions = useFeatureFlag('order_editor_advanced');
return (
<div>
<BasicOptions />
{showAdvancedOptions && <AdvancedOptions />}
</div>
);
}
import { useFeatureGate } from '@repo/feature-flags/hooks';
export function FeaturePreview() {
const { Gate } = useFeatureGate('beta_features');
return (
<Gate fallback={<UpgradeBanner />}>
<BetaFeaturePanel />
</Gate>
);
}
Flag Name Description Default
email_enabled Enable email processing for orders false
dagster_enabled Enable Dagster-based data sync false
typesense_search Use Typesense for product search false
new_order_editor New React-based order editor false
matching_shipto_resolver_v1 Outer gate for the CK ship-to customer resolver (Temporal worker). The /api/erp/customers ship-to boost checks it too, and gates the customer-search API’s client-echoed resolvedCustomerId shortcut (off = parameter ignored); review-v2 auto-validate auto-applies the resolver-marked customer (customer_selected match_type='auto_shipto'). See rollout doc below. false
email_triage_trusted_sender_auth_required_v1 Temporal pull path only (Gmail/Graph β€” not webapp/Cloudflare email intake). Requires a dmarc=pass + provider marker in Authentication-Results before a trusted-sender domain match skips email triage (CWE-290 hardening; shadow-logs when off). No-op unless email_triage_trusted_senders_v1 is also on. See PR #2849 trusted-sender auth gap. false
email_triage_internal_sender_v1 Temporal pull path only. Skips mail sent by the organization’s own staff before content triage, so an org forwarding its own sales orders into its connected mailbox stops producing order rows. Matches the bare From address exactly (never by domain) against email_aliases rows with alias_type='user'. Evaluated per-connection, before the trusted-sender allowlist. Do not enable for any connection yet β€” the staff roster counts Ordermatic staff who join a customer org and never drops leavers; both blockers are tracked as P0 in TODOS.md. false
email_triage_coupa_po_notices_v1 Temporal pull path only. Turns Coupa new-PO notices into orders. A *.coupahost.com sender with a β€œNew Purchase Order … has been issued” subject routes to process as new_order (engine_status=coupa_po_override), and extraction sets the PO to VMS’s E4 form, COUP/ plus the last 7 digits, keeping the Coupa PO in the order notes. Invoice and payment notices from the same sender still skip. A sender that is not DMARC-aligned is logged, not blocked. Evaluated per-connection. false
matching_coupa_customer_v1 Temporal pull path only. Binds a Coupa new-PO notice to the E4 account its PO prefix maps to in matchingConfig.customerAutobind.coupaAccounts (VMS: US to 5436, CA to 5492) instead of the name match, and binds an on-file ship-to only on a street or postal-code match. Falls through to the existing chain when the prefix is unmapped or the account is missing. Evaluated per-connection. false
extraction_coupa_due_rules_v1 Due date and USDOT rules for Coupa new-PO notices, read from extraConfig.eclipse.coupaDueRules. The temporal-worker reads each finished-goods line’s stock (SUM(MaterialOnHand.Quantity)) and desired level (desiredLevelColumn, VMS ReorderLevel) from E4 at extraction and sets the completion dates: in stock, next business day; short with a desired level, 5 business days; short with none, or stock unreadable, the order holds for review. A generic USDOT line (usdot.linePattern) holds the order for a USDOT number; submit refuses it until the CSR fills usdot.numberField in review, then binds the USDOT <number> item at the PO price. Evaluated per-connection. false
email_triage_shopify_orders_v1 Temporal pull path only. Turns Shopify new-order notifications from stores listed in the ERP connection’s extra_config.eclipse.shopify.stores into orders (engine_status=shopify_order_override), but only when DKIM or SPF passed for a Shopify domain. An unverified one keeps the model’s decision, tagged shopify_sender_unverified. Replies and forwards never match. Evaluated per-connection. false
extraction_shopify_orders_v1 Temporal pull path only. Reads a verified Shopify notification from a configured store with a deterministic parser instead of the model: SKU lines (with the store’s skuMap for bundles), PO #<order>/<PO or unit>, shipping address, notes. Reason codes land on the order as sticky SHOPIFY_INTAKE review issues; no lines parsed means the model reads it. Evaluated per-connection. false
email_triage_fleet_sold_order_v1 Temporal pull path only. Turns VMS’s Enterprise Fleet SOLD ORDER handoffs into orders. An email from rkraudel@vmsinc.com whose subject starts β€œSOLD ORDER: Enterprise Fleet” routes to process as new_order (engine_status=fleet_sold_order_override), ahead of revision intake. RE:/FW: replies keep today’s triage. Evaluated per-org. false
extraction_fleet_sold_order_v1 Temporal pull path only. Reads those SOLD ORDER emails with a template parser (activities/fleet_sold_order.py) instead of the LLM: PO is the vehicle’s PO / Unit / VIN line for one vehicle, else See Notes; a stock rerun (Rerun: SOH) gets one finished-good line for the EFM program code; print reruns (Rerun: Yes), new programs and shell orders are held for review with no lines. When the ERP connection sets eclipse.fleetInstallRate, it also reads the program’s install rate from E4 (estimate, else newest job) into the notes; no rate holds the order for review. Parsed fields and E4 notes go to extracted_orders.intake_fields. Needs migration 0204. Evaluated per-org. false
eclipse_e4_order_notes_v1 Per-connection. Eclipse E4 SubmitOrder writes ErpOrderInput.notes: headerDetail to header JobDetailDescription, componentDetails[i] to line i’s DetailDescription, shipmentInstructions to Shipment Instructions. Today only Fleet SOLD ORDERs carry notes. Off is byte-identical. false
eclipse_e4_fleet_install_component_v1 Per-connection, unverified on E4. With eclipse_e4_order_notes_v1 on, appends an External Install component (Description Install, qty = vehicles, UD1 VMS 1) to Fleet jobs, priced at the install rate the worker read from E4 at extraction times the vehicles (unpriced with a submit warning when there is none), and leaves the header total to E4. Needs a TST- probe first. false
eclipse_e4_historical_due_days_v1 Gates the historical-buffer fallback for the Eclipse E4 (VMS) ERP due date (resolveEclipseE4DueDateWithHistory, called from submit.ts and generate-quote.ts). When ON and there’s no CSR-set completion date, dueDate is computed from a per-ship-to business-day buffer mined nightly into erp_ship_to_due_days by eclipse_e4_due_days_job, instead of reusing the PO’s requested date unchanged. Requires eclipse_e4_due_dates_v1 ALSO ON for the connection β€” that flag gates whether the computed due date reaches the ERP at all; this flag alone changes nothing. OFF (default) = byte-identical to prior behavior. false
inbox_conversation_view_v1 Org-scoped, webapp only. Turns on the inbox conversation view. One Conversation tab in the cockpit detail pane replaces the Email and Thread tabs and lists every message on the item’s email thread with its triage outcome, archive and trash activity, and internal notes with @mentions that send an in-app notification. GET /api/work-items/:id/conversation and POST /api/work-items/:id/notes return 404 while it is off. Replying from the inbox is not built, and only inbound mail shows. false
inbox_sla_engine_v1 Org-scoped. Turns on the order-turnaround SLA engine in the inbox: SLA deadlines (slaDueAt, slaWarnAt) are computed for new work items, the inbox page and /api/work-items/list sort open items before terminal ones and then by slaDueAt, and each row shows the SLA urgency pill, whose card (click, tap, Enter or Space) holds the deadline, schedule window and manual priority override. Off keeps chronological order, hides the pill even though every row carries slaTier, and makes /api/work-items/sla-policy, sla-policy-history and [id]/priority return 404. false
agentic_shadow_submit_v1 Org-scoped. Records every intake agent submit (/api/orders/agent-submit, live or shadow) in agent_shadow_submissions: the payload the CSR submit path built, or the gate that held it. CSR submits, splits and Finished approvals in the org are recorded there too. It no longer forces the agent into dry run: live submit is agentic_auto_submit_enabled plus erp_write_enabled. Needs agentic_validation_enabled. Report: packages/eval/scripts/agent-shadow-compare.ts. false
inbox_validation_state_v1 Org-scoped, webapp read path only (uxStateSql / deriveUxState in apps/webapp/src/lib/inbox). Derives an awaiting_review item’s inbox category from the agentic validation verdict as well as its lines: unresolved lines, no ERP customer or a current verdict with blocking issues mean Needs fix; a verdict computed against older order content (auto_validation_version differs from order_content_version), a requires_review verdict or confidence below 0.95 mean Needs review. work_items.status is untouched. false

Flagsmith segments allow targeting users based on traits:

Segment Targeting Rule
internal_team email CONTAINS @boonetek.com
beta_users betaEnabled = true
enterprise_orgs subscriptionTier = enterprise
staging_environment environment = staging

Python services (apps/temporal-worker, apps/dagster) evaluate Flagsmith directly via packages/feature-flags-py (feature_flags_py), scoped to the connection (connection_<connectionId>) or Clerk organization identity (org_...):

from feature_flags_py import get_feature_flag
# In apps/temporal-worker (e.g. activities/typesense.py or workflows/email_processing.py):
is_enabled = get_feature_flag(
"matching_shipto_resolver_v1",
identity=clerk_organization_id,
traits={"clerk_organization_id": clerk_organization_id},
default=False,
)
if is_enabled:
# Run flag-gated resolver path
...
  1. Create the flag in Flagsmith UI

    • Go to your Flagsmith project
    • Create a new feature flag with a descriptive name (snake_case)
    • Set default state (enabled/disabled)
    • Add any targeting rules or segments
  2. Use the flag in code

    const isEnabled = await getFeatureFlag('your_new_flag', identity);
  3. Update this documentation

    • Add the flag to the β€œCurrent Feature Flags” table above
  1. Use descriptive names: order_processing_v2 not flag1
  2. Default to disabled: New features should be off by default
  3. Remove stale flags: After 100% rollout, remove flag checks from code
  4. Document flags: Keep this guide updated with current flags
  5. Use segments: Target internal team first, then beta users, then gradual rollout
  • Check FLAGSMITH_REFRESH_INTERVAL setting
  • Verify FLAGSMITH_API_URL is correct
  • Check Flagsmith server health
  • Verify FLAGSMITH_ENVIRONMENT_KEY is correct
  • Check flag exists in Flagsmith for the correct environment
  • Verify identity traits match targeting rules
  • Enable local evaluation: FLAGSMITH_LOCAL_EVALUATION=true
  • Increase refresh interval for stable flags
  • Use caching for high-traffic routes