ServiceNowDevelopment

From .now.ts to production: the ServiceNow SDK, Fluent and Git workflow

Part 3 of 4 in the Code-first ServiceNow series. What code-first ServiceNow development looks like day to day: a Fluent project layout, short TypeScript examples that compile with SDK 4.13.0, the build and deploy loop, branching and pull requests, CI quality gates, ATF, and a promotion path from DEV to TEST to PROD that keeps the SDK away from production.

AQ
Ali Qaiser
AWS Certified | ServiceNow Architect | Enterprise AI Consultant
26 September 2026
12 min read
From .now.ts to production: the ServiceNow SDK, Fluent and Git workflow
In brief

Part 3 of 4 in the Code-first ServiceNow series. What code-first ServiceNow development looks like day to day: a Fluent project layout, short TypeScript examples that compile with SDK 4.13.0, the build and deploy loop, branching and pull requests, CI quality gates, ATF, and a promotion path from DEV to TEST to PROD that keeps the SDK away from production.

Key Takeaways
  • A Fluent project is a normal TypeScript project: `.now.ts` files for metadata, modules for server logic, `now.config.json` for the scope and a generated `keys.ts` that pins every record's `sys_id`.
  • The inner loop is short: edit, `npm run build`, `npm run deploy` to a non-production instance, check, commit. A failed build blocks deployment.
  • Every change goes through a branch and a pull request that states the scope, metadata affected, test evidence, target instance and rollback considerations.
  • The planned CI gate runs `npm ci` and `now-sdk build --frozen-keys` on every pull request, with no ServiceNow credentials available to untrusted PR code.
  • ServiceNow's guidance is to use direct SDK installs for development instances only, and to promote to TEST and PROD through the Application Repository, which `now-sdk cicd` can drive, behind a human approval gate.

Code-first ServiceNow, part 3 of 4. Part 1: Architecture · Part 2: Workstation setup · Part 3: SDK, Fluent and Git workflow (this article) · Part 4: Governance and AI agents

Parts 1 and 2 covered the why and the workstation. This part is about the work itself: how a Fluent project is laid out, what the code looks like, how it gets built and installed, and how a change travels from a feature branch to production without anyone clicking a change into a live instance.

All examples use the scope and conventions of my own practice repository (scope x_33764_practice). The Fluent snippets were compiled with @servicenow/sdk 4.13.0 before publication. Where something is on the roadmap rather than running today, I say so.

Project layout

The reference repository today looks like this:

practice-app/
├── .nvmrc                      # 24
├── .vscode/extensions.json     # recommends the Fluent extension
├── now.config.json             # scope, scopeId, name, tsconfig path
├── package.json                # pinned SDK, scripts, Node engine
├── package-lock.json
├── scripts/bootstrap-workspace.sh
├── docs/                       # architecture, setup, workflow, promotion, security, roadmap
└── src/
    ├── fluent/
    │   ├── example.now.ts      # client script + business rule
    │   └── generated/keys.ts   # Now.ID -> sys_id registry (generated, committed)
    ├── server/
    │   ├── script.ts           # server module imported by the business rule
    │   └── tsconfig.json
    └── tsconfig*.json

As an application grows, the workflow document recommends organising Fluent files by concern:

src/fluent/
├── tables.now.ts
├── roles.now.ts
├── security/acls.now.ts
├── automation/business-rules.now.ts
└── tests/atf.now.ts

now.config.json holds the application identity, not connection details:

{
  "scope": "x_33764_practice",
  "scopeId": "05e48ae83b2f478fb01fdcd2dac47191",
  "name": "Fluent Practice",
  "tsconfigPath": "./src/server/tsconfig.json"
}

The scripts

The package.json scripts are thin wrappers around the SDK CLI:

ScriptRunsPurpose
npm run buildnow-sdk buildCompile and validate Fluent and modules into an installable package
npm run deploynow-sdk installInstall the latest build on the instance for the selected alias
npm run typesnow-sdk dependenciesDownload type definitions for dependencies in now.config.json (needs instance auth)
npm run transformnow-sdk transformConvert instance records or XML into Fluent source
npm run verifytypes then buildLocal end-to-end check

Fluent in practice

Fluent is a typed, declarative language. The SDK statically analyses it and compiles it to ServiceNow metadata; it is not executed locally, so dynamic constructs such as loops that generate records will not compile.

A business rule backed by a real module

This is the repository's own example. The business rule is declared in Fluent and its script is an imported TypeScript function, not a string buried in XML:

// src/fluent/example.now.ts (excerpt)
import { BusinessRule } from '@servicenow/sdk/core'
import { showStateUpdate } from '../server/script'

BusinessRule({
    $id: Now.ID['br0'],
    action: ['update'],
    table: 'incident',
    script: showStateUpdate,
    name: 'LogStateChange',
    order: 100,
    when: 'after',
    active: true,
})
// src/server/script.ts
import { gs, type GlideRecord } from '@servicenow/glide'

export function showStateUpdate(current: GlideRecord, previous: GlideRecord) {
    const currentState = current.getValue('state')
    const previousState = previous.getValue('state')

    gs.addInfoMessage(`state updated from "${previousState}" to "${currentState}"`)
}

@servicenow/glide provides typed Glide APIs, so the editor knows what current.getValue returns before anything reaches an instance.

A table, roles and ACLs

A small, realistic slice of a scoped app: one table, a user and an admin role, and record ACLs.

// src/fluent/tables.now.ts
import { Table, StringColumn, DateColumn } from '@servicenow/sdk/core'

// The variable name must match the table name
export const x_33764_practice_note = Table({
    name: 'x_33764_practice_note',
    label: 'Practice Note',
    display: 'title',
    schema: {
        title: StringColumn({ label: 'Title', mandatory: true }),
        due_date: DateColumn({ label: 'Due date' }),
        status: StringColumn({
            label: 'Status',
            choices: { open: 'Open', done: 'Done' },
        }),
    },
})
// src/fluent/roles.now.ts
import { Role } from '@servicenow/sdk/core'

export const practiceUser = Role({
    $id: Now.ID['practice_user'],
    name: 'x_33764_practice.user',
})

export const practiceAdmin = Role({
    $id: Now.ID['practice_admin'],
    name: 'x_33764_practice.admin',
    containsRoles: [practiceUser],
})
// src/fluent/security/acls.now.ts
import { Acl } from '@servicenow/sdk/core'
import { practiceAdmin, practiceUser } from '../roles.now'

Acl({
    $id: Now.ID['note_read'],
    type: 'record',
    table: 'x_33764_practice_note',
    operation: 'read',
    roles: [practiceUser],
    description: 'Practice users can read notes',
})

Acl({
    $id: Now.ID['note_delete'],
    type: 'record',
    table: 'x_33764_practice_note',
    operation: 'delete',
    roles: [practiceAdmin],
    description: 'Only practice admins can delete notes',
})

This mirrors what the reference application on my PDI already contains (roles for users and admins, read, create and write for users, delete for admins), expressed in a way a reviewer can read in a minute.

Record identity: Now.ID and keys.ts

Every $id: Now.ID['...'] is a stable, human-readable key. On build, the SDK records the mapping from each key to a real sys_id in src/fluent/generated/keys.ts. Commit that file with the code that introduces the key. If it is missing or stale, a build on another machine generates different sys_ids, and what should be an update on the next environment becomes a duplicate insert.

Dependencies and types

If your application references tables, roles or script includes from other scopes, declare them under dependencies in now.config.json and download their definitions:

npm run types                                   # now-sdk dependencies
npx now-sdk dependencies --auth pdi1 --fluent-only

This powers IntelliSense and compile-time validation against real platform metadata. It needs an authenticated instance connection. The SDK documentation says the generated definitions (under @types/servicenow by default) are meant to be committed, so every developer, and CI, builds against the same types without each running the command.

Build, then deploy

The rule is simple: always build before install, and a failed build blocks deployment. When something looks odd, use the clean pattern:

rm -rf target
npm ci
npm run types
npm run build
npm run deploy      # now-sdk install, to the default or --auth alias

Two SDK options deserve care:

  • now-sdk install --reinstall uninstalls and reinstalls the application so the instance matches the package exactly. The CLI reference warns that metadata created on the instance but not present locally will be lost. My routing rules go further: never use transform or --reinstall against global configuration you don't own.
  • now-sdk transform pulls instance changes back into Fluent. It overwrites local files, so commit or stash first.

The workflow document's rules for two-way sync are worth adopting as written: know which side is authoritative for a change, review generated source before committing, do not mix unrelated instance edits into a code change, use @fluent-ignore or other sync directives only with documented reasoning, and keep pull requests small.

Branching and pull requests

There is no direct development on main. Work happens on short-lived branches:

Figure 3.1: Branch model. Short-lived feature and fix branches merge into main through reviewed pull requests with a green build; SDK upgrades are their own change; nobody develops directly on mainFigure 3.1: Branch model. Short-lived feature and fix branches merge into main through reviewed pull requests with a green build; SDK upgrades are their own change; nobody develops directly on main

Branch prefixes are feature/, fix/, chore/ and spike/, and commits follow a conventional style: feat: add incident enrichment rule, fix: correct ACL condition, test: add ATF coverage for request creation, docs: update deployment runbook, chore: upgrade SDK dependency.

Every substantial pull request should state:

  • the requirement or story;
  • the ServiceNow scope;
  • the metadata affected;
  • test evidence;
  • the target instance used;
  • rollback considerations;
  • screenshots only when UI validation matters;
  • known limitations.

That list is also what I ask AI agents to produce in their handoff report, which makes their pull requests reviewable in the same way as anyone else's.

CI quality gates (planned)

The CI gate is issue #9 on the roadmap and is not running yet. The intended pipeline is: checkout, configure Node 24, npm ci, npm run build, static and lint checks as they are added, dependency and security checks, and publishing build and test evidence. Its acceptance criteria include one I consider non-negotiable: no ServiceNow credentials are exposed to untrusted pull request execution.

The SDK adds one check that belongs in every Fluent pipeline. now-sdk build --frozen-keys fails with "Keys file is out-of-date" if the build would change keys.ts, catching anyone who pushed a new identifier without committing the regenerated file. I confirmed that behaviour locally against SDK 4.13.0. A clean npm ci is equally useful: it refuses to install if package.json and the lockfile disagree, which is exactly the kind of drift you want to catch before review, not after a deployment.

A minimal pull request workflow for GitHub Actions, as a starting point rather than a finished file, could look like this:

# .github/workflows/pr-build.yml (illustrative; not yet in the repository)
name: pr-build
on:
  pull_request:
    branches: [main]
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm
      - run: npm ci
      - run: npx now-sdk build --frozen-keys
      - run: npm audit --audit-level=high

Note what is absent: no instance URL, no secrets, no install step. Anything that talks to an instance runs in a separate, trusted job after merge, using environment-scoped secrets.

ATF: tests that ship with the app

ATF tests can be authored in Fluent, which means they live in the same scope, go through the same review and ship with the application. A short server-side test for the table above:

// src/fluent/tests/atf.now.ts
import '@servicenow/sdk/global'
import { Test } from '@servicenow/sdk/core'

Test(
    {
        $id: Now.ID['atf_note_insert'],
        name: 'Practice Note - insert and validate',
        description:
            'Inserts a practice note with status=open, then asserts the stored record has status=open.',
        failOnServerError: true,
    },
    (atf) => {
        const note = atf.server.recordInsert({
            $id: Now.ID['atf_note_insert_step'],
            table: 'x_33764_practice_note',
            fieldValues: { title: 'ATF note', status: 'open' },
            assert: 'record_successfully_inserted',
        })

        atf.server.recordValidation({
            $id: Now.ID['atf_note_validate_step'],
            table: 'x_33764_practice_note',
            recordId: note.record_id,
            fieldValues: 'status=open',
            assert: 'record_validated',
        })
    }
)

The SDK has grown useful ATF tooling this year. TestSuite() (4.11.0) defines suites in Fluent. now-sdk cicd testsuite run and now-sdk cicd test run (from 4.10.0) trigger runs from the command line and exit non-zero on failure, so they can gate a pipeline. now-sdk cicd test logs (4.12.0) pulls failure logs back into CI. On the platform side, ServiceNow's Australia release added ATF code coverage for custom scripts.

Status in the reference programme: the first source-authored ATF test exists in the reference application and is installed on the PDI, but it has not yet been run and its evidence captured (issues #10 and #17). Test fixture strategy and test data clean-up are still to be defined.

Promotion: DEV to TEST to PROD

The repository's environment model:

EnvironmentPurposeDeployment
PDI / LABExperimentsDeveloper
DEVIntegrated engineeringControlled developer or CI
TESTFormal validationPromotion pipeline
PRODLiveApproved release only

The standard lifecycle is: workspace, feature branch, local SDK build, commit, push, pull request, review, DEV deploy, ATF and API validation, merge, TEST promotion, production release.

How the move to TEST and PROD should happen technically is where ServiceNow's own SDLC guide is most useful. Its guidance is that the SDK's direct install is for development instances; test and production should install a published version from the Application Repository through the CI/CD APIs, which gives an immutable versioned artefact, native rollback, a security boundary (instances install from a repository they trust rather than accepting connections from a CI runner) and an audit trail. The guide says it plainly: do not use the SDK to deploy to production. now-sdk cicd publish, install and rollback wrap those APIs.

Figure 3.2: Pull request to CI to deployment (target design). The PR job runs npm ci, a frozen-keys build and an audit with no instance secrets; after review and merge CI installs to DEV, publishes a version to the Application Repository, installs it on TEST and runs ATF, pauses for human approval, then installs the same version on PROD and tags the commitFigure 3.2: Pull request to CI to deployment (target design). The PR job runs npm ci, a frozen-keys build and an audit with no instance secrets; after review and merge CI installs to DEV, publishes a version to the Application Repository, installs it on TEST and runs ATF, pauses for human approval, then installs the same version on PROD and tags the commit

The same pipeline as a flow, with the failure paths:

Figure 3.3: Promotion pipeline with failure paths (target design). Pull request build, merge with a version bump, DEV install, publish to the Application Repository, TEST install and ATF, human approval, then PROD install of the same version and tagging; failures go back to a fix branchFigure 3.3: Promotion pipeline with failure paths (target design). Pull request build, merge with a version bump, DEV install, publish to the Application Repository, TEST install and ATF, human approval, then PROD install of the same version and tagging; failures go back to a fix branch

A few practical points from the guide that are easy to miss:

  • Bump the version before merge. now-sdk cicd publish and install default the version from package.json, and Application Repository versions are immutable, so publishing an existing version fails.
  • Scope secrets per environment. Each job exports only the secrets for its target instance, so a DEV job can never read PROD credentials. For GitHub Actions that means environment secrets, with required reviewers on the production environment to make the approval gate enforceable.
  • Rollback is a feature of the Application Repository path. now-sdk cicd rollback --app-version <previous> reverts within the platform's rollback window. Rollback from Git alone means rebuilding and reinstalling an older tag, and the repository's promotion document rightly adds that releases touching tables, data, fix scripts, scheduled jobs or integrations need an explicit rollback assessment either way.
  • PDIs have limits. ServiceNow's developer community notes that Release Ops does not support PDIs, so the full TEST and PROD path needs real sub-production and production instances. The alternative on-platform route, Release Ops with deployment requests and assessment playbooks, is a sensible choice for teams that prefer to orchestrate releases on the instance.

In the reference programme, these promotion controls (issue #11: PR approval, environment-specific credentials, TEST validation, release tags, a production approval gate, deployment audit and rollback assessment) are on the backlog. The acceptance criterion is the right one: every production release can be tied to a Git commit, a pull request, a tested artefact and an explicit approval.

SDK upgrades are changes too

The SDK moves quickly (five minor releases between mid-July and late September 2026), so the repository treats an upgrade as a deliberate change: read the release notes, update the dependency, clean install, build, test in PDI or DEV, raise a pull request and adopt it in a controlled way. For client repositories, use the SDK version approved for that environment even if npm has something newer.

Part 4 steps back from mechanics to governance: environment tiers, what AI agents may and may not do, secrets, audit, and an honest guide to when Fluent is the wrong tool.

The Code-first ServiceNow series

  1. Code-first ServiceNow: why Git, not the instance, now holds my application
  2. Building a ServiceNow code-first workstation: Ubuntu 24.04, Docker, Coder and Node 24
  3. From .now.ts to production: the ServiceNow SDK, Fluent and Git workflow (you are here)
  4. Governing code-first ServiceNow: environment tiers, AI-agent guardrails and when to stay native

Sources

Expert Commentary

The Fluent code is the easy part. The discipline that matters is the loop around it: build before deploy, a pull request for every change, a CI gate that runs without instance credentials, ATF that ships with the app, and promotion to TEST and PROD through the Application Repository rather than direct SDK installs.

Topics
ServiceNowServiceNow SDKFluentTypeScriptGitCI/CDATF
All insights

Need Help With Your Implementation?

Get expert guidance from our certified ServiceNow and AWS architects.

Schedule a Consultation