Infrahub Generator Creator
Overview
Expert guidance for creating Infrahub Generators. Generators
query data from Infrahub via GraphQL and create new nodes and
relationships based on the result -- enabling design-driven
automation where a "design" object automatically creates
downstream infrastructure.
Project Context
Infrahub config:
!cat .infrahub.yml 2>/dev/null || echo "No .infrahub.yml found"
Existing generators:
!find . -name "*.py" -path "*/generators/*" 2>/dev/null | head -20
When to Use
- Building design-driven automation (topology -> devices)
- Creating objects from templates or design definitions
- Implementing idempotent create-or-update workflows
- Auto-generating infrastructure from high-level designs
- Understanding the generator tracking system
Rule Categories
| Priority |
Category |
Prefix |
Description |
| CRITICAL |
Architecture |
architecture- |
Components, groups |
| CRITICAL |
Python Class |
python- |
Generator, generate() |
| HIGH |
Tracking |
tracking- |
Upsert, idempotent |
| HIGH |
API Ref |
api- |
Constructor, props |
| HIGH |
Registration |
registration- |
.infrahub.yml config |
| MEDIUM |
Patterns |
patterns- |
Cleaning, batch, store |
| LOW |
Testing |
testing- |
infrahubctl commands |
Schema Features This Skill Depends On
Generators create real objects, so the schema must
permit the shape they emit. Catch these gaps before
the first run — re-running a buggy generator can
delete data via the tracking cleanup.
Before writing Python
A generator should compute objects from a design. If
what you're about to write is "make these N specific
objects from a hardcoded list," that list is data —
move it to objects/ and either let the object
loader handle it directly or pass it into a smaller
generator. Walk this ladder before reaching for
InfrahubGenerator:
| Signal |
Cheaper layer |
See rule |
| Generator hardcodes object lists, role catalogs, or status sets |
YAML data files under objects/ (loaded by the object loader) |
yagni-generator-hardcoding-data |
| Generator recreates a built-in IPAM/VLAN primitive (custom IP address, prefix, VLAN nodes) |
inherit_from: [BuiltinIPAddress / BuiltinIPPrefix / IpamVLAN] in the schema, then the generator computes references rather than reimplementing the primitive |
yagni-custom-domain-primitives-instead-of-builtin |
Generator's output shape duplicates objects already in opsmill/schema-library |
inherit_from a library generic; the generator computes the instance but not the shape |
yagni-duplicate-shape-not-extracted-to-generic |
Generator allocates a subnet/IP/VLAN/port with ipaddress math, random, or a hand-written "find the first free one" loop |
A built-in resource pool — allocate_next_ip_prefix / allocate_next_ip_address, CoreIPPrefixPool / CoreNumberPool — which tracks utilization and stays idempotent across re-runs |
yagni-imperative-allocation-vs-resource-pool |
generate() stamps a fixed set of children with constant values and no computation (no branching, derived naming, or allocation) |
An Object Template (generate_template: true) users clone — the structure lives in data, not Python |
yagni-generator-that-should-be-template |
Bootstrap, seed, and demo generators (under
bootstrap/, seed/, demo/) are exempt — they
exist specifically to hardcode initial state. Use
Python when the generator is genuinely computing
objects from a design definition; see
rules/python-generate.md
for the legitimate cases.
Once you are writing Python, type your SDK calls with generated protocol
classes rather than string kinds — client.create(NetworkDevice, ...), not
kind="NetworkDevice" — so a schema change fails type-check instead of at
runtime. See
protocols-adopt-typed-kinds.
Generator Basics
Every generator has three components:
- Target group -- objects that trigger the generator
- GraphQL query (
.gql file) -- fetches the design data
- Python class -- inherits from
InfrahubGenerator,
implements generate()
from infrahub_sdk.generator import InfrahubGenerator
class MyGenerator(InfrahubGenerator):
async def generate(self, data: dict) -> None:
obj = await self.client.create(
kind="DcimDevice",
data={"name": "spine-01"},
)
await obj.save(allow_upsert=True)
Workflow
Follow these steps when creating a generator:
- Identify the design pattern — What "design"
object triggers generation? What objects should be
created from it? Read
rules/architecture-components.md
for the target group and generator components.
- Write the GraphQL query — Create a
.gql file
that fetches the design data. Read
../infrahub-common/graphql-queries.md
for query patterns.
- Implement the Python class — Inherit from
InfrahubGenerator, implement generate(). Read
rules/python-generate.md
for the class pattern and
rules/api-reference.md
for available methods.
- Make it idempotent — Use
allow_upsert=True so
re-running creates or updates without duplicates.
See rules/tracking-idempotent.md.
- Check for
from_graphql adoption opportunity — if
generate() iterates response edges and calls
self.client.get() to re-fetch typed peers, consider
refactoring to InfrahubNode.from_graphql() to collapse
O(N + 1) round trips to O(1). Read
rules/patterns-hydration.md
for the decision tree, detection heuristic, and refactor
recipe.
- Register in .infrahub.yml — Add under
generator_definitions with the target group. See
rules/registration-config.md.
- Test — Run
infrahubctl generator to validate.
See rules/testing-commands.md.
Supporting References
1---2name: infrahub-managing-generators-33description: Creates Infrahub Generators — design-driven automation that builds infrastructure objects from templates and topology definitions. TRIGGER when: building design-to-implementation workflows, auto-creating objects from templates, topology-driven generation. DO NOT TRIGGER when: designing schemas, writing data transforms, querying live data, populating static data files.4---56# Infrahub Generator Creator78## Overview910Expert guidance for creating Infrahub Generators. Generators11query data from Infrahub via GraphQL and create new nodes and12relationships based on the result -- enabling design-driven13automation where a "design" object automatically creates14downstream infrastructure.1516## Project Context1718Infrahub config:19!`cat .infrahub.yml 2>/dev/null || echo "No .infrahub.yml found"`2021Existing generators:22!`find . -name "*.py" -path "*/generators/*" 2>/dev/null | head -20`2324## When to Use2526- Building design-driven automation (topology -> devices)27- Creating objects from templates or design definitions28- Implementing idempotent create-or-update workflows29- Auto-generating infrastructure from high-level designs30- Understanding the generator tracking system3132## Rule Categories3334| Priority | Category | Prefix | Description |35| -------- | ------------ | --------------- | ---------------------- |36| CRITICAL | Architecture | `architecture-` | Components, groups |37| CRITICAL | Python Class | `python-` | Generator, generate() |38| HIGH | Tracking | `tracking-` | Upsert, idempotent |39| HIGH | API Ref | `api-` | Constructor, props |40| HIGH | Registration | `registration-` | .infrahub.yml config |41| MEDIUM | Patterns | `patterns-` | Cleaning, batch, store |42| LOW | Testing | `testing-` | infrahubctl commands |4344## Schema Features This Skill Depends On4546Generators create real objects, so the schema must47permit the shape they emit. Catch these gaps before48the first run — re-running a buggy generator can49delete data via the tracking cleanup.5051| If the generator... | The schema must... | See |52| ------------------- | ------------------ | --- |53| Creates objects of kind X | Have node X defined with the attributes the generator sets — extra attributes silently fail validation, missing required ones abort the create | [../infrahub-managing-schemas/rules/attribute-defaults-and-types.md](../infrahub-managing-schemas/rules/attribute-defaults-and-types.md) |54| Links the created object to a parent | Have a Component/Parent relationship pair with matching identifiers and `optional: false` on the Parent side | [../infrahub-managing-schemas/rules/relationship-component-parent.md](../infrahub-managing-schemas/rules/relationship-component-parent.md) |55| Reads a "design" node to drive output | Define that node's `human_friendly_id` so the generator's tracking key stays stable across runs | [../infrahub-managing-schemas/rules/display-human-friendly-id.md](../infrahub-managing-schemas/rules/display-human-friendly-id.md) |56| Is triggered by membership in a group | The target group must be a `CoreGeneratorGroup` (not `CoreStandardGroup`) — the dispatcher only recognizes the former | [rules/registration-config.md](./rules/registration-config.md) |57| Should be idempotent on re-run | Every `save()` uses `allow_upsert=True`; the run's tracking context deletes objects from prior runs that aren't recreated | [rules/tracking-idempotent.md](./rules/tracking-idempotent.md) |5859## Before writing Python6061A generator should *compute* objects from a design. If62what you're about to write is "make these N specific63objects from a hardcoded list," that list is data —64move it to `objects/` and either let the object65loader handle it directly or pass it into a smaller66generator. Walk this ladder before reaching for67`InfrahubGenerator`:6869| Signal | Cheaper layer | See rule |70| ------ | ------------- | -------- |71| Generator hardcodes object lists, role catalogs, or status sets | YAML data files under `objects/` (loaded by the object loader) | [yagni-generator-hardcoding-data](../infrahub-auditing-repo/rules/yagni-generator-hardcoding-data.md) |72| Generator recreates a built-in IPAM/VLAN primitive (custom IP address, prefix, VLAN nodes) | `inherit_from: [BuiltinIPAddress / BuiltinIPPrefix / IpamVLAN]` in the schema, then the generator computes references rather than reimplementing the primitive | [yagni-custom-domain-primitives-instead-of-builtin](../infrahub-auditing-repo/rules/yagni-custom-domain-primitives-instead-of-builtin.md) |73| Generator's output shape duplicates objects already in `opsmill/schema-library` | `inherit_from` a library generic; the generator computes the *instance* but not the *shape* | [yagni-duplicate-shape-not-extracted-to-generic](../infrahub-auditing-repo/rules/yagni-duplicate-shape-not-extracted-to-generic.md) |74| Generator allocates a subnet/IP/VLAN/port with `ipaddress` math, `random`, or a hand-written "find the first free one" loop | A built-in resource pool — `allocate_next_ip_prefix` / `allocate_next_ip_address`, `CoreIPPrefixPool` / `CoreNumberPool` — which tracks utilization and stays idempotent across re-runs | [yagni-imperative-allocation-vs-resource-pool](../infrahub-auditing-repo/rules/yagni-imperative-allocation-vs-resource-pool.md) |75| `generate()` stamps a fixed set of children with constant values and no computation (no branching, derived naming, or allocation) | An Object Template (`generate_template: true`) users clone — the structure lives in data, not Python | [yagni-generator-that-should-be-template](../infrahub-auditing-repo/rules/yagni-generator-that-should-be-template.md) |7677Bootstrap, seed, and demo generators (under78`bootstrap/`, `seed/`, `demo/`) are exempt — they79exist specifically to hardcode initial state. Use80Python when the generator is genuinely computing81objects from a design definition; see82[rules/python-generate.md](./rules/python-generate.md)83for the legitimate cases.8485Once you *are* writing Python, type your SDK calls with generated protocol86classes rather than string kinds — `client.create(NetworkDevice, ...)`, not87`kind="NetworkDevice"` — so a schema change fails type-check instead of at88runtime. See89[protocols-adopt-typed-kinds](../infrahub-common/rules/protocols-adopt-typed-kinds.md).9091## Generator Basics9293Every generator has three components:94951. **Target group** -- objects that trigger the generator962. **GraphQL query** (`.gql` file) -- fetches the design data973. **Python class** -- inherits from `InfrahubGenerator`,98 implements `generate()`99100```python101from infrahub_sdk.generator import InfrahubGenerator102103class MyGenerator(InfrahubGenerator):104 async def generate(self, data: dict) -> None:105 obj = await self.client.create(106 kind="DcimDevice",107 data={"name": "spine-01"},108 )109 await obj.save(allow_upsert=True)110```111112## Workflow113114Follow these steps when creating a generator:1151161. **Identify the design pattern** — What "design"117 object triggers generation? What objects should be118 created from it? Read119 [rules/architecture-components.md](./rules/architecture-components.md)120 for the target group and generator components.1212. **Write the GraphQL query** — Create a `.gql` file122 that fetches the design data. Read123 [../infrahub-common/graphql-queries.md](../infrahub-common/graphql-queries.md)124 for query patterns.1253. **Implement the Python class** — Inherit from126 `InfrahubGenerator`, implement `generate()`. Read127 [rules/python-generate.md](./rules/python-generate.md)128 for the class pattern and129 [rules/api-reference.md](./rules/api-reference.md)130 for available methods.1314. **Make it idempotent** — Use `allow_upsert=True` so132 re-running creates or updates without duplicates.133 See [rules/tracking-idempotent.md](./rules/tracking-idempotent.md).1345. **Check for `from_graphql` adoption opportunity** — if135 `generate()` iterates response edges and calls136 `self.client.get()` to re-fetch typed peers, consider137 refactoring to `InfrahubNode.from_graphql()` to collapse138 `O(N + 1)` round trips to `O(1)`. Read139 [rules/patterns-hydration.md](./rules/patterns-hydration.md)140 for the decision tree, detection heuristic, and refactor141 recipe.1426. **Register in .infrahub.yml** — Add under143 `generator_definitions` with the target group. See144 [rules/registration-config.md](./rules/registration-config.md).1457. **Test** — Run `infrahubctl generator` to validate.146 See [rules/testing-commands.md](./rules/testing-commands.md).147148## Supporting References149150- **[reference.md](./reference.md)** -- Class API,151 lifecycle, idempotency contract, `.infrahub.yml`152 registration (with the `query:`-required shape that153 differs from check_definitions)154- **[examples.md](./examples.md)** -- Complete Generator155 patterns (POP topology, network segment, minimal)156- **[../infrahub-common/graphql-queries.md](../infrahub-common/graphql-queries.md)**157 -- GraphQL query writing reference158- **[../infrahub-common/infrahub-yml-reference.md](../infrahub-common/infrahub-yml-reference.md)**159 -- .infrahub.yml project configuration160- **[../infrahub-common/marketplace-reference.md](../infrahub-common/marketplace-reference.md)**161 -- reuse a marketplace-published schema for the target162 data model before hand-rolling one to generate against163- **[../infrahub-common/rules/](../infrahub-common/rules/)** -- Shared rules164 (git integration, caching gotchas)165- **[../infrahub-common/rules/workflow-information-priority.md](../infrahub-common/rules/workflow-information-priority.md)**166 -- Skill content first; how to consult `docs.infrahub.app`167 on a genuine gap (e.g. deleting nodes)168- **[../infrahub-managing-schemas/SKILL.md](../infrahub-managing-schemas/SKILL.md)**169 -- Schema definitions Generators work with170- **[rules/](./rules/)** -- Individual rules organized by171 category prefix