Generators
A Generator is a generic plugin that queries data and creates new nodes and relationships based on the result.
- Within your schema you could create an abstract service object that through a Generator creates other nodes.
- A Generator that creates a service usually has to give it addressing and identifiers. Request those from a pool through the SDK — see Resource Manager for the pool types and how allocation behaves.
- Want to read how Generators can be used to create a service catalog? See our blog post on How to Turn Your Source of Truth into a Service Factory.
High level design​
Generators are defined as a Generator definition within an .infrahub.yml file. A Generator definition consists of a number of related objects.
- Group of targets - Objects that the Generator will act upon
- Generator class - Python code that defines the generation logic
- GraphQL Query - Data collection specification
Running a Generator definition creates or updates the objects that the Generator saves. Through the SDK tracking feature, each run also deletes the objects that an earlier run saved and the current run did not save again. See Objects a run deletes.
The targets point to a group that will consist of objects that are impacted by the Generator. The members of this group can be any type of object within your schema, service objects, devices, contracts or anything you want the Generator to act upon. Generator instances record the run for each target object, and Generator groups (CoreGeneratorGroup or CoreGeneratorAwareGroup) track the objects that a run created.
The GraphQL query defines the data that will be collected when running the Generator. Any object identified in this step is added as a member to a GraphQL query group (CoreGraphQLQueryGroup). The membership in these groups are then used to determine which Generators need to be executed as part of a proposed change during the pipeline run.
The Generator itself is a Python class that is based on the InfrahubGenerator class from the SDK. Just like Transformations and checks, the Generators are user defined.
Generators can be executed in several ways, depending on your workflow and where you are in the lifecycle (local development vs. in Infrahub):
-
During development with infrahubctl
Use the
infrahubctl generatorcommand to iterate locally while building and testing your Generator. -
Manually from the UI
From the Infrahub UI, open the Generator Definition detail page (Actions > Generator Definitions) and click Run to trigger the Generator on demand.
To run a Generator for one target only, open that target's detail page, open the Actions menu, and select the Generator in the Generators section. The section lists the Generator definitions whose target group contains objects of the same kind. An entry is disabled when the object is not a member of that target group.
-
Automatically via Proposed Changes
When you open a Proposed Change that affects the Generator's targets, the Generator runs as part of Infrahub's CI checks. Review the results in the Checks and Data tabs of the Proposed Change. This behavior can also be disabled per Generator in the repository configuration file.
-
Automatically via Events and Actions
You can configure Infrahub Event rules and Actions to trigger Generators automatically based on changes in your data. This enables fully automated execution aligned with your workflows.
Per-target execution model​
Infrahub does not run a Generator once for the entire target group. Instead, it creates one independent run per member of the target group.
When you trigger a Generator definition, Infrahub:
- Fetches the target group and enumerates its members.
- For each member, extracts scoped variables from the target object using the
parametersmapping. - Creates an independent Generator run for that member, passing the scoped variables to the GraphQL query.
A Generator definition targeting a group with 10 members produces 10 separate runs. Each run sees only the data relevant to its specific target object.
Generator Definition
│
â–¼
Target Group
├── Member A → Run A (variables from A)
├── Member B → Run B (variables from B)
└── Member C → Run C (variables from C)
Each run is fully independent — it has its own query variables, its own query results, and its own Generator instance. Runs do not share state.
Query parameter mapping​
The parameters field in .infrahub.yml controls how Infrahub extracts variables from each target object and passes them to the GraphQL query. This is the mechanism that scopes each run to its target.
How it works​
Given this Generator definition:
generator_definitions:
- name: widget_generator
file_path: "generators/widget_generator.py"
targets: widgets
query: widget_query
class_name: WidgetGenerator
parameters:
name: "name__value"
And this GraphQL query:
query Widgets($name: String!) {
TestWidget(name__value: $name) {
edges {
node {
name { value }
count { value }
}
}
}
}
For each member of the widgets group, Infrahub:
- Reads the parameter mapping:
name→"name__value" - Extracts the value from the target object using the defined path
- Passes it as a query variable
For example:
| Target object | Extraction path | Extracted value | Query variable |
|---|---|---|---|
widget1 | widget1.name.value | "widget1" | $name = "widget1" |
widget2 | widget2.name.value | "widget2" | $name = "widget2" |
Each run's GraphQL query only returns data for its specific target, keeping runs independent.
Double-underscore notation​
The double-underscore (__) in parameter values traverses the object hierarchy:
name__value: attributename, propertyvaluelocation__name__value: relationshiplocation(cardinality-one), then attributename, propertyvalue
The first segment is checked against the object's schema. If it matches an attribute, the remaining segments traverse the attribute's properties. If it matches a cardinality-one relationship, Infrahub fetches the related node and continues the traversal recursively.
Only cardinality-one relationships are supported in parameter paths. Cardinality-many relationships cannot be traversed this way.
Parallel execution​
Because each run is independent — scoped to one target object with no shared state — Infrahub dispatches all runs for a Generator definition concurrently.
This means:
- All members of a target group are processed in parallel, not sequentially.
- Performance scales with available workers, not with target count. A group with 100 members doesn't take 100x longer than a group with 1 member.
- Different Generator definitions can also run concurrently when triggered independently.
What this means for Generator design​
Because runs are concurrent:
- Your Generator code should not depend on side effects from other runs of the same Generator.
- Each run should be self-contained — it reads its scoped data, creates its objects, and finishes.
- If you need ordering (layer A must complete before layer B starts), use separate Generator definitions with a trigger mechanism rather than relying on execution order within a single definition. See modular Generators for this pattern.
Generator instances​
Each per-target run creates or updates a CoreGeneratorInstance — a tracking object that links three things together:
- The Generator definition that was run
- The target object (the specific group member)
- The status of that run (
pending,ready, orerror)
Generator instances enable:
- Per-target status tracking: you can see which targets succeeded and which failed, without needing to inspect logs.
- Selective re-runs: you can re-run the Generator for a single target object without affecting others. Only the instance for that target gets updated.
- Instance after its target is deleted: when you delete a target object, its Generator instance remains, and the proposed change Generator checks skip it. See What Infrahub deletes when a target or definition is removed.
You can view Generator instances in the Infrahub UI under the Generator Definition detail page.
Designing groups for parallelism​
Since group structure determines execution structure, how you organize your target groups directly affects parallelism and operational flexibility.
The principle​
Group at the level where you want independent execution. If racks should generate independently, make racks the target — not pods. If entire sites should generate as a unit, make sites the target.
More members in the target group means more parallel runs and better utilization of available workers.
Example: modular parallelism​
In a modular Generator setup (see modular Generators), parallelism increases at each layer:
| Layer | Target group | Members | Parallel runs |
|---|---|---|---|
| Fabric | dc_fabrics | 1 fabric | 1 |
| Pod | dc_pods | 4-8 pods | 4-8 |
| Rack | dc_racks | 32+ racks | 32+ |
The fabric Generator runs once (1 target). It creates pod objects. The pod Generator runs 4-8 times concurrently. Each pod Generator creates rack objects. The rack Generator runs 32+ times concurrently. Total parallelism is multiplicative across layers.
Guideline: prefer more, smaller targets​
A single Generator targeting a group with one member that creates 100 objects runs as one sequential operation. The same work split across 10 targets with 10 objects each runs as 10 concurrent operations — significantly faster on a system with available capacity.
The relationship between member_of_groups and Generator targeting​
Objects become Generator targets by being members of the group specified in the Generator definition's targets field.
How objects become targets​
You add objects to a target group through the member_of_groups relationship, which can be set:
- In the UI: when creating or editing an object (see organizing objects with groups)
- In object files: define group membership in your YAML object definitions
- Programmatically: via the SDK or GraphQL mutations
Dynamic targeting​
Adding or removing group members changes what gets targeted on the next Generator run:
- Add an object to the group: it becomes a target and gets its own Generator run next time the definition executes
- Remove an object from the group: it is no longer targeted. When the definition's
parametersgive each target different values, each target has its own Generator group, and the objects that earlier runs created for the removed object are not deleted, because no run uses that Generator group any more. When the definition has noparameters, all targets share one Generator group, and the next run for another target deletes those objects unless it saves them again (see Objects a run deletes and known limitation #3289) - Delete an object that is a target: its Generator instance remains, and the proposed change Generator checks skip it. See What Infrahub deletes when a target or definition is removed.
Standard groups vs. Generator groups​
These group types serve different purposes and are often confused:
| Group type | Purpose | You manage it |
|---|---|---|
CoreStandardGroup | Defines which objects are targeted by a Generator. Listed in the targets field of the Generator definition. | Yes — you create it and control membership |
CoreGeneratorGroup | Tracks which objects were created by a Generator run. The SDK tracking feature uses this kind when the definition sets execute_after_merge: true (the default). The group is branch-local. | No — the SDK creates and updates it |
CoreGeneratorAwareGroup | Tracks which objects were created by a Generator run. The SDK tracking feature uses this kind when the definition sets execute_after_merge: false. The group is branch-aware. | No — the SDK creates and updates it |
The target group (CoreStandardGroup) is an input to the Generator — "run against these objects." The Generator group (CoreGeneratorGroup or CoreGeneratorAwareGroup) is an output — "these objects were created by this Generator."
The SDK names each Generator group <generator definition name>-<parameters hash>, where the hash is the MD5 hash of the query parameters of the run. Runs with different parameters, such as one run per target device, therefore write to different Generator groups. When a run has no parameters, the name is the definition name alone.
Objects a run deletes​
A Generator run keeps the objects it saves and deletes the objects it no longer saves. When generate() finishes, the SDK compares the members of the run's Generator group, which are the objects that earlier runs with the same parameters saved, with the objects the current run saved. It deletes every earlier member that the current run did not save, then writes the group with the objects the current run saved. This happens on every run, whether Infrahub starts it or you start it with infrahubctl generator. It also happens on a run that saves no objects: that run deletes every object that earlier runs with the same parameters saved.
When the definition has no parameters, the runs for all its targets use the same Generator group, as described in Standard groups vs. Generator groups. A run for one target then deletes the objects that a run for another target saved and the current run did not save.
Before Infrahub 1.12, a run that saved no objects deleted nothing, and the cleanup stopped at the first object that the server refused to delete. Before you upgrade, check for Generators that skip saving objects when their inputs have not changed: the next run deletes those objects.
Save every object you want to keep, on every run​
A run deletes an object that an earlier run saved when the current run skips the save, for example because the object's values have not changed. Save every object that the Generator creates on every run. Use allow_upsert=True, so that saving an existing object updates it instead of creating a duplicate (see Best practices for modular Generators).
Save objects the Generator does not own with update_group_context=False​
Each save() call in generate() adds the object to the Generator group. Reading an object with client.get() or client.filters() does not add it. When a Generator updates an object that it did not create and must not delete, such as a downstream target to which it writes only a checksum, save that object with update_group_context=False:
for pod in pods:
if pod.checksum.value != checksum:
pod.checksum.value = checksum
await pod.save(allow_upsert=True, update_group_context=False)
The SDK does not add the object to the Generator group, so the cleanup does not delete it when a later run skips the save.
An object that an earlier run already added to the Generator group stays in the group until a run does not save it into the group. That run deletes the object, whether it saves the object with update_group_context=False or skips the save: the run compares the group's members with the objects it saved into the group, and that object is not among them. Remove the object from the Generator group before the Generator runs again.
When a run fails​
generate()raises an exception. The SDK does not run the cleanup and does not update the Generator group, so the run deletes nothing.- The server refuses to delete an object, for example because an object outside the run still points to it through a mandatory relationship. The object stays in the Generator group, and the SDK continues with the other deletes. After it writes the group, the run fails with
Unable to delete <count> unused member(s) of the tracking group, followed by the ID of each object and the server's reason. Infrahub sets the Generator instance toerror, andinfrahubctl generatorprints a table of the objects and reasons. Each later run tries to delete the object again, and fails until you remove what blocks the delete. - Any other failure stops the cleanup, such as a lost connection, an expired token, or a branch that no longer exists, needs a rebase, or is being merged. The objects that the SDK did not reach stay in the Generator group, and the run fails with that error.
Deletes on a branch​
The cleanup deletes objects on the branch that the Generator runs on. When a Generator runs in a Proposed Change, it deletes objects of a branch-aware kind only on the Proposed Change's branch, and merging the branch applies those deletes to the default branch.
An object of a branch-agnostic kind exists once for all branches. A run on a branch that deletes such an object removes it from every branch, including the default branch, at the time of the run rather than when the branch is merged. If a Generator updates branch-agnostic objects that it does not own, save them with update_group_context=False.
What Infrahub deletes when a target or definition is removed​
A Generator run leaves four kinds of data in Infrahub: the Generator instance, the objects the Generator created, the Generator group that tracks those objects, and the CoreGraphQLQueryGroup that Infrahub creates for the Generator's GraphQL query. The query group is named <query name>__<parameters hash>. Removing a target or a definition deletes only some of them:
| Event | Generator instance | Objects the Generator created | Generator group | GraphQL query group |
|---|---|---|---|---|
| You remove the target object from the target group | Not deleted | Not deleted (#3289) | Not deleted | Not deleted |
| You delete the target object | Not deleted | Not deleted (#3289) | Not deleted | Not deleted. The deleted object is no longer a member. |
| You remove the Generator definition | Deleted only on the branch where the definition is deleted. When that branch is merged, the instances on the default branch remain. | Not deleted | Not deleted | Not deleted on the default branch, also when you remove the query from .infrahub.yml. |
The proposed change Generator checks skip a Generator instance whose target object no longer exists, so an instance that remains does not make a check fail.
Infrahub does not delete Generator groups automatically. Infrahub deletes a GraphQL query group with its query only on the branch where the query is deleted. Query groups are branch-local, so when that branch is merged, the query groups on the default branch remain. To remove both with a retired Generator, see Remove a generator.
Generator groups appear in the list of groups in the web interface. GraphQL query groups are internal groups, which the list hides by default; remove the internal groups filter to view them. See the visibility model.
Query response modes​
The convert_query_response flag in .infrahub.yml controls how the GraphQL query results are delivered to your generate() method. This affects how you access data and what SDK features are available.
Raw dict mode (default)​
When convert_query_response is false (the default), the data parameter passed to generate() is the raw GraphQL response dictionary:
class DeviceGenerator(InfrahubGenerator):
async def generate(self, data: dict) -> None:
for edge in data["TestDevice"]["edges"]:
device_name = edge["node"]["name"]["value"]
device_role = edge["node"]["role"]["value"]
# ... create objects using self.client
This is the more lightweight mode — no conversion overhead, and you work with plain Python dictionaries. You can also use Pydantic models to parse the response into typed objects for better IDE support and validation.
SDK object mode​
When convert_query_response is true, Infrahub converts the GraphQL response into InfrahubNode SDK objects. These are available via self.nodes and self.store. You do not use the data parameter in this mode:
class DeviceGenerator(InfrahubGenerator):
async def generate(self, data: dict) -> None:
for device in self.nodes:
device_name = device.name.value
device_role = device.role.value
# SDK features available: relationships, .save(), .delete()
When to use each​
Use raw dict mode (false) when | Use SDK object mode (true) when |
|---|---|
| Dict access is sufficient for your use case | You need SDK features like .save(), .delete(), or relationship traversal on the queried objects |
| You want minimal overhead | You prefer cleaner attribute access (node.name.value vs node["name"]["value"]) |
| The query returns a flat structure | You want to use self.store to look up nodes by ID |
| You want to use Pydantic models for type-safe parsing | You want the SDK to handle response parsing automatically |
Execution lifecycle​
The execute_in_proposed_change and execute_after_merge flags in .infrahub.yml control when a Generator runs in relation to the branch lifecycle.
execute_in_proposed_change (default: true)​
When true, the Generator runs during proposed change CI.
When false, the Generator is skipped entirely in proposed changes. Use this for Generators that are triggered by events (checksum triggers) or when you want to manually run the generator.
execute_after_merge (default: true)​
When true, the Generator runs again after the branch has been merged.
When false, the Generator will not run after the branch has been merged.
Running a Generator from events​
Inside a proposed change, Generator runs and artifact generation start at the same time, so an artifact that reads a Generator's output can render before that output exists (see Ordering between Generators and artifacts). Run the Generator on the branch before you open the proposed change. Chain two event rules:
- A node trigger rule matches the mutation that creates the input data and fires a group action that adds the object to the Generator definition's target group.
- A group trigger rule on that group matches the member-added event and fires a Generator action that runs the Generator against the new member.
The Generator runs only for the objects that are members of the Generator definition's target group. If none of the triggering objects are members, the run fails with Target <id> is not part of the group <id>.
Create both rules on the default branch, and set branch_scope to other_branches so the chain fires on the branches where you review the result in a proposed change. A rule created on another branch does not fire (see where trigger rules are stored and evaluated).
Set execute_in_proposed_change: false on the Generator, so that it does not also run inside the proposed-change pipeline. The flag applies only to that pipeline: it does not prevent an event from running the Generator.
Known limitations​
- 3289 deleting a Generator target object should delete the created objects of that target
- 11374 deleting an object of a branch-agnostic kind on a branch leaves it in its groups, and reading the members of those groups then fails with HTTP 500.
Learn by doing​
For a step-by-step walkthrough that builds a Generator from scratch, see Build your first Generator in the Academy tutorials.