Named placeholders that resolve at load and deployment time. Pull in file contents, run queries against the target server, and override values per environment without editing the package.
By the SchemaSmith Team · Last reviewed
Script tokens are how one schema package becomes ten environments.
Script tokens are how one schema package becomes ten environments. Define a token once, reference it everywhere, override it per environment without touching a script file. The simple form is just two curly braces and a name — but tokens go far beyond that. They can pull in file contents, execute server-side queries to generate values at deployment time, embed entire table schemas as JSON, and surface custom metadata you've attached through Extensions. One feature, many superpowers, all in the free toolset.
The basic form:
{{TokenName}}
Token replacement is case-insensitive. These all resolve to the same value:
SELECT * FROM [{{MainDB}}].dbo.Customers
SELECT * FROM [{{maindb}}].dbo.Customers
SELECT * FROM [{{MAINDB}}].dbo.Customers
Tokens that appear in scripts but have no matching definition are left in place unchanged. There is no silent corruption from an unresolved token.
Tokens are replaced in every place SchemaSmith processes script content, both in JSON expression fields and in .sql files.
BaselineValidationScriptValidationScriptVersionStampScriptBaselineValidationScriptDatabaseIdentificationScriptVersionStampScriptInside table JSON files:
CheckExpression on columnsDefault on columnsExpression on table-level check constraintsFilterExpression on indexes (where supported)ShouldApplyExpression on tables, columns, indexes, foreign keys, check constraints, indexed views, materialized views, and other supported componentsIn every product and template script folder:
| Folder | Scope |
|---|---|
Before Product/, After Product/ |
Product |
Before Scripts/, After Scripts/ |
Template |
Schemas/ |
Template |
DataTypes/ (SQL Server) / Domain Types/, Enum Types/, Composite Types/ (PostgreSQL) / equivalents |
Template |
Functions/, Views/, Procedures/ |
Template |
Triggers/, DDLTriggers/ |
Template |
Table Data/ |
Template |
Any custom script folders defined in Template.json ScriptFolders |
Template |
The exact set of default folders depends on the product's Platform. Anything you add via custom ScriptFolders participates in token resolution exactly the same way as the defaults.
Define tokens in Product.json under ScriptTokens. These are available across every template and product-level script.
{
"Name": "SaasProduct",
"TemplateOrder": ["Registry", "Client"],
"ScriptTokens": {
"RegistryDb": "Registry",
"MigrationVersion": "1.0.1"
}
}
In this example, {{RegistryDb}} resolves to Registry everywhere: in every template's scripts and in every SQL file across the product.
Define tokens in Template.json under ScriptTokens. Template tokens override product tokens with the same key for the duration of that template's execution, and they can also introduce new tokens that only exist inside the template.
Say your Product.json defines a product-wide default for MainDB:
{
"Name": "SaasProduct",
"TemplateOrder": ["Registry", "Reporting"],
"ScriptTokens": {
"MainDB": "ProductionMain"
}
}
Then one of its templates overrides MainDB and adds a new SchemaOwner token that the product never declared:
{
"Name": "Reporting",
"ScriptTokens": {
"MainDB": "ReportingAlias",
"SchemaOwner": "rpt"
}
}
Inside the Reporting template, {{MainDB}} resolves to ReportingAlias instead of the product-scope ProductionMain. The new {{SchemaOwner}} token is available only inside this template; other templates in the same product don't see it.
SchemaSmith adds these tokens automatically. You don't define them; they appear when relevant.
| Token | Value | Available in |
|---|---|---|
{{ProductName}} |
The Name from Product.json |
All product and template scripts |
{{TemplateName}} |
The Name from the current Template.json |
Template scripts only |
{{TableSchema}} |
Full serialized JSON of every table in the current template, with single quotes escaped ('') for safe embedding in SQL string literals |
Template scripts |
{{IndexedViewSchema}} |
Full serialized JSON of every indexed view in the current template (SQL Server) | Template scripts |
{{MaterializedViewSchema}} |
Full serialized JSON of every materialized view in the current template (PostgreSQL) | Template scripts |
{{TableSchema_<TemplateName>}} |
Same as {{TableSchema}} but reaches across templates; name another template explicitly to read its table set |
Any template |
{{IndexedViewSchema_<TemplateName>}} |
Cross-template indexed view JSON | Any template |
{{MaterializedViewSchema_<TemplateName>}} |
Cross-template materialized view JSON | Any template |
{{ObjectScripts_<TemplateName>}} |
Cross-template inventory of programmable object scripts (functions, views, procedures, triggers) | Any template |
{{QueryTokens_<TemplateName>}} |
Cross-template inventory of query-style tokens for sharing | Any template |
{{TableSchema}} is the entire current template's table model serialized as JSON, ready to drop into a stored procedure parameter, a JSON_VALUE/json_each/JSON_TABLE query, or an audit row. Combined with the Custom Properties feature, you can write a single migration script that introspects your table definitions and reads your team's custom metadata at deployment time, without ever leaving SchemaSmith.
The cross-template variants ({{TableSchema_OtherTemplate}}) are how a deployment script in one template can read the schema of another. This unlocks coordination between linked templates without copy-pasting JSON.
When the referenced template is a schema template, each table or view's Schema field would normally hold the per-iteration {{SchemaName}} token; the cross-template snapshot replaces it with the literal placeholder <per-iteration>, signalling that those schemas are iteration-dependent. Use the cross-template variants for structural introspection (column counts, custom properties, table names) — schema-qualified DDL still needs to live inside the schema template itself, where {{SchemaName}} resolves per iteration. See the schema-name token below.
Anything you put in an Extensions object on a table component is also available as a token in that component's expression fields. Bare names come from the component's own Extensions; Table.-prefixed names come from the parent table. See Custom Properties for the full mechanism.
{
"Name": "[Orders]",
"Extensions": { "Environment": "Production" },
"Indexes": [
{
"Name": "[IX_Orders_CreatedDate]",
"IndexColumns": "[CreatedDate]",
"ShouldApplyExpression": "SELECT CASE WHEN '{{Table.Environment}}' = 'Production' THEN 1 ELSE 0 END"
}
]
}
That single expression lets the index decide whether to apply itself based on metadata that lives on the parent table, without leaving the schema package or splitting the definition across environment-specific files.
Schema templates make the active iteration's schema name available everywhere tokens resolve — script bodies, JSON field values, table data references, and dependent token values. Without {{SchemaName}}, deploying a product to dozens or hundreds of tenant schemas would require either per-tenant script files or a complex external preprocessing step. With it, a single set of scripts fans out to every schema in one quench, and each iteration sees a fully-qualified, unambiguous version of every object reference it touches.
{{SchemaName}} is a built-in token set by the engine at the start of each schema-template iteration. You don't define it in ScriptTokens — it appears automatically when the engine is running inside a schema template's iteration context.
{{SchemaName}} is available inside any iteration of a schema template — a Template.json whose SchemaIdentificationScript field is set. It resolves in every place tokens normally resolve:
Default, CheckExpression, Expression, FilterExpression, ShouldApplyExpressionBaselineValidationScript and VersionStampScript on the templateScriptTokens values (covered below){{SchemaName}} is not available outside a schema-template iteration:
Template.json without SchemaIdentificationScript), {{SchemaName}} is unresolved. The engine produces a clear error if it appears in a context where it would need to substitute.Product.json BaselineValidationScript, VersionStampScript) — those run at product scope, before and after all template iterations, with no iteration in scope.The most common use is qualifying every object reference that belongs to the current tenant's schema. From the TenantCRM demo's AddCustomer.sql:
CREATE OR ALTER PROCEDURE [{{SchemaName}}].[AddCustomer]
@CustomerName NVARCHAR(128),
@Email NVARCHAR(256) = NULL,
@CustomerID INT = NULL OUTPUT
AS
BEGIN
INSERT INTO [{{SchemaName}}].[Customers] ([CustomerName], [Email])
VALUES (@CustomerName, @Email);
INSERT INTO [dbo].[GlobalAuditLog] ([TenantName], [EventType], [Detail])
VALUES (N'{{SchemaName}}', N'CustomerAdded', N'Name=' + @CustomerName);
END;
CREATE OR REPLACE PROCEDURE "{{SchemaName}}".add_customer(
p_customer_name VARCHAR(128),
p_email VARCHAR(256) DEFAULT NULL
)
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO "{{SchemaName}}".customers (customer_name, email)
VALUES (p_customer_name, p_email);
INSERT INTO public.global_audit_log (tenant_name, event_type, detail)
VALUES ('{{SchemaName}}', 'CustomerAdded', 'name=' || p_customer_name);
END;
$$;
Notice that {{SchemaName}} qualifies the procedure itself, the insert target, and also appears as a plain string value in the GlobalAuditLog insert — recording which tenant's schema this event came from. {{SchemaName}} substitutes as text before the script executes, so it can play both roles: identifier qualifier and string literal.
Cross-schema references like [dbo].[GlobalAuditLog] and public.global_audit_log are hard-coded with their literal schema prefix. Shared tables that span tenants aren't in the iteration schema, so they stay fully qualified with their actual schema name.
Tables in a schema template have their Schema field defaulted to {{SchemaName}} automatically — you don't need to set it. When you omit the schema prefix from the filename (Customers.json instead of dbo.Customers.json), the engine fills it in for each iteration.
For foreign keys pointing to shared tables in another schema, set RelatedTableSchema explicitly:
{
"Name": "[Customers]",
"ForeignKeys": [
{
"Name": "[FK_Customers_Countries]",
"Columns": "[CountryCode]",
"RelatedTableSchema": "[dbo]",
"RelatedTable": "[Countries]",
"RelatedColumns": "[Code]"
}
]
}
Without an explicit RelatedTableSchema, the engine defaults it to {{SchemaName}} — which would incorrectly point the FK at tenant_acme.Countries instead of dbo.Countries. Explicit cross-schema references are preserved as-is. For the full Template.json field reference and all schema-template fields, see Schema Templates.
A user-defined ScriptTokens value can itself contain {{SchemaName}}. The engine detects this at template load and escalates that token to per-iteration resolution — it can't be cached at load time or across database deployments because the value differs by schema. See Resolution timing for how this fits the three-tier model.
{
"ScriptTokens": {
"TenantAuditTable": "{{SchemaName}}.AuditLog"
}
}
Any script that references {{TenantAuditTable}} gets the iteration-qualified value for each tenant without needing to write {{SchemaName}} in every script. For a full narrative walkthrough of schema templates — authoring layout, deployment log, tenant onboarding, and cross-schema FK patterns — see Multi-Tenant Deployments.
Independent of the priority order in Token Resolution Order, each token also has a resolution frequency — how many times the engine computes its value during a deployment run. There are three tiers:
| Tier | When the value is computed | Which tokens |
|---|---|---|
| Per product | Once, at template load — before any database connection is opened | Fully static tokens: plain string values with no <*Query*> tag and no {{SchemaName}} reference |
| Per database | Once per target database, just before that database's scripts run | <*Query*> tokens whose body doesn't reference {{SchemaName}} directly or transitively |
| Per iteration | Once per schema-template iteration, with the iteration's {{SchemaName}} substituted in first | Any token whose body references {{SchemaName}} directly or transitively |
The tier matters because a per-product value is computed once and reused everywhere, while a per-iteration value is freshly computed for tenant_acme, then again for tenant_beta, and so on. Per-database query tokens run against the live server but are cached across the iterations of a single target database — so the connection round-trip happens once per database, not once per tenant.
The escalation rule. The engine can only promote a token's resolution tier, never demote it. A <*Query*> token without any {{SchemaName}} reference could legitimately depend on database-specific state — the absence of {{SchemaName}} isn't proof the result is the same for every tenant. Only the presence of {{SchemaName}} (directly, or transitively through a token it references) lets the engine conclude the value must be recomputed per iteration.
Transitive escalation. If token A's body references {{SchemaName}} and token B's body references {{A}}, then B is also per-iteration — even if B's body contains no {{SchemaName}} text. The engine walks the token dependency graph at template load and marks every transitively reachable token.
Using an iteration-scoped token in a product-level script (Product.json BaselineValidationScript or VersionStampScript) is a validation error at template load. Those scripts run outside any iteration, and the engine has no schema name to substitute into the token's value.
This is where tokens earn their keep. Override product token values in a settings file without modifying the schema package. Same package, different environments, different values.
Add a ScriptTokens section to your tool's settings file (for example, SchemaQuench.settings.json):
{
"Target": {
"Server": "staging-server"
},
"ScriptTokens": {
"RegistryDb": "Registry_Staging",
"MigrationVersion": "1.0.1-rc1"
}
}
Config overrides only apply to tokens that already exist in Product.json. You can't introduce new tokens via configuration alone; the package declares the contract, the environment fills in the values.
Override product token values using environment variables. The naming pattern is:
SmithySettings_ScriptTokens__TokenName=Value
Note the prefix SmithySettings_ (single underscore) and the double underscore __ before the token name. This follows the .NET configuration environment variable convention.
rem Windows (cmd)
set SmithySettings_ScriptTokens__RegistryDb=Registry_CI
set SmithySettings_ScriptTokens__MigrationVersion=1.0.1-ci.42
rem Windows (PowerShell)
$env:SmithySettings_ScriptTokens__RegistryDb = "Registry_CI"
$env:SmithySettings_ScriptTokens__MigrationVersion = "1.0.1-ci.42"
export SmithySettings_ScriptTokens__RegistryDb="Registry_CI"
export SmithySettings_ScriptTokens__MigrationVersion="1.0.1-ci.42"
Like config-level overrides, environment variable overrides only apply to tokens that already exist in Product.json. See Configuration for the full settings-and-environment layering.
When the same token name appears in multiple places, the most specific definition wins. Tokens resolve in layers, from lowest to highest priority:
| Priority | Source | Scope |
|---|---|---|
| 1 (lowest) | Product.json ScriptTokens |
All scripts |
| 2 | Settings file ScriptTokens section |
Overrides matching product keys |
| 3 | Environment variables (SmithySettings_ScriptTokens__*) |
Overrides matching product keys |
| 4 (highest) | Template.json ScriptTokens |
Template scripts only |
Product.json.ProductName token is added.ScriptTokens are merged on top of the resolved product tokens. Template tokens with matching keys win.TemplateName, TableSchema, IndexedViewSchema, and MaterializedViewSchema tokens are added.*_<TemplateName> tokens are added once all templates have loaded.Extensions are merged in per component when the table model is processed.<*File*>, <*Query*>, and the rest) resolve as described above, then the simple {{TokenName}} substitution runs against the final token map.Four scenarios that tie the mechanics together. Each shows the same token shape across SQL Server, PostgreSQL, and MySQL; the SQL dialect on each tab reflects what that engine expects.
A SaaS product uses tokens to manage database names and version stamps across development, staging, and production. Product.json defines the baseline:
{
"Name": "SaasProduct",
"TemplateOrder": ["Registry", "Client"],
"ScriptTokens": {
"RegistryDb": "Registry",
"MigrationVersion": "3.2.0"
}
}
A migration script announces the deployment target using the resolved tokens. The token shape is identical across platforms; only the logging statement differs.
PRINT 'Deploying {{ProductName}} '
+ '{{MigrationVersion}} against '
+ '{{RegistryDb}}';
DO $$ BEGIN
RAISE NOTICE 'Deploying % % against %',
'{{ProductName}}',
'{{MigrationVersion}}',
'{{RegistryDb}}';
END $$;
SELECT CONCAT(
'Deploying {{ProductName}} ',
'{{MigrationVersion}} ',
'against {{RegistryDb}}'
) AS deployment_banner;
For staging, drop a ScriptTokens.RegistryDb override into SchemaQuench.settings.json and ship the same package. For CI, do the same via SmithySettings_ScriptTokens__RegistryDb. The schema package is unchanged across all three environments; only the resolved token values differ.
When templates need to reference sibling schema managed by other templates, product-level tokens keep the references consistent. The token pattern is the same across platforms; what the tokens encode differs because each engine isolates differently. SQL Server and MySQL use separate databases, PostgreSQL uses schemas within one database.
Separate databases, three-part names:
{
"Name": "ECommerce",
"TemplateOrder": ["Catalog", "Orders", "Reporting"],
"ScriptTokens": {
"CatalogDb": "ProductCatalog",
"OrdersDb": "OrderProcessing"
}
}
CREATE OR ALTER VIEW dbo.SalesSummary AS
SELECT o.OrderDate, p.ProductName, o.Quantity, o.Total
FROM [{{OrdersDb}}].dbo.Orders o
JOIN [{{CatalogDb}}].dbo.Products p ON o.ProductId = p.Id;
One database, separate schemas:
{
"Name": "ECommerce",
"TemplateOrder": ["Catalog", "Orders", "Reporting"],
"ScriptTokens": {
"CatalogSchema": "product_catalog",
"OrdersSchema": "order_processing"
}
}
CREATE OR REPLACE VIEW analytics.sales_summary AS
SELECT o.order_date, p.product_name, o.quantity, o.total
FROM {{OrdersSchema}}.orders o
JOIN {{CatalogSchema}}.products p ON o.product_id = p.id;
Separate databases, db-qualified names:
{
"Name": "ECommerce",
"TemplateOrder": ["Catalog", "Orders", "Reporting"],
"ScriptTokens": {
"CatalogDb": "product_catalog",
"OrdersDb": "order_processing"
}
}
CREATE OR REPLACE VIEW `analytics`.`SalesSummary` AS
SELECT o.`OrderDate`, p.`ProductName`, o.`Quantity`, o.`Total`
FROM `{{OrdersDb}}`.`Orders` o
JOIN `{{CatalogDb}}`.`Products` p ON o.`ProductId` = p.`Id`;
PostgreSQL can't join across separate databases natively. The idiomatic pattern is one database with multiple schemas, and SchemaSmith deploys each template against its own schema within that database. Cross-database queries via postgres_fdw or dblink are possible but add setup that's out of scope for most deployments; if you need them, the token approach still works: define foreign-server tokens and reference them the same way.
A migration script that consumes the live table model to drive its own logic. The stored procedure walks the JSON, reads each table's columns and any Extensions.AuditScope you've attached, and emits the right CREATE TRIGGER statements: one declarative source of truth, one runtime that adapts to it.
DECLARE @TableSchema NVARCHAR(MAX) = '{{TableSchema}}';
EXEC dbo.GenerateAuditTriggers @TableSchema;
DO $$
DECLARE
v_table_schema TEXT := '{{TableSchema}}';
BEGIN
CALL public.generate_audit_triggers(v_table_schema);
END $$;
SET @TableSchema = '{{TableSchema}}';
CALL GenerateAuditTriggers(@TableSchema);
A Before migration script needs to know which rows on the server are flagged for the new feature. Point the token at a query file using <*QueryFile*> so the SQL stays readable, and SchemaSmith runs that query once against the actual server you're deploying to before substituting the result into every script that references the token.
The token definition is the same shape across platforms; the query body differs because each engine has its own catalog and quoting style.
Token points at a query file:
{
"ScriptTokens": {
"EnabledTargets": "<*QueryFile*>queries/enabled-targets.sql"
}
}
queries/enabled-targets.sql:
SELECT TargetName
FROM dbo.FeatureFlags
WHERE FlagName = 'NewBilling'
AND Enabled = 1;
Any script in the template:
-- {{EnabledTargets}} resolves to one target per line
PRINT 'Applying billing schema to: {{EnabledTargets}}';
Token points at a query file:
{
"ScriptTokens": {
"EnabledTargets": "<*QueryFile*>queries/enabled-targets.sql"
}
}
queries/enabled-targets.sql:
SELECT target_name
FROM public.feature_flags
WHERE flag_name = 'NewBilling'
AND enabled = true;
Any script in the template:
-- {{EnabledTargets}} resolves to one target per line
DO $$ BEGIN
RAISE NOTICE 'Applying billing schema to: %', '{{EnabledTargets}}';
END $$;
Token points at a query file:
{
"ScriptTokens": {
"EnabledTargets": "<*QueryFile*>queries/enabled-targets.sql"
}
}
queries/enabled-targets.sql:
SELECT `TargetName`
FROM `FeatureFlags`
WHERE `FlagName` = 'NewBilling'
AND `Enabled` = 1;
Any script in the template:
-- {{EnabledTargets}} resolves to one target per line
SELECT CONCAT('Applying billing schema to: ',
'{{EnabledTargets}}') AS status;
Across these four scenarios, the token shape stays constant — {{TokenName}} in scripts, ScriptTokens in JSON — while the SQL dialect and the override surface flex per environment. The same schema package ships unchanged to development, staging, and production; the token layer is where each environment's shape gets expressed.
Define tokens once in Product.json and override them per environment with settings files or environment variables, never editing the script files.
Start the tokens lab