Templates
Introduction
Templates allow you to use the components in a workspace as a pattern for creating or updating new components. A template consists of the baseline components you want to use as a pattern, optional input variables, and the logic for how to transform the baseline components into new components. Templates are written as TypeScript functions in a declarative style and executed with the si command line tool. They are typically stored in git.
TIP
Templates are written in a declarative internal TypeScript DSL. You build up the behavior you want, and then the template engine executes it on your behalf.
When a template is run it will:
- Build the set of "baseline" components, either by searching a workspace or reading a cached YAML file
- Load any input variables you define
- Apply transformations to the baseline, creating a "working set" of components that should be created or updated
- Update any subscriptions that are inclusive of the working set to point to the new components
- Create a Change Set
- Search for existing components that were previously created by this template, creating the "existing set"
- Compare the "working set" to the "existing set", determining what components need to be created, updated, or deleted
- Perform the create, update, or delete operations in the Change Set
Each invocation of a template uses a supplied "invocation key", which is used to both name the change set and determine which components should be in the existing set.
INFO
Templates are idempotent (they only make changes they need to make) and convergent (they will move from an incorrect state to a correct state) when run repeatedly in a workspace with the same invocation key. For example: if you applied a "Standard VPC" template to a workspace with the invocation key "default-vpc", ran it, then updated the template to remove NAT Gateways, the NAT Gateways created in the initial run would be deleted.
This makes templates easy to iterate on, and useful as a tool for ongoing evolution of the components they create (even after they are applied).
The general workflow in developing templates is:
- Create or Discover a working implementation in a workspace
- Write the template that applies transformations to your working example
- Iterate on your template by running it repeatedly with the same invocation key
- Create a baseline cache of the working example
- Run the template in another workspace, using the baseline cache
Generating new templates
To generate a new template with the CLI:
$ si template generate rebuild
✨ info si Generating template structure
✨ info si Template generated successfully
Template generated: /home/adam/src/template-test/rebuild.ts
Next steps:
1. Edit /home/adam/src/template-test/rebuild.ts to customize your template
2. Run your template with: si template run /home/adam/src/template-test/rebuild.tsThe resulting template file will contain:
import { TemplateContext } from "jsr:@systeminit/si";
export default async function (ctx: TemplateContext) {
ctx.search(["schema:*"]);
}With no changes, this template will make an exact replica of the workspace it is run against.
INFO
This is because templates use the baseline as the source of components to create. Since schema:* matches all components, this template will create a copy of every component in the workspace in a new change set. This assumes the baseline components themselves were not created by a previous execution of this template with the same invocation key; if they were, then it would do nothing at all (because the components are already identical, and templates are idempotent).
Running templates
To run a template with the CLI:
$ si template run rebuild.ts --key recreate
✨ info si Loading Template: "file:///home/adam/src/template-test/rebuild.ts"
✨ info si Building baseline with search strings: [ 'schema:*' ]
✨ info si Found 17 unique components from search
✨ info si Loaded baseline component "AWS::EC2::SubnetRouteTableAssociation" "demo-subnet-1-route-association" (1/17)
✨ info si Loaded baseline component "AWS::EC2::KeyPair" "demo-keypair" (2/17)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-1" (3/17)
✨ info si Loaded baseline component "AWS::EC2::VPC" "demo-vpc" (4/17)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-2" (5/17)
✨ info si Loaded baseline component "AWS::EC2::Route" "demo-internet-route" (6/17)
✨ info si Loaded baseline component "AWS::EC2::RouteTable" "demo-route-table" (7/17)
✨ info si Loaded baseline component "AWS::EC2::InternetGateway" "demo-igw" (8/17)
✨ info si Loaded baseline component "Region" "demo virginia" (9/17)
✨ info si Loaded baseline component "AWS::ElasticLoadBalancingV2::TargetGroup" "demo-target-group-ip" (10/17)
✨ info si Loaded baseline component "AWS::EC2::SecurityGroup" "demo-web-sg" (11/17)
✨ info si Loaded baseline component "AWS::EC2::Instance" "demo-web-server-1" (12/17)
✨ info si Loaded baseline component "AWS::EC2::Instance" "demo-web-server-2" (13/17)
✨ info si Loaded baseline component "AWS::ElasticLoadBalancingV2::LoadBalancer" "demo-load-balancer" (14/17)
✨ info si Loaded baseline component "AWS::ElasticLoadBalancingV2::Listener" "demo-alb-listener" (15/17)
✨ info si Loaded baseline component "AWS::EC2::VPCGatewayAttachment" "demo-igw-attachment" (16/17)
✨ info si Loaded baseline component "AWS::EC2::SubnetRouteTableAssociation" "demo-subnet-2-route-association" (17/17)
✨ info si Built baseline with 17 components from search
✨ info si Initializing working set: 17 components
✨ info si Getting or creating change set: "rebuild-recreate"
✨ info si Found 0 existing components
✨ info si Computing delta
✨ info si Pending changes: 17 creates, 0 updates, 0 deletes
✨ info si Creating "Region" "demo virginia" (1/17)
✨ info si Creating "AWS::EC2::KeyPair" "demo-keypair" (2/17)
✨ info si Creating "AWS::EC2::VPC" "demo-vpc" (3/17)
✨ info si Creating "AWS::EC2::InternetGateway" "demo-igw" (4/17)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-1" (5/17)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-2" (6/17)
✨ info si Creating "AWS::EC2::RouteTable" "demo-route-table" (7/17)
✨ info si Creating "AWS::EC2::SecurityGroup" "demo-web-sg" (8/17)
✨ info si Creating "AWS::EC2::VPCGatewayAttachment" "demo-igw-attachment" (9/17)
✨ info si Creating "AWS::EC2::SubnetRouteTableAssociation" "demo-subnet-1-route-association" (10/17)
✨ info si Creating "AWS::EC2::Route" "demo-internet-route" (11/17)
✨ info si Creating "AWS::EC2::SubnetRouteTableAssociation" "demo-subnet-2-route-association" (12/17)
✨ info si Creating "AWS::EC2::Instance" "demo-web-server-1" (13/17)
✨ info si Creating "AWS::EC2::Instance" "demo-web-server-2" (14/17)
✨ info si Creating "AWS::ElasticLoadBalancingV2::LoadBalancer" "demo-load-balancer" (15/17)
✨ info si Creating "AWS::ElasticLoadBalancingV2::TargetGroup" "demo-target-group-ip" (16/17)
✨ info si Creating "AWS::ElasticLoadBalancingV2::Listener" "demo-alb-listener" (17/17)
✨ info si Execution complete: 17 succeeded, 0 failedRunning this template will create a 'rebuild-recreate' change set with 17 new components in it, each an exact copy of the existing components, with any subscriptions between them updated to point to the new components.
INFO
If a component has a subscription to another component that also has an entry in the working set, its subscription will be updated to point to its peer in the working set. If it has a subscription to a component that is not in the working set, it will remain subscribed to the existing subscription. For example, if all the components subscribe to an AWS Credential, and the AWS Credential is not in the working set, they will remain subscribed to the original AWS Credential.
Invocation Key
Each template run requires an invocation key to be passed with the --key parameter. This value is used to correlate a particular invocation of the template to the components it creates or updates. This is how we enable idempotency over subsequent invocations of the template. By allowing you to specify the key, we also enable you to run the same template more than once against the same workspace.
Baseline Components
Baseline components can be defined in two ways: via dynamic searching or as static YAML.
Dynamic Search
Specify the baseline components for your template with one or more search strings. The results of all searches (deduplicated) will become the working set.
import { TemplateContext } from "jsr:@systeminit/si";
export default async function (ctx: TemplateContext) {
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
}TIP
The search syntax in System Initiative can express very complex boolean logic - you most likely only need one search query for almost all use cases. Supporting multiple search strings is there for convenience in composing complex templates by breaking up the search logic as needed.
YAML Cache
In order to make templates re-usable even without access to the workspaces the baseline components exist in, you can create a "baseline cache" file. This stores the results of your baseline search (and some information about the schemas they use) as YAML.
Given a template like the one we defined above for Dynamic Search, we would cache all the VPC and Subnet components in our workspace.
To create a baseline YAML cache with the CLI:
$ si template run ./vpc-and-subnet.ts --key cache --cache-baseline vpc-and-subnet-baseline.yaml --cache-baseline-only
✨ info si Loading Template: "file:///home/adam/src/template-test/vpc-and-subnet.ts"
✨ info si Building baseline with search strings: [ 'schema:"AWS::EC2::VPC"', 'schema:"AWS::EC2::Subnet"' ]
✨ info si Found 3 unique components from search
✨ info si Loaded baseline component "AWS::EC2::VPC" "demo-vpc" (1/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-1" (2/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-2" (3/3)
✨ info si Built baseline with 3 components from search
✨ info si Caching baseline to "vpc-and-subnet-baseline.yaml"
✨ info si Cached 3 components and 2 schemas to "vpc-and-subnet-baseline.yaml"
✨ info si Baseline cache written successfully. Exiting.The resulting YAML file contains all the information about your baseline components.
TIP
Make a cache of your baseline frequently, and store it alongside the template file in source control. This will allow you to recreate the infrastructure in your workspace exactly as it was at a given point in time!
To utilize this cache when running a template, specify the --baseline option to template run:
$ si template run ./vpc-and-subnet.ts --key rebuild --baseline vpc-and-subnet-baseline.yaml
✨ info si Loading Template: "file:///home/adam/src/template-test/vpc-and-subnet.ts"
✨ info si Loading baseline data from "vpc-and-subnet-baseline.yaml"
✨ info si Initializing working set: 3 components
✨ info si Getting or creating change set: "vpc-and-subnet-rebuild"
✨ info si Found 0 existing components
✨ info si Computing delta
✨ info si Pending changes: 3 creates, 0 updates, 0 deletes
✨ info si Creating "AWS::EC2::VPC" "demo-vpc" (1/3)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-2" (2/3)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-1" (3/3)
✨ info si Execution complete: 3 succeeded, 0 failedInput Variables
Templates can specify input data that can be used as variables when transforming the working set.
TIP
If you are familiar with Terraform or Open Tofu, template inputs are the equivalent of input variables.
Input Schema
You specify the input schema using the zod schema language.
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const inputSchema = z.object({
environment: z.string().default("dev"),
region: z.enum([ "us-east-1", "us-east-2" ]),
vpcAttributes: z.object({
vpcCount: z.number(),
}),
});
type Inputs = z.infer<typeof inputSchema>;
ctx.inputs(inputSchema)
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
}This code defines an input schema that is a object with 3 keys: environment, region, and vpcAttributes. The environment value is a string that defaults to "dev", the region is either us-east-1 or us-east-2, and vpcAttributes is itself an object with a single vpcCount key, whose value is a number.
TIP
Wondering what the type Inputs = z.infer<typeof inputSchema> line is all about? It's a little bit of TypeScript magic that creates a TypeScript type from the schema you defined, so that we can use it later to have your editor provide intellisense on your input data!
There is a helper export you can use if you want to specify the details of a subscription as an input, to later be consumed by the ensure attribute helpers named SubscriptionInput. To use it:
import { SubscriptionInput, TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const inputSchema = z.object({
environment: z.string().default("dev"),
region: SubscriptionInput,
vpcAttributes: z.object({
vpcCount: z.number(),
}),
});
type Inputs = z.infer<typeof inputSchema>;
ctx.inputs(inputSchema)
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
}YAML Input Files
To specify the inputs to the template, write a yaml file that matches your schema. The best practice is to have the file end with '-input.yaml', such as 'vpc-and-subnet-input.yaml'.
environment: prod
region: us-east-1
vpcAttributes:
vpcCount: 2environment: prod
region:
kind: "search"
query: 'name:"standard region"'
path: "/domain/region"
vpcAttributes:
vpcCount: 2To specify your input values when running the template, add the --input option:
$ si template run ./vpc-and-subnet.ts --key rebuild --input ./vpc-and-subnet-inputs.yaml
✨ info si Loading Template: "file:///home/adam/src/template-test/vpc-and-subnet.ts"
✨ info si Loading input data from: "./vpc-and-subnet-inputs.yaml"
✨ info si Building baseline with search strings: [ 'schema:"AWS::EC2::VPC"', 'schema:"AWS::EC2::Subnet"' ]
✨ info si Found 3 unique components from search
✨ info si Loaded baseline component "AWS::EC2::VPC" "demo-vpc" (1/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-1" (2/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-2" (3/3)
✨ info si Built baseline with 3 components from search
✨ info si Initializing working set: 3 components
✨ info si Getting or creating change set: "vpc-and-subnet-rebuild"
✨ info si Found 0 existing components
✨ info si Computing delta
✨ info si Pending changes: 3 creates, 0 updates, 0 deletes
✨ info si Creating "AWS::EC2::VPC" "demo-vpc" (1/3)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-1" (2/3)
✨ info si Creating "AWS::EC2::Subnet" "demo-subnet-2" (3/3)
✨ info si Execution complete: 3 succeeded, 0 failedReferencing Inputs
You can reference the input data as the second argument to the transformation function, or through the second argument to the rename function. Both are demonstrated in the next section.
TIP
You specify the input schema in your template file, but the inputs themselves are not evaluated until your template is executed. That is why the input data is not available outside the transformation function or replacement patterns.
Renaming Components
The most common change a template makes is to rename components to make them distinct. You can do this by specifying one or more name patterns, which will be applied to the working set in order.
export default async function (ctx: TemplateContext) {
const inputSchema = z.object({
environment: z.string().default("prod"),
region: z.enum(["us-east-1", "us-east-2"]),
vpcAttributes: z.object({
vpcCount: z.number(),
}),
});
type Inputs = z.infer<typeof inputSchema>;
ctx.inputs(inputSchema);
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
ctx.namePattern([
{ pattern: /demo-(.+)/, replacement: "prod-$1" },
]);
}A name pattern consists of two arguments: the initial pattern and the replacement pattern.
The initial pattern is a JavaScript regular expression. The replacement is a JavaScript string replacement pattern.
In the above example, it will rename all components from "demo-whatever" to "prod-whatever".
Each name pattern you provide will be evalauted against the entire working set, in order. This means you can have multiple patterns that rename the came component.
Using Input Variables in Replacement Patterns
To use input variables in replacement patterns, use EJS string templating syntax in the replacement pattern. Assuming a template with the following input schema and name pattern:
export default async function (ctx: TemplateContext) {
const inputSchema = z.object({
environment: z.string().default("prod"),
region: z.enum(["us-east-1", "us-east-2"]),
vpcAttributes: z.object({
vpcCount: z.number(),
}),
});
type Inputs = z.infer<typeof inputSchema>;
ctx.inputs(inputSchema);
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
ctx.namePattern([
{ pattern: /demo-(.+)/, replacement: "<%= inputs.environment %>-$1" },
]);
}TIP
While you can write complex expressions with EJS, typically all you will need is the syntax above - which inserts a variable into the string.
With the following input yaml:
environment: prod
region: us-east-1
vpcAttributes:
vpcCount: 2Results in the following output:
$ si template run ./vpc-and-subnet.ts --key rebuild --input ./vpc-and-subnet-inputs.yaml 6s
✨ info si Loading Template: "file:///home/adam/src/template-test/vpc-and-subnet.ts"
✨ info si Loading input data from: "./vpc-and-subnet-inputs.yaml"
✨ info si Building baseline with search strings: [ 'schema:"AWS::EC2::VPC"', 'schema:"AWS::EC2::Subnet"' ]
✨ info si Found 3 unique components from search
✨ info si Loaded baseline component "AWS::EC2::VPC" "demo-vpc" (1/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-1" (2/3)
✨ info si Loaded baseline component "AWS::EC2::Subnet" "demo-subnet-2" (3/3)
✨ info si Built baseline with 3 components from search
✨ info si Initializing working set: 3 components
✨ info si Applying pattern 1/1: "demo-(.+)" -> "prod-$1"
✨ info si Getting or creating change set: "vpc-and-subnet-rebuild"
✨ info si Found 0 existing components
✨ info si Computing delta
✨ info si Pending changes: 3 creates, 0 updates, 0 deletes
✨ info si Creating "AWS::EC2::VPC" "prod-vpc" (1/3)
✨ info si Creating "AWS::EC2::Subnet" "prod-subnet-1" (2/3)
✨ info si Creating "AWS::EC2::Subnet" "prod-subnet-2" (3/3)
✨ info si Execution complete: 3 succeeded, 0 failedThe components have all been renamed according to the 'environment' input variable.
Transforming the Working Set
Transforming the working set allows you to manipulate the attributes of the components, create new components, make clones of existing components, or remove components from the set. This is done through adding a transformation function:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const inputSchema = z.object({
environment: z.string().default("prod"),
region: z.enum(["us-east-1", "us-east-2"]),
vpcAttributes: z.object({
vpcCount: z.number(),
}),
});
type Inputs = z.infer<typeof inputSchema>;
ctx.inputs(inputSchema);
ctx.search([
'schema:"AWS::EC2::VPC"',
'schema:"AWS::EC2::Subnet"',
]);
ctx.namePattern([
{ pattern: /demo-(.+)/, replacement: "<%= inputs.environment %>-$1" },
]);
ctx.transform(async (workingSet, inputs) => {
inputs = inputs as Inputs;
return workingSet;
});
}The function takes an async callback function, whose arguments are the workingSet and your inputs. The function must return a workingSet (if you do not, your transformation function may not work as expected.)
INFO
The inputs = inputs as Inputs line is bit of TypeScript magic that enables intellisense for your input variables.
Finding components in the working set
To find components in the working set, you can use the find function:
const subnet = workingSet.find((component) => component.name === "prod-subnet-1");
if (subnet) {
// transform it
}You can also iterate over the components in the working set, and use conditional logic:
for (const component of workingSet) {
if (component.name === "prod-subnet-1") {
// transform it
}
}Changing Attributes
ensureAttribute
Once you have found the component you want to set the attributes of, you can set its attributes with the ensureAttribute helper. It takes four arguments:
- The component to update
- The attribute path to set
- The value to set the attribute to, or the subscription to create
- An optional object that modifies the behavior of the helper
const subnet = workingSet.find((component) => component.name === "prod-subnet-1");
if (subnet) {
await ctx.ensureAttribute(
subnet,
"/domain/EnableDns64",
true,
)
}You can set the attribute to any value that fits the schema - boolean, string, number, objects, maps, arrays, etc.
TIP
The attribute path syntax will automatically create nested objects and map entries as needed. You can either set the whole object as a value, or set individual properties.
Setting Subscriptions
You can set a subscription to another components attribute through searching for the component to subscribe to or by referencing it directly by name or id.
await ctx.ensureAttribute(
component,
"/domain/extra/Region",
{
kind: "search",
query: 'name:"demo region"',
path: "/domain/region",
},
);await ctx.ensureAttribute(
component,
"/domain/extra/Region",
{
kind: "$source",
component: "demo region",
path: "/domain/region",
},
);await ctx.ensureAttribute(
component,
"/domain/extra/Region",
{
kind: "$source",
component: "01K2YVY4WE8KBM01H05R74RKX8", // Component ID
path: "/domain/region",
},
);The public API supports a low level API for setting subscriptions using $source sytnax, which can also be used.
await ctx.ensureAttribute(
component,
"/domain/extra/Region",
{
$source: {
path: "/domain/region",
}
}
);TIP
The entire ensure* family of helper functions support the same subscription syntax for values.
skipIfMissing
The ensureAttribute helper will only set the value on the component if needed (it is idempotent), and will throw an error if the attribute you are trying to set does not exist in the components schema. For convenience, you can disable this check with an optional skipIfMissing argument:
for (const component of workingSet) {
ctx.ensureAttribute(
component,
"/domain/EnableDns64",
true,
{ skipIfMissing: true }
);
}In this example, we iterate over every component in the working set, and set the "/domain/EnableDns64" attribute to true only if that attribute exists on the components schema, otherwise we do nothing.
TIP
All the ensure* family of helpers support the skipIfMissing option.
ensureAttributeMissing
If you want to ensure an attribute is not set, use ensureAttributeMissing:
await ctx.ensureAttributeMissing(
component,
"/domain/EnableDns64",
)It takes 3 arguments:
- The component to update
- The attribute path to ensure is missing
- The optional object to modify the behavior of the helper
Ensuring an attribute is missing will delete the values entirely from the components attributes. It is idempotent - if the attribute is already missing, nothing is done.
ensureArrayAttribute
If you need to make sure an array has an entry with a specific value, use ensureArrayAttribute. It supports finding and replacing specific values, adding new values if none are present, and supports the subscription syntax for values.
Scalar Values
To ensure an array contains a scalar value, such as a string, number, or boolean:
await ctx.ensureArrayAttribute(
component,
"/domain/Ports",
(e) => e.value === 8080,
8080
);It takes five arguments:
- The component to update
- The attribute path to the array
- A match function that determines if a given entry in the array should be changed
- The value to set in all matching positions
- The optional object to modify the behavior of the helper
In the above example, if there is an entry with the value 8080, it will not be changed. If there is no value that matches 8080, then a new entry in the array will be added with 8080.
To update a specific value to a new value, change the match function:
await ctx.ensureArrayAttribute(
component,
"/domain/Ports",
(e) => e.value === 80,
8080
);In this case, it would change any entry whose value is 80 to 8080.
Object Values
Arrays of objects are a common pattern in many APIs. Given a structure like:
{
"/domain/Tags": [
{ Key: "Name", Value: "demo-subnet-1" },
{ Key: "Cost Center", Value: "operations" },
],
}We can update the Name entry with the following code:
await c.ensureArrayAttribute(
component,
"/domain/Tags",
(e) => e.subpath === "Key" && e.value === "Name",
{ Key: "Name", Value: "demo-subnet-awesome" }
);This would change the 'Value' of the object whose 'Key' is 'Name' to 'demo-subnet-awesome', rather than 'demo-subnet-1'. If there is no entry with 'Key: Name', then one will be created.
Partial object merging
If you provide a partial object for the value, it will be merged with any existing elements. This allows you to update an individual element of an object without changing the other properties:
await c.ensureArrayAttribute(
component,
"/domain/Tags",
(e) => e.subpath === "Key" && e.value === "Name",
{ Value: "demo-subnet-awesome" }
);The match function
The match function is gets each element of the array (by convention the variable 'e'), and if it returns true, will update that element.
The element object has the following properties:
- subpath: Each path beneath the entry; for example, "Key" or "Cost Center"
- value: The current value for this subpath
- fullPath: The complete attribute path (/domain/Tags/0/Key)
- index: The current array index (0 or 1, for example)
ensureArrayAttributeMissing
The ensureArrayAttributeMissing helper deletes array elements (or specific properties) that match your criteria, and then reindexes the array (to avoid sparse arrays).
Delete entire array elements
Given a structure like this:
{
"/domain/Ports": [
"80",
"8080",
"443",
],
}We can remove port 8080 as follows:
await c.ensureArrayAttributeMissing(
component,
"/domain/Ports",
(e) => e.value === "8080"
);Which will result in an array with [ "80", "443" ].
It takes five arguments:
- The component to update
- The attribute path to the array
- A match function that determines if a given entry in the array should be removed
- An optional array of keys that should be deleted
- The optional object to modify the behavior of the helper
Deleting specific properties of an array entry
Occasionally, you will want to delete only particular properties within an array entry. In this case, you can specify those as the fourth argument.
Given a structure like this:
{
"/domain/Tags": [
{ Key: "Name", useInTemplate: true, Value: "Crow" },
{ Key: "Cost Center", useInTemplate: true, Value: "operations" },
],
}await c.ensureArrayAttributeMissing(
component,
"/domain/Tags",
(e) => e.subpath === "Key" && e.value === "Name",
["useInTemplate"]
);Will remove only useInTemplate: true from the matching entry object.
Creating new components
You can create new components with the newComponent helper.
c.transform(async (workingSet, inputs) => {
const newServer = await c.newComponent(
"AWS::EC2::Subnet",
"public-subnet-1",
{
"/domain/CidrBlock": "10.0.1.0/24",
"/domain/MapPublicIpOnLaunch": true,
"/domain/Tags/0/Key": "Name",
"/domain/Tags/0/Value": "public-subnet-1",
}
);
workingSet.push(newServer);
return workingSet;
});There are 3 arguments:
- The schema of the component you want to create
- The name of the component
- The attributes you want to set, expressed as attribute paths.
WARNING
You must always push the variable containing your new component on to the workingSet if you want it to be created!
Copying existing components
A common use case in templating is wanting to create a variable number of components based on an example. Do that with the copyComponent helper:
ctx.transform(async (workingSet, inputs) => {
const subnetExample = workingSet.find(c => c.name === "subnet-example");
for (let i = 1; i <= 5; i++) {
const newSubnet = ctx.copyComponent(subnetExample, `subnet-${i}`);
workingSet.push(newSubnet);
}
return workingSet;
});There are 2 arguments:
- The component to copy
- The name of the new component
A typical pattern would then be to use the ensure helper functions to configure your newly cloned component appropriately.
WARNING
You must always push the variable containing your new component on to the workingSet if you want it to be created!
Specifying the Template Name
By default, every template will be named after the file it is stored in, minus the .ts extension. To set the name directly:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
ctx.name("new-name");
}To retrieve the name from a template file called new-vpc.ts:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const currentName = ctx.name(); // returns "new-vpc" for "new-vpc.ts"
}Specifying the Change Set
By default, the template will create a change set that is the combination of the templates name and its invocation key (the --key argument).
For example, if the template file is named 'new-vpc.ts' and --key rebuild is passed to template run, the change set would be new-vpc-rebuild.
To set the change set name directly:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
ctx.changeSet("create-new-stuff");
}To retrieve the change set name:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const changeSetName = ctx.changeSet(); // returns "new-vpc-rebuild"
}Getting the Invocation Key
You can get the invocation key with:
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";
export default async function (ctx: TemplateContext) {
const invocationKey = ctx.invocationKey(); // returns "rebuild"
}Using NPM or JSR Libraries
Templates are invoked with the Deno TypeScript runtime. They support dynamically importing any module from the NPM or JSR package registries.
import { TemplateContext } from "jsr:@systeminit/si";
import { z } from "npm:zod@4";The first line imports the @systeminit/si library, which defines our templating syntax, from JSR.
The second line imports the zod schema validation library from NPM. It also ensures that we get the most recent release of version 4 of the library.
You can see the full syntax and range of options in the Deno documentation.
TIP
This means you can use the entirety of the JavaScript ecosystem to create truly dynamic template solutions. Call out to third party APIs, do complex network address math, or integrate with your internal systems.
LSP Support
Follow the instructions to set up your environment for Deno development to enable the Deno LSP server for your preferred editor. This will provide inline documentation for every template helper, and intellisense for the typescript types as you develop.