Counters and Indices
Controlling Iteration, State, and Numbering in Your Graph
Because rescile processes configurations declaratively over loops of resources, knowing how to track iteration state is critical. Whether you are generating sequential hostnames, splitting CIDR blocks, or incrementing IDs, you need the right counter for the job.
rescile provides several distinct counters and indices. Choosing the right one depends entirely on scope—whether you are counting across a rule block, within an array property, or globally across the entire importer run.
Quick Comparison
| Counter | Type | Scope & Reset Behavior | Primary Use Case |
|---|---|---|---|
origin_resource_counter |
Variable | Rule Block: Resets to 0 at the start of every [[create_resource]] rule. |
Numbering top-level resources sequentially as they are processed. |
property.index |
Variable | Local Array: Resets to 0 for every array processed via create_from. |
Indexing derived resources generated from a list property. |
counter(key=...) |
Function | Global: Persists across the entire importer run. Resets only on a fresh run. | Grouping and numbering resources independently based on a shared relationship or attribute. |
loop.index0 |
Variable | Inline Template: Standard Tera loop tracking. | Used strictly inside {% for ... %} template blocks. |
1. The Rule-Scoped Variable: origin_resource_counter
The origin_resource_counter is an implicitly available integer variable injected into the context of every [[create_resource]] block.
It starts at 0 and increments by 1 for every origin_resource evaluated by that specific rule block.
When to use it: Use this when you are iterating over a flat list of global resources and need a deterministic, sequential ID or network block assigned to each one.
Example: Global Subnet Allocation
Assume you have 10 department resources and you want to give each one a unique /24 subnet sequentially from a master /16 block.
# data/models/department.toml
origin_resource = "department"
[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
name = "subnet_for_{{ origin_resource.name }}"
[create_resource.properties]
# Iteration 1: counter is 0 -> 10.0.0.0/24
# Iteration 2: counter is 1 -> 10.0.1.0/24
# Iteration 3: counter is 2 -> 10.0.2.0/24
subnet_cidr = "{{ '10.0.0.0/16' | cidr_nth_subnet(prefix=24, nth=origin_resource_counter) }}"
2. The Array-Scoped Variable: property.index
When you use the create_from = { property = "..." } pattern to generate multiple resources from an array property on a single node, you are entering a nested loop.
Inside this context, origin_resource_counter continues to track the parent node’s iteration, while property.index tracks the current item’s zero-based position within the array. The value itself is available as property.value (or simply {{ value }}).
When to use it: Use this when splitting resources or data natively tied to the size and order of a list property on the origin resource.
Example: Splitting a VPC into Fault Domains
If a network resource has an array property fault_domain = ["az-a", "az-b", "az-c"], we can use property.index to pick the correctly sized subnet for each AZ.
# data/models/subnet.toml
origin_resource = "network"
# Calculate the exact number of fault domains and split the parent CIDR equally
subnet_list = { "function!" = '''
{% set count = origin_resource.fault_domain | length %}
{% set ranges = origin_resource.cidr | default(value="10.0.0.0/8") | cidr_split_n(n=count) %}
{{- ranges | json_encode | safe -}}
''' }
[[create_resource]]
create_from = { property = "fault_domain", as = "subnet" }
relation_type = "CONNECTED_TO"
name = "{{ origin_resource.name }}_subnet_{{ property.value }}"
[create_resource.properties]
# Grab the subnet CIDR from our pre-calculated list using the array index
cidr = "{{ subnet_list | nth(n=property.index) }}"
net_count = "{{ property.index }}"
fault_domain_name = "{{ property.value }}"
3. The Global Stateful Function: counter(key=...)
Sometimes you need to number resources sequentially, but they belong to different logical groups (like numbering servers 1, 2, 3 per environment, or subnets 0, 1, 2 per VPC).
The origin_resource_counter cannot do this because it increments universally for the rule block. To solve this, rescile provides the custom counter(key=...) Tera function.
This function maintains a global state dictionary during the importer run. Every time you call it with a unique key, it returns 0. The next time you call it with that same key, it returns 1, and so on.
When to use it:
Use this for localized, grouped incrementing. The key can be a string, a number, a boolean, an object with a name property, or an array of those types (which rescile automatically sorts and joins into a deterministic key).
Example: Independent Regional Subnet Allocation
Assume you have departments scattered across different global regions. You want to allocate a /24 subnet for each department, but the numbering needs to restart from 0 for each region’s master block.
# data/models/department_subnets.toml
origin_resource = "department"
[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
name = "subnet_for_{{ origin_resource.name }}"
[create_resource.properties]
# The 'key' is the region relation array attached to the department.
# If Dept A and Dept B are in 'eu-central-1', they get counter 0 and 1.
# If Dept C is in 'us-east-1', it starts fresh with counter 0.
subnet_cidr = "{{ origin_resource.region[0].cidr | cidr_nth_subnet(prefix=24, nth=counter(key=origin_resource.region)) }}"
# Generate sequential hostnames per environment: srv-prod-0, srv-prod-1, srv-dev-0...
hostname = "srv-{{ origin_resource.environment }}-{{ counter(key=origin_resource.environment) }}"
4. The Inline Template Variable: loop.index / loop.index0
This is standard functionality provided by the Tera templating engine. It is completely isolated to string generation inside a {% for ... %} block and has no awareness of the graph structure.
loop.index0: Zero-based index (0, 1, 2...)loop.index: One-based index (1, 2, 3...)
When to use it: Use this strictly when formatting strings or lists inside a property assignment.
Example: Generating a formatted string of tags
[create_resource.properties]
# Create a comma-separated list, knowing exactly when to omit the trailing comma
tag_list = "{% for tag in origin_resource.tags %}{{ loop.index }}: {{ tag | upper }}{% if not loop.last %}, {% endif %}{% endfor %}"
Summary Decision Tree
- Am I iterating an inline Tera
{% for %}loop to output a single string? → Useloop.index0 - Am I generating separate nodes from an array property via
create_from? → Useproperty.index - Am I generating separate nodes from the main
origin_resourceloop and need a simple, universal sequence ID? → Useorigin_resource_counter - Am I generating separate nodes from the main loop, but need the sequence ID grouped or partitioned by a specific context (like a VPC, Region, or Environment)?
→ Use
counter(key="...")