Brownfield Migration: Existing Scoped App → Fluent + Git
SkillDev toolsThis skill should be used when the user asks to "convert this app to fluent", "migrate a scoped app to source control", "modernize this servicenow app", "bring an existing app into git", "transform to fluent", or mentions "brownfield" migration of an existing ServiceNow scoped application to the ServiceNow SDK / Fluent.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Brownfield Migration: Existing Scoped App → Fluent + Git skill
What this skill tells your AI
The instructions your AI receives, as published by serac-labs/serac in packages/skills/fluent-brownfield-migration/SKILL.md and read by ahel’s review.
Take a scoped app that lives only on an instance and move it into a git-managed ServiceNow SDK (Fluent) project — incrementally, with zero functional change at the start. Based on @servicenow/sdk 4.x (now-sdk CLI). Run snow_fluent_status first to see where a project already stands (scope, SDK pin, keys.ts, XML vs Fluent counts); use snow_fluent_explain for offline SDK docs on any topic.
Where these tools run: the
snow_fluent_*tools are local-only — they execute the ServiceNow SDK (now-sdk) on the developer's machine, so they only exist when the MCP server runs locally over stdio. Over a hosted HTTP transport (e.g. a web chat) they are not available, so do not call them; drive the same flow through git + CI instead: commit the project files, trigger the build/deploy pipeline, and follow the workflow runs.
1. Step zero: pull the app down as-is
Use snow_fluent_init with from=<sys_app sys_id> (CLI: now-sdk init --from <sys_id>). Since SDK 2.2.6 this does not generate Fluent code by default — it leaves every record as XML in the metadata/ folder. Those XML files are the exact files the instance exports, so there is no change to the application at this point. now.config.json captures scope + scopeId (= the sys_app sys_id), and the XML builds and installs as-is.
This means you get value immediately: the app is in git, diffable, and deployable via snow_fluent_build + snow_fluent_install — before converting a single record to TypeScript. Commit this baseline first.
✅ init --from sys_id → commit metadata/ + now.config.json + keys.ts → build → install to dev → verify
❌ init --from sys_id → immediately transform everything → fight 200 build errors with no known-good baseline
2. Convert incrementally — never big-bang
Convert table-by-table with snow_fluent_transform using the tables arg (CLI: now-sdk transform --table <comma-separated tables>, table targeting since 4.7.0). After a successful conversion, transformed files are scaffolded into the generated/ directory and removed from metadata/.
The loop, per step:
snow_fluent_transformfor ONE table (e.g.sys_scriptfor Business Rules).snow_fluent_build— immediately, every time.- Fix what broke, smoke-read the generated code.
- Commit. One table family per commit.
Why never big-bang transform the whole app:
- Flat output. Transform dumps everything into one flat
generated/folder and doesn't separate script/HTML content into individual files — a whole-app transform is an unreviewable blob (GitHub discussion #25). - Unknown instance fields break builds. Transform can emit fields missing from the SDK type definitions (e.g.
survey_overwriteonsys_app_module,sys_reportactive/type), turning into TypeScript errors. One table at a time, these are fixable; all at once, they bury you (#61). - Form-layout re-transforms duplicate. Re-transforming form layouts appends
sys_ui_sectioncontent instead of replacing it, creating duplicates (#41). Small steps make this detectable.
✅ transform tables=sys_script → build → commit → transform tables=sys_script_include → build → commit
❌ transform the entire metadata/ folder in one shot, then try to build
3. The precedence rule and catching twins
When the same record exists as both a Fluent entity (.now.ts) and an XML file in metadata/, the XML version wins on build. A half-deleted XML twin silently masks your new Fluent code.
- After each transform step, confirm the XML left
metadata/(snow_fluent_statusshows XML vs Fluent counts). - Build with
error_on_conflict(CLI:now-sdk build --errorOnConflict) to make Fluent-vs-XML sys_id conflicts fatal instead of silent. Use this for the whole migration; addfrozen_keys(--frozenKeys) in CI to fail when keys.ts is out of date.
4. What converts well vs. what to leave as XML (for now)
Convert early (mature Fluent APIs): Table (incl. columns), BusinessRule, Acl, ClientScript, ScriptInclude, UiAction, Role, Property, ScheduledScript (4.5.0+), RestApi.
Convert late or carefully: Forms — historically broken (sys_ui_section handling) until the dedicated Form API in SDK 4.6.0; verify your SDK pin is ≥ 4.6 before transforming forms, and watch for the duplication bug on re-transform. Flows exist since 4.3.0 but are still maturing.
Leave as XML / generic Record API: classic Workflow (wf_workflow), reports and Performance Analytics, UI macros, UI scripts, processors, schedules, data sources. These have no first-class Fluent API — XML in metadata/ round-trips unchanged and ships in the package just fine. Don't force them.
A partially converted app is a perfectly healthy end state. XML and Fluent coexist indefinitely in the same build.
5. ES5 vs modern TypeScript — know which layer you're in
The repo-wide "ES5 only" rule applies to script content that executes on the instance's Rhino engine — the string/file behind a script: property. The Fluent DSL itself is modern TypeScript and must be written as modern TypeScript:
// Fluent DSL: modern TS is correct here ✅
import '@servicenow/sdk/global'
import { BusinessRule } from '@servicenow/sdk/core'
BusinessRule({
$id: Now.ID['validate-request'],
table: 'x_acme_req_request',
when: 'before',
action: ['insert', 'update'],
// script CONTENT runs on Rhino: keep it ES5 ✅ (var, function, string concat — no const/let/arrow/template literals)
script: "(function executeRule(current, previous) { var u = current.getValue('requested_for'); if (!u) { current.setAbortAction(true); gs.addErrorMessage('Requested for is required'); } })(current, previous);",
})
❌ Rewriting Fluent DSL files to var/function() style "for ES5 compliance" — wrong layer. ✅ ES5 inside script: strings and src/server/ code destined for Rhino; modern TS everywhere else.
6. Staying in sync while others still edit in Studio
Migration takes days or weeks; teammates may keep changing the app on the instance. Before each transform session, pull instance changes into local XML with snow_fluent_download using incremental (CLI: now-sdk download <directory> --incremental), review the git diff, commit, then continue converting. Run snow_fluent_dependencies after pulling if new table references appeared (add_table + scope to bring in tables outside the app; also refreshes type defs).
Declare a cutover date: after it, all changes flow through git + snow_fluent_install, and Studio editing of this app stops. Until then, download-before-transform is the discipline that prevents overwriting colleagues' work.
Install caveats (snow_fluent_install): SDK installs bypass update sets entirely and create no rollback context. reinstall is the only "rollback" and it's destructive (uninstall + fresh install; side-effect records are not cleaned up). Install from automation only to dev/test instances — production goes through the App Repository, not now-sdk install.
7. Finishing the migration
keys.tsis sacred.src/fluent/generated/keys.tsmapsNow.ID['...']to sys_ids and is regenerated by every build. It MUST be committed — an uncommitted keys.ts means duplicate records on the next install. Enforce withsnow_fluent_build+frozen_keysin CI (fails with "Keys file is out-of-date" if a dev forgot).- Repo hygiene:
.gitignorealready excludes.now/,dist/,node_modules/. Commitnow.config.json,metadata/,src/,keys.ts,package.json. - Team switches to Fluent-first: changes happen in
.now.tsfiles, PRs reviewed in git, deploys via install to dev → publish to App Repo for test/prod. - Advanced — global apps: since SDK 4.4.0 (requires an Australia-release instance),
init --fromalso converts global scoped apps, andnow-sdk move --ids <sys_ids>lets a global app claim and customize global metadata. Treat as advanced; scoped apps are the well-trodden path.
Verification checklist (run after every milestone, and at the end)
- Build clean:
snow_fluent_buildwitherror_on_conflictexits without errors and without XML/Fluent twin warnings. Note: a missingpackage.jsonprints an ERROR but still exits 0 — check thatdist/was actually produced. - Install to dev:
snow_fluent_installto a dev instance succeeds; useinfoto confirm the last install status on the instance. - Smoke-test on the instance: exercise the app's main flows (create a record, trigger the key Business Rule, open the main form/list).
- Diff record counts: with
snow_query_table, count records per migrated table (sys_script,sys_script_include,sys_security_acl, etc., filtered bysys_scope) before vs after install — counts must match. Duplicates signal an uncommitted keys.ts or a form-layout re-transform; missing records signal a masked XML twin. - Git state: working tree clean after build (a dirty keys.ts after a "no-op" build means someone skipped a commit).
Signals
- GitHub stars
- 78
- Forks
- 26
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
fluent-brownfield-migration- Source
- github.com/serac-labs/serac