skip to content

In TeamCity, what is the Kotlin DSL, and how does a .teamcity/settings.kts file in your repository become the server's build configuration?

level: middleimportance: should knowfreq 55%

answer

  1. Settings live in VCS, not just the server
  2. Kotlin format, entry point settings.kts
  3. Server compiles, then applies the model
  4. Compile failure keeps previous settings
  5. Secrets are tokens, never plaintext

basics

~20 s

TeamCity's Kotlin DSL describes projects and build configurations as compiled Kotlin code in .teamcity/settings.kts. Enabling Versioned Settings on a project makes the server fetch that file from VCS, compile it, and apply the result as the project's live settings.

solid answer

~50 s

TeamCity normally stores settings on the server, edited through the UI. The Versioned Settings feature makes a project keep those settings in a VCS root instead, in either XML or Kotlin format. In Kotlin format the repository carries a `.teamcity` directory whose entry point is `settings.kts`: a script that builds a `project { }` graph of `buildType`s, VCS roots, triggers, steps and dependencies using the `jetbrains.buildServer.configs.kotlin` API. When a commit lands, the server checks out the settings, compiles the script and applies the resulting model; if it does not compile, the server reports the error and keeps the previously applied settings rather than wiping the project. Synchronization can be one-way, where VCS is the source of truth, or two-way, where UI edits are committed back. Secrets are not inlined — password parameters appear as `credentialsJSON:` tokens pointing at values held on the server.

code

kotlin · 21 lines
kotlin
import jetbrains.buildServer.configs.kotlin.*
import jetbrains.buildServer.configs.kotlin.buildSteps.gradle
import jetbrains.buildServer.configs.kotlin.triggers.vcs

version = "2025.03"

project {
    listOf("payments", "billing", "reporting").forEach { service ->
        buildType {
            id("Build_${service}")
            name = "Build $service"
            vcs { root(DslContext.settingsRoot) }
            steps {
                gradle {
                    tasks = ":$service:build"
                }
            }
            triggers { vcs { } }
        }
    }
}

go deeper

for a junior

Know that TeamCity settings can live in the repository under .teamcity as Kotlin code rather than only in the server UI, and that the server reads and applies that file.

for a middle

Explain the mechanics: Versioned Settings with Kotlin format, settings.kts as the entry point, the server compiling and applying the model, and what happens when compilation fails.

for a senior

Show you have operated it — sync direction and who is allowed to edit in the UI, how secure values stay off disk as server-side tokens, and how ID or UUID changes detach build history during a refactor.

for a principal

Own the decision of whether configuration-as-code belongs in the product repository at all, how DSL API versions couple to server upgrades across many teams, and who reviews pipeline changes once they are code.

## The problem the DSL solves TeamCity is a server-side CI product: historically you created a project, added build configurations, and clicked your way through VCS roots, triggers, build steps and dependencies. That configuration lived in the server's Data Directory as XML, not in your repository. It worked, but it meant configuration was not reviewable in pull requests, not versioned with the code it built, and painful to duplicate across dozens of near-identical services. The **Versioned Settings** feature moves a project's settings into a VCS root. You choose a format — XML (essentially the server's own storage format) or **Kotlin**, the typed DSL. Kotlin format is what people mean when they say "TeamCity Kotlin DSL". ## What lives in the repository With Kotlin format enabled, TeamCity generates a `.teamcity` directory in the repository. Its entry point is `settings.kts`. Alongside it sits a Maven project descriptor so an IDE can resolve the DSL library from the server and give you completion and navigation over the real API — the same jars the server compiles against. A minimal script looks like this: ```kotlin import jetbrains.buildServer.configs.kotlin.* import jetbrains.buildServer.configs.kotlin.buildSteps.gradle import jetbrains.buildServer.configs.kotlin.triggers.vcs version = "2025.03" project { buildType(Build) } object Build : BuildType({ name = "Build" vcs { root(DslContext.settingsRoot) } steps { gradle { tasks = "clean build" } } triggers { vcs { } } }) ``` Three things are worth noticing. The `version` value pins the DSL API version, which is tied to the TeamCity server version — an old server cannot compile a script written against a newer API. `DslContext.settingsRoot` refers to the VCS root the settings themselves came from, so the script does not have to hardcode a repository URL. And `object Build : BuildType({ ... })` is ordinary Kotlin: the configuration is a value in a program, not a document. ## How the server applies it On each change to the settings VCS root, the server checks the settings out, compiles the Kotlin, executes it to produce an in-memory model of projects and build configurations, and reconciles that model with what the project currently has. Because it is a compile-then-run step, an error in your script is a **compilation failure surfaced on the project's Versioned Settings page**, and the server continues serving the last settings it successfully applied. That is a meaningful safety property: a bad commit to `settings.kts` breaks the settings update, not the ability to run builds. Synchronization has a direction. In one-way mode, VCS is authoritative and UI edits are either forbidden or transient. In two-way mode, edits made in the UI are committed back to the repository by the server, which is convenient for people who prefer clicking but produces machine-authored commits that reviewers must read. ## Identity and history Each build configuration carries a stable ID derived from its object name and the project, plus a UUID recorded in the DSL for entities created through the UI. The IDs are how builds, history and artifacts stay attached to a configuration. Renaming an object so its ID changes — or changing a UUID — makes TeamCity treat it as a different configuration and detach the old build history. This is the most common self-inflicted injury when refactoring a DSL, and the reason people rename cautiously and check the generated IDs. ## Secrets Settings in VCS must not carry plaintext credentials. TeamCity handles this by keeping secure values on the server and writing a token into the DSL instead: a password parameter appears as a `credentialsJSON:` value referencing the server-stored secret. The consequence is that a `settings.kts` file is not fully portable to another server on its own — the referenced secrets have to exist there too. ## When it is worth it The DSL pays off when you have many similar configurations: a function or loop that emits one build configuration per service replaces dozens of hand-maintained copies, and a change to the shared function updates all of them at once. Templates and inheritance from the classic model still exist and are expressible in the DSL. For a project with two build configurations that rarely change, the extra machinery — a compile step, an API version coupled to the server, Kotlin knowledge in review — mostly buys ceremony.

  • What happens to a build configuration's history if you rename its object in settings.kts?
    If the rename changes the configuration's ID, TeamCity treats it as a new configuration: the old one is removed and its build history, statistics and artifacts no longer appear under the new one. That is why refactors keep IDs stable — via an explicit id, or by renaming deliberately and accepting the loss.
  • What does two-way synchronization of versioned settings change about day-to-day work?
    UI edits are allowed and the server commits them back to the settings repository. It keeps clicking possible for people who dislike Kotlin, but it means the repository receives machine-authored commits that nobody reviewed before they took effect, so the pull request is no longer the gate on configuration changes.
  • Why does the DSL script pin a version value at the top?
    It declares which TeamCity DSL API version the script targets. The server compiles the script against that API, so the pinned version has to be supported by the server you are running. Upgrading the server can require bumping it; targeting a newer API than the server knows fails to compile.

saying these in an interview costs you the question

  • Thinks the DSL is YAML with Kotlin-ish syntax
  • Believes a broken settings.kts deletes the project's configurations
  • Puts real passwords in settings.kts because the repo is private
  • Assumes any settings.kts compiles on any TeamCity server version
  • Renames DSL objects freely, unaware build history detaches

context