| name | add-feature-flag |
| description | Add a new boolean feature flag (dynamic config key) to cadence-web. Use when asked to add, create, or set up a feature flag, config flag, or `*_ENABLED` toggle. |
Add a feature flag
Feature flags are dynamic config entries whose resolver returns boolean. Completeness is enforced by TypeScript — the fixture and the Zod schema map are typed against dynamicConfigs, so a missing entry fails npm run typecheck.
Reference example: WORKFLOW_DIAGNOSTICS_ENABLED (added in the same 4 files below).
Naming
- Key:
UPPER_SNAKE_CASE, ending in _ENABLED — e.g. WORKFLOW_DIAGNOSTICS_ENABLED
- Env var:
CADENCE_ + key — e.g. CADENCE_WORKFLOW_DIAGNOSTICS_ENABLED
- Resolver file: kebab-case of the key — e.g.
workflow-diagnostics-enabled.ts
- Resolver function: camelCase of the key — e.g.
workflowDiagnosticsEnabled
Files to touch
Add the new entry directly after a related existing flag in each file (not alphabetically).
1. Resolver — src/config/dynamic/resolvers/<flag-name>.ts
export default async function <flagName>(): Promise<boolean> {
return process.env.CADENCE_<FLAG_NAME> === 'true';
}
- Default export,
async, returns Promise<boolean>.
- Off by default; forks override the resolver.
- If the flag depends on another flag, import and call that resolver rather than re-reading its env var (see
workflow-diagnostics-in-history-enabled.ts). Check the cheap env var first.
- If the flag needs args (e.g.
{ cluster, domain }), add <flag-name>.types.ts with a <FlagName>ResolverParams type (see cron-list-enabled.types.ts).
2. Register — src/config/dynamic/dynamic.config.ts
Three edits:
import <flagName> from './resolvers/<flag-name>';
<FLAG_NAME>: ConfigAsyncResolverDefinition<undefined, boolean, 'request', true>;
<FLAG_NAME>: {
resolver: <flagName>,
evaluateOn: 'request',
isPublic: true,
},
Replace undefined with the args type if the resolver takes args. isPublic: true is what exposes the key to the client via /api/config.
3. Zod schema — src/config/dynamic/resolvers/schemas/resolver-schemas.ts
<FLAG_NAME>: {
args: z.undefined(),
returnType: z.boolean(),
},
4. Test fixture — src/utils/config/__fixtures__/resolved-config-values.ts
<FLAG_NAME>: false,
The getConfigValue auto-mock reads from this, so server-side tests see the new key automatically.
5. Resolver test (only if the resolver has logic)
Skip for a plain env-var check. If the resolver combines conditions, calls another resolver, or takes args, add src/config/dynamic/resolvers/__tests__/<flag-name>.node.ts. Mirror batch-actions-ui-enabled.node.ts: jest.mock the imported resolvers, save/restore the env var in beforeEach/afterEach.
Consuming the flag
Client:
const { data: isEnabled } = useSuspenseConfigValue('<FLAG_NAME>');
Server (route handlers): await getConfigValue('<FLAG_NAME>').
Client tests mock the endpoint:
render(<Component />, {
endpointsMocks: [
{
path: '/api/config',
httpMethod: 'GET',
mockOnce: false,
httpResolver: async () => HttpResponse.json(true),
},
],
});
When the feature ships
Once the UI lands:
- Add a row to the Feature flags table in
README.md
- Add a commented
# CADENCE_<FLAG_NAME>=true under ### Feature flags in .env
Verify
npm run typecheck
npm run test:unit:node <flag-name>.node.ts
npm run test:unit:node -- src/route-handlers/get-config src/utils/config src/config/dynamic
npm run lint
Optional live check: npm run dev, then curl 'http://localhost:8088/api/config?configKey=<FLAG_NAME>' → false; rerun with CADENCE_<FLAG_NAME>=true npm run dev → true.