Brownfield Migration: Existing Scoped App → Fluent + Git
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).