Skip to content

Creating Entities

Function blocks can create new devices, interfaces, and device groups in the neops CMS — the foundation for device discovery, inventory automation, and dynamic topology management.


Why create entities from a function block?

Entity creation from within a function block removes the manual onboarding bottleneck: a discovery step scans a subnet, queries a CMDB, or parses a spreadsheet and feeds new entities directly into the neops data model — programmatically, repeatably, and idempotently.

Common use cases:

  • Subnet discovery — ICMP/SNMP sweep that registers every responding device.
  • CMDB synchronization — reconcile an external device list with neops.
  • Interface enumeration — parse show interfaces and create an Interface entity for each port.

Entity wrappers

The SDK provides three wrapper classes in neops_worker_sdk.workflow.entities:

Wrapper Extends Use case
Device DeviceTypeDto Create a new device in the CMS
Interface InterfaceTypeDto Create a new interface on an existing or new device
DeviceGroup DeviceGroupTypeDto Create a new logical group

Each wrapper inherits every field from the underlying DTO and adds two fields:

Field Type Purpose
skip_if_identifier_exists bool Skip creation when a matching entity already exists
identifier_fields Sequence[DeviceField\|InterfaceField\|GroupField] Fields used to detect duplicates

An id is auto-generated at instantiation via a cryptographically random temporary value, usable immediately for cross-entity references (e.g., linking a new Interface to its parent Device).


Creating a device

This function block discovers devices on a subnet and registers them in the CMS by appending new Device instances to the context:

class DiscoverDevices(FunctionBlock[DiscoverParams, DiscoverResult]):
    async def run(self, params: DiscoverParams, context: WorkflowContext) -> FunctionBlockResult[DiscoverResult]:
        discovered_ips = await self._scan_subnet(params.subnet)

        for ip in discovered_ips:
            new_device = Device(
                hostname=f"discovered-{ip.replace('.', '-')}",
                ip=ip,
                skip_if_identifier_exists=True,
                identifier_fields=[DeviceField.ip],
            )
            if context.devices is not None:
                context.devices.append(new_device)

        self.logger.info(f"Discovered {len(discovered_ips)} devices on {params.subnet}")

        return FunctionBlockResult(
            message=f"Discovered {len(discovered_ips)} devices.",
            success=True,
            data=DiscoverResult(discovered_count=len(discovered_ips)),
        )

    async def _scan_subnet(self, subnet: str) -> list[str]:
        """Placeholder: in practice, use icmplib or SNMP discovery."""
        del subnet
        return ["10.0.1.1", "10.0.1.2", "10.0.1.3"]

    async def acquire(self, params: DiscoverParams) -> FunctionBlockAcquireResult:
        del params
        return FunctionBlockAcquireResult(message="No resources required.", success=True, acquires=None)

Key observations:

  1. Device(...) creates a wrapper with standard DTO fields plus creation options.
  2. context.devices.append(new_device) adds the entity to the context; the diffing engine detects it as new at the end of execution.
  3. The temporary ID on new_device.id is valid for cross-references within the same execution (e.g., creating an Interface that belongs to this device).

Deduplication with skip_if_identifier_exists

When skip_if_identifier_exists=True, the CMS checks whether an entity with matching identifier field values already exists before inserting. If a match is found, the creation is silently skipped.

This makes the function block idempotent with respect to entity creation. Running the same discovery twice against the same subnet does not produce duplicate devices — exactly the behavior you want for scheduled or retried workflows.

Identifier fields are mandatory when skipping

Setting skip_if_identifier_exists=True without specifying identifier_fields raises a ValidationError at instantiation. The SDK enforces this constraint via a Pydantic model validator.


Identifier fields

Identifier fields tell the CMS which combination of fields constitutes a "unique" entity. Available options:

Entity Enum Values
Device DeviceField hostname, ip
Interface InterfaceField name
DeviceGroup GroupField name

These enums are validated against actual DTO model fields at import time — if the upstream API renames a field, the SDK fails fast rather than silently ignoring the mismatch. You can combine multiple fields for compound uniqueness:

Device(
    hostname="core-rtr-01",
    ip="10.0.0.1",
    skip_if_identifier_exists=True,
    identifier_fields=[DeviceField.hostname, DeviceField.ip],
)

The CMS considers the entity a duplicate only if all listed fields match an existing record. Field order is preserved in the generated DTO.


The diffing engine

You never call a "create entity" API yourself. Instead, the SDK tracks changes automatically:

flowchart LR
    A["Context snapshot<br/>(deep copy at init)"] --> D["Diffing engine"]
    B["Function block<br/>modifies context"] --> C["Current state<br/>(after execution)"]
    C --> D
    D --> E["CREATE ops"]
    D --> F["PATCH ops"]
    D --> G["DELETE ops"]
  1. At init, the SDK takes a deep copy of all entity lists (the snapshot).
  2. Your function block mutates the live lists: appending, modifying, or removing.
  3. After run() returns, compute_db_updates() diffs current state vs snapshot.

The diff produces three operation types:

Condition Operation DTO
Entity in current state but not in snapshot CREATE EntityCreateDto
Entity in both, with changed fields PATCH EntityPatchDto
Entity in snapshot but not in current state DELETE EntityDeleteDto

Read-only and computed fields (id, created_at, updated_at, lock states, permission fields) are excluded from diffing automatically.

Combine creation and modification

You can create new entities and modify existing ones in the same function block execution. The diffing engine handles mixed operations in a single pass.


Next: Pure and Idempotent Function Blocks — understand how safety semantics affect workflow retry and rollback behavior.