# Create Module

> Author a new Cartography intel module end-to-end (entry point, sync GET/TRANSFORM/LOAD/CLEANUP, declarative data model, integration test, schema docs). Use when the user asks to add a new provider, integration, intel module, or service ingestion to Cartography (e.g. "add a new module for service X", "integrate ServiceY", "create a sync for Z API").

- Skill: `cartography-cncf/create-module` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add cartography-cncf/create-module`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cartography-cncf/create-module/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: cartography-cncf (https://skillmd.com/u/cartography-cncf)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/cartography-cncf/create-module

---


# create-module

Build a brand new Cartography intel module from scratch using the modern declarative data model. The module must follow the standard sync pattern (`get` -> `transform` -> `load` -> `cleanup`) and be exercised by an integration test.

## Critical rules

1. **Use the data model**, not handwritten Cypher. Call `load()` / `load_matchlinks()` from `cartography.client.core.tx`, and `GraphJob.from_node_schema()` for cleanup.
2. **Sub-resource relationships always point to a tenant-like node** (AWSAccount, AzureSubscription, GCPProject, GitHubOrganization, your `<Service>Tenant`). Never to an infrastructure parent.
3. **Required fields use direct dict access**, optional fields use `.get()` with `None` default. Do not silently swallow exceptions in `get()`.
4. **Only standard schema fields**: any custom field added to a `CartographyNodeSchema` / `CartographyRelSchema` subclass is ignored. See the `add-node-type` and `add-relationship` skills.
5. **Integration tests must call `sync()`**, not individual `load()` calls. Mock only external boundaries (API clients, credentials).
6. **All commits use `git commit -s`** (DCO).

## Instructions

### Step 1 — Lay out the module

```
cartography/intel/your_service/
├── __init__.py          # Entry point: start_your_service_ingestion()
└── users.py             # Domain sync (or devices.py, projects.py, etc.)

cartography/models/your_service/
├── tenant.py            # Tenant/account schema
└── user.py              # Domain schemas

tests/data/your_service/
└── users.py             # Mock API payloads

tests/integration/cartography/intel/your_service/
└── test_users.py        # End-to-end sync test
```

The entry point (`__init__.py`) reads from `Config`, validates required credentials, builds `common_job_parameters`, and dispatches to per-domain `sync()` functions. See `references/sync-pattern.md` for a copy-paste template.

### Step 2 — Wire CLI + Config

In `cartography/cli.py`:
- Add `PANEL_YOUR_SERVICE = "Your Service Options"` and register it in `MODULE_PANELS`.
- Add Typer options inside `_build_app().run()` (use `Annotated[Optional[str], typer.Option(...)]` with `rich_help_panel=PANEL_YOUR_SERVICE`).
- Resolve secrets from `os.environ` and pass them into `cartography.config.Config(...)`.

In `cartography/config.py`, extend `Config.__init__` with the new fields. Then in your module entry point, validate them and short-circuit with `logger.info("... not configured - skipping module")` when missing.

### Step 3 — Register the module in `cartography/sync.py`

Add one entry to `TOP_LEVEL_MODULES` using the lazy wrapper. **Do not add a top-level `import cartography.intel.your_service` to `sync.py`** — that defeats lazy SDK loading and reintroduces the slow-startup problem.

```python
TOP_LEVEL_MODULES = OrderedDict({
    ...
    "your_service": _LazyStage("cartography.intel.your_service", "start_your_service_ingestion"),
    ...
    # `analysis` must remain last
    "analysis": _LazyStage("cartography.intel.analysis", "run"),
})
```

Pick a sensible position relative to neighbors (cloud providers grouped together, etc.). The provider's heavy SDK imports stay where they are — they only fire when this stage is selected and run.

### Step 4 — Implement the sync pattern

For each domain (users, devices, projects, ...):

```python
@timeit
def sync(
    neo4j_session: neo4j.Session,
    api_key: str,
    tenant_id: str,
    update_tag: int,
    common_job_parameters: dict[str, Any],
) -> None:
    raw = get(api_key, tenant_id)               # 1. GET — dumb, raises on failure
    data = transform(raw)                       # 2. TRANSFORM — shape for ingest
    load_users(neo4j_session, data, tenant_id, update_tag)  # 3. LOAD — data model
    cleanup(neo4j_session, common_job_parameters)           # 4. CLEANUP — GraphJob
```

`get()` should be minimal: set timeouts, call `response.raise_for_status()`, and let errors propagate. AWS get-functions wrap with `@aws_handle_regions`. See `references/sync-pattern.md` for the long-form template, error-handling rules, and transform examples.

### Step 5 — Define the data model

Create dataclasses in `cartography/models/your_service/`. Required for every node:

```python
@dataclass(frozen=True)
class YourServiceUserNodeProperties(CartographyNodeProperties):
    id: PropertyRef = PropertyRef("id")                                    # REQUIRED
    lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)  # REQUIRED
    # business properties...
    tenant_id: PropertyRef = PropertyRef("TENANT_ID", set_in_kwargs=True)
```

The schema picks a label, properties, and the mandatory `sub_resource_relationship` to your tenant-like node:

```python
@dataclass(frozen=True)
class YourServiceUserSchema(CartographyNodeSchema):
    label: str = "YourServiceUser"
    properties: YourServiceUserNodeProperties = YourServiceUserNodeProperties()
    sub_resource_relationship: YourServiceTenantToUserRel = YourServiceTenantToUserRel()
    other_relationships: OtherRelationships = OtherRelationships([
        YourServiceUserToHumanRel(),
    ])
```

For advanced node configurations (extra labels, conditional labels, scoped cleanup, one-to-many) see the `add-node-type` skill. For relationships, MatchLinks, and multi-module patterns see the `add-relationship` skill. See `references/data-model.md` for the full reference.

### Step 6 — Load + cleanup

```python
def load_users(neo4j_session, data, tenant_id, update_tag):
    load(neo4j_session, YourServiceTenantSchema(), [{"id": tenant_id}], lastupdated=update_tag)
    load(neo4j_session, YourServiceUserSchema(), data, lastupdated=update_tag, TENANT_ID=tenant_id)

def cleanup(neo4j_session, common_job_parameters):
    GraphJob.from_node_schema(YourServiceUserSchema(), common_job_parameters).run(neo4j_session)
```

If you hand-write a Cypher write query during prototyping, use `run_write_query()` (managed transaction + retries), never `neo4j_session.run()`.

### Step 7 — Integration test

In `tests/integration/cartography/intel/your_service/test_users.py`, patch only `get()` and call `sync()` end-to-end. Assert outcomes (nodes + relationships) using `tests.integration.util.check_nodes` / `check_rels`. Do not assert on mock call counts or internal parameters. See `references/testing.md` for a full template and the test boundary policy.

### Step 8 — Module documentation

Create `docs/root/modules/your_service/index.md` and
`docs/root/modules/your_service/config.md`, then add `config` and `schema` to
the module's toctree. Follow `references/config-docs.md` exactly for the
canonical `config.md` structure and content-placement rules.

Do not create or hand-edit `schema.md`. Sphinx generates it from the
declarative data model. Write human-readable schema documentation in the model
itself:

- Add a docstring to every node and relationship schema.
- Add `description=` to every displayed `PropertyRef`.
- Use the `enrich-ontology` skill when adding semantic labels or canonical
  ontology projections.

### Step 9 — Optional: analysis jobs

If the module needs post-ingestion enrichment (internet exposure, permission inheritance, cross-resource linking), call `run_typed_analysis_job()` / `run_scoped_typed_analysis_job()` at the end of the entry point. See the `analysis-jobs` skill.

### Step 10 — Pre-submission checks

```bash
make lint
# integration test for the module:
pytest tests/integration/cartography/intel/your_service/ -x
```

Sign every commit: `git commit -s -m "..."`. Update the PR description to match `.github/pull_request_template.md`.

## Final checklist

- [ ] Entry point validates config and skips cleanly when unconfigured
- [ ] CLI panel + `Config` fields wired, secrets resolved from env vars
- [ ] Module registered in `cartography/sync.py:TOP_LEVEL_MODULES` via `_LazyStage`, with no top-level `import cartography.intel.<service>` added to `sync.py`
- [ ] Sync follows GET -> TRANSFORM -> LOAD -> CLEANUP
- [ ] All schemas use only standard fields (`label`, `properties`, `sub_resource_relationship`, `other_relationships`, `extra_node_labels`, `scoped_cleanup`)
- [ ] Sub-resource relationship targets a tenant-like node
- [ ] Required fields use `data["x"]`, optional use `data.get("x")` with `None` default
- [ ] `extra_index=True` set on frequently queried fields
- [ ] Integration test exercises `sync()`, asserts nodes + rels with `check_nodes` / `check_rels`
- [ ] `index.md` and canonical `config.md` added with a `config` / `schema` toctree
- [ ] Schema classes have docstrings and displayed `PropertyRef` values have descriptions
- [ ] No hand-written `schema.md` was added
- [ ] `make lint` clean, `git commit -s` used

## Common issues

See the `troubleshooting` skill for `ModuleNotFoundError`, `PropertyRef validation failed`, missing relationships, cleanup misbehavior, and date-handling pitfalls.

## References (load on demand)

- `references/sync-pattern.md` — full templates for `__init__.py`, `sync()`, `get()`, `transform()`, error-handling rules.
- `references/data-model.md` — node properties, schema, sub-resource relationships, loading, ECS example.
- `references/testing.md` — integration test template, `check_nodes` / `check_rels`, mocking policy, integration test boundary.
- `references/coding-conventions.md` — error handling, type hints, logging levels and format, deprecation conventions.
- `references/config-docs.md` — canonical module `config.md` template and content-placement rules.

