Course 2 · Module 4 — Script tokens: adjustable dyes
You’ve got the same deploy saved three times — one copy for dev, one for staging, one for prod — and they differ by a database name and a version string. Change the schema and now you’re editing three files in lockstep, praying you didn’t fat-finger the prod copy. That’s not version control; that’s a copy-paste museum. And the day the dev copy and the prod copy quietly drift apart is the day you ship the wrong one. Let’s stop keeping copies. Let’s keep one package and let each environment fill in its own values.
One name, many values
Section titled “One name, many values”A script token is the simplest powerful thing in the toolkit: two curly braces and a name.
PRINT 'Deploying {{ProductName}} {{ReleaseVersion}} to {{Environment}}';You write {{Environment}} once in the script. What it becomes is decided per target — Development here, Production there — and the script file never changes between them. That’s the whole idea behind Forge’s name for them: adjustable dyes. Same casting, same mold, different color in every batch. The metal’s identical; the dye is what you swap.
Tokens are case-insensitive ({{env}} and {{ENV}} hit the same value), and a token with no matching definition is left exactly as-is — no silent corruption from a typo’d name. They resolve everywhere SchemaSmith processes script content: .sql files in every folder, and JSON expression fields like Default, CheckExpression, and ValidationScript.
Where the values come from
Section titled “Where the values come from”Here’s the layering that makes “one package, many environments” real. A token’s value can be set in three places, and the more specific one wins:
Product.json→ScriptTokensis the baseline — your package’s default values, shipped with it.- The settings file →
ScriptTokensoverrides those defaults for a given run, without editing the package. - Environment variables (
SmithySettings_ScriptTokens__TokenName) override them again — the CI-pipeline hook, same mechanism, no file at all.
Settings and environment overrides can only change tokens the package already declares. The package owns the contract — which tokens exist — and the environment fills in the values. That’s the discipline: you can’t smuggle a brand-new token in from a config file, so a package’s token surface is always knowable from the package itself.
The lab ships one product, BillingService, whose Product.json declares the dev baseline:
"ScriptTokens": { "Environment": "Development", "ReleaseVersion": "2.4.0-dev", "EngineVersion": "<*Query*>SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128))"}And two settings files point at the same package. dev.settings.json adds no overrides — it rides the package defaults. prod.settings.json overrides the two static values:
"ScriptTokens": { "Environment": "Production", "ReleaseVersion": "2.4.0"}That’s the entire difference between a dev deploy and a prod deploy: one settings file overrides two tokens. The package — every table, every script — is byte-for-byte identical.
Watch the dye change
Section titled “Watch the dye change”The package defines a DeploymentLog table and an [ALWAYS] after-script that stamps the run’s resolved tokens into it. Deploy with the dev settings first:
cd sqlserverschemaquench --ConfigFile:dev.settings.jsonSchemaQuench echoes the resolved tokens right at the top, so you can see what this run will use before it touches anything:
Product Script Tokens: Environment: Development ReleaseVersion: 2.4.0-dev ProductName: BillingServiceThen it creates the table and runs the stamp script:
Resolving Product Level Query Tokens[localhost,11433].[learn] Adding new table [dbo].[DeploymentLog][localhost,11433].[learn] Quenching .\Package\Templates\Main\After Scripts\Stamp Deployment [ALWAYS].sql[localhost,11433].[learn] Successfully QuenchedNow deploy the same package with the prod settings — no file edits, just a different --ConfigFile:
schemaquench --ConfigFile:prod.settings.json Product Script Tokens: Environment: Production ReleaseVersion: 2.4.0 ProductName: BillingServiceTwo runs, one package. Let’s prove the dye took. Read the table back:
SELECT Environment, ProductName, ReleaseVersion, EngineVersion FROM dbo.DeploymentLog ORDER BY Environment;Environment ProductName ReleaseVersion EngineVersion------------- ---------------- --------------- -------------Development BillingService 2.4.0-dev 16.0.4260.1Production BillingService 2.4.0 16.0.4260.1Two rows, same package, different values. The Environment and ReleaseVersion columns differ because the settings file overrode the tokens; everything else came from the one package. Run either deploy a second time and you get no new row — the stamp script is idempotent, so re-running dev doesn’t pile up duplicate dev rows. Define once, deploy everywhere, no copies to drift.
Tokens that compute themselves
Section titled “Tokens that compute themselves”Look back at that third token. EngineVersion didn’t hold a value — it held a query token:
"EngineVersion": "<*Query*>SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128))"A <*Query*> token runs its SQL against the live target at deployment time, then drops the result in wherever {{EngineVersion}} appears. You didn’t type 16.0.4260.1 anywhere — SchemaSmith asked the server and the answer became the value. That’s the line in the log that says Resolving Product Level Query Tokens: the query firing before your scripts run.
That’s what “self-maintaining” means here. The script doesn’t carry the answer; it carries the question, and the answer is always current because it’s computed against the server you’re actually deploying to. Point the same package at a different server and the value follows the server — no edit. (<*Query*> has a small family: <*File*> pulls in a file’s contents, <*BinaryFile*> injects binary as the engine’s literal form, and <*QueryFile*> keeps a long query in its own file. All resolve the same way — your script just sees {{TokenName}}.)
The system token that knows your whole schema
Section titled “The system token that knows your whole schema”There’s one automatic token worth meeting now, because the next-but-one module is built on it. {{TableSchema}} resolves to the full JSON of every table in the current template — column names, types, indexes, and any custom metadata you’ve attached. The system hands it to you; you don’t define it.
DECLARE @TableSchema NVARCHAR(MAX) = '{{TableSchema}}';EXEC dbo.GenerateAuditTriggers @TableSchema;A migration script that reads your own table model and adapts as the schema grows — add a column, and the next run sees it without you re-authoring the script. That’s the doorway to custom properties: your team’s own metadata, attached to the model and read at deploy time through this exact token. We’ll forge that in a later module; for now, just know the system already hands your scripts the whole shape of your schema.
Per-engine notes
Section titled “Per-engine notes”The token shape ({{TokenName}}, <*Query*>) is identical on all three engines — only the SQL inside flexes:
| SQL Server | PostgreSQL | MySQL | |
|---|---|---|---|
| Engine-version query | SERVERPROPERTY('ProductVersion') → 16.0.4260.1 | split_part(current_setting('server_version'),' ',1) → 16.13 | VERSION() → 8.0.45 |
| New-table wording | Adding new table [dbo].[DeploymentLog] | Create new table public.deploymentlog | Create table `DeploymentLog` |
| Idempotent stamp | WHERE NOT EXISTS | WHERE NOT EXISTS | INSERT IGNORE (MySQL forbids referencing an insert’s target table in a subquery) |
Same package contract across all three; the dye layer is where each environment — and each engine — expresses its own shape.
Check yourself: How do you deploy the same script to dev and prod with different values — a different database name, version stamp, or environment label — without editing it per environment?
Use script tokens resolved per target. Write {{TokenName}} in the script once, declare its default in Product.json, and override the value per environment in that environment’s settings file (or via a SmithySettings_ScriptTokens__ environment variable). The package never changes between dev and prod — only the resolved token values do.
Think of script tokens as the rack of dyes beside the forge. The casting is the same every time — one mold, one package — but you reach for a different dye per batch, and some dyes mix themselves from whatever’s in the crucible the moment you pour. One package, every environment, no copies on the bench to drift out of true. The metal stays honest; the color follows the job.
Want the deeper detail? The script-tokens reference documents every advanced tag, the full override layering, and the three-tier resolution timing (per-product, per-database, per-iteration). Run the lab on all three engines to feel the same package land two different ways in your own hands.
Got a token puzzle — a value that needs to come from the target server, or a package you’re trying to collapse from three copies down to one? Email me at forgebarrett@schemasmith.com — I read every one.
Next up: schema’s parameterized and deploys itself — but the rows that ride along with it, the lookup and reference data, need to ship too. That’s the data side of the forge, and it’s where we head next.
Until then, may your dyes hold their color in every fire, and your one package fit every forge you carry it to.
— Forge