troubleshooting
Diagnostic playbook for the most common errors encountered while developing Cartography intel modules.
Common issues and solutions
Import errors
# Problem: ModuleNotFoundError for your new module
# Solution: ensure __init__.py files exist in all directories
cartography/intel/your_service/__init__.py
cartography/models/your_service/__init__.py
Checklist:
-
__init__.pyexists incartography/intel/your_service/ -
__init__.pyexists incartography/models/your_service/ - Module is imported in the parent
__init__.pyif needed
Schema validation errors
# Problem: "PropertyRef validation failed"
# Solution: check dataclass syntax and PropertyRef definitions
@dataclass(frozen=True) # do not forget frozen=True
class YourNodeProperties(CartographyNodeProperties):
id: PropertyRef = PropertyRef("id") # must have type annotation
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
Common causes:
- Missing
frozen=Truein@dataclass. - Missing type annotation (
: PropertyRef). - Typo in the
PropertyReffield name.
Relationship connection issues
# Problem: relationships not created
# Solution: ensure target nodes exist before creating relationships
# Load parent nodes first:
load(neo4j_session, TenantSchema(), tenant_data, lastupdated=update_tag)
# Then load child nodes with relationships:
load(neo4j_session, UserSchema(), user_data, lastupdated=update_tag, TENANT_ID=tenant_id)
Debugging steps:
- Check the target node label matches exactly.
- Verify
target_node_matcherkeys match the target node's property names. - Ensure the value in your data dict or kwargs is not
None.
Cleanup job failures
# Problem: "GraphJob failed" during cleanup
# Solution: check common_job_parameters
common_job_parameters = {
"UPDATE_TAG": config.update_tag, # must match what is set on nodes
"TENANT_ID": tenant_id, # if using scoped cleanup (default)
}
# Problem: cleanup deletes too much (wrong scoped_cleanup setting)
# Solution: verify scoped_cleanup is appropriate
@dataclass(frozen=True)
class MySchema(CartographyNodeSchema):
# tenant-scoped resources — default, do not specify
# scoped_cleanup: bool = True
# global resources only — rare
scoped_cleanup: bool = False # vuln data, threat intel, etc.
For details on when to override scoped_cleanup, see the add-node-type skill.
Data transform issues
# Problem: KeyError during transform
# Solution: handle required vs optional fields correctly
{
"id": data["id"], # required — let it fail
"name": data.get("name"), # optional
# avoid empty-string defaults — they hide missing data
# "email": data.get("email", ""),
"email": data.get("email"), # use None default
}
Schema definition issues
# Problem: adding custom fields to schema classes
# Solution: remove them — only standard fields are recognised
@dataclass(frozen=True)
class MyRel(CartographyRelSchema):
# Remove custom fields — they are silently ignored:
# conditional_match_property: str = "some_field"
# custom_flag: bool = True
# extra_config: dict = {}
# Keep only the standard relationship fields
target_node_label: str = "TargetNode"
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(...)
direction: LinkDirection = LinkDirection.OUTWARD
rel_label: str = "CONNECTS_TO"
properties: MyRelProperties = MyRelProperties()
For the standard schema fields, see the add-node-type skill.
Performance issues
# Problem: slow queries
# Solution: index frequently queried fields
email: PropertyRef = PropertyRef("email", extra_index=True)
# Query on indexed fields when possible
MATCH (u:User {id: $user_id}) # good — id is always indexed
MATCH (u:User {name: $name}) # bad — name might not be indexed
Fields used inside a target_node_matcher are indexed automatically.
MatchLink issues
# Problem: MatchLinks not creating relationships
# Solution: both source and target nodes must exist first
load(neo4j_session, SourceNodeSchema(), source_data, ...) # 1. source nodes
load(neo4j_session, TargetNodeSchema(), target_data, ...) # 2. target nodes
load_matchlinks( # 3. then MatchLinks
neo4j_session,
YourMatchLinkSchema(),
mapping_data,
lastupdated=update_tag,
_sub_resource_label="AWSAccount",
_sub_resource_id=account_id,
)
# Problem: MatchLink cleanup not working
# Solution: use GraphJob.from_matchlink with the right args
GraphJob.from_matchlink(
YourMatchLinkSchema(),
"AWSAccount", # _sub_resource_label
common_job_parameters["AWS_ID"], # _sub_resource_id
common_job_parameters["UPDATE_TAG"], # update_tag
).run(neo4j_session)
For full MatchLink details, see the add-relationship skill.
Debugging tips
- Check existing patterns first. Look at similar modules in
cartography/intel/before inventing new ones. - Verify imports. All
CartographyNodeSchema/CartographyRelSchemaimports must point tocartography.models.core.*. - Test transform functions with real API responses.
- Validate Cypher in Neo4j Browser when relationships are not appearing.
- Check file naming. Module files should match the service name (
cartography/intel/lastpass/users.py). - Run tests incrementally. After each change, run the integration test.
- Test through
sync(), not isolatedload()calls.
Key files
| File | Purpose |
|---|---|
cartography/client/core/tx.py |
Core load() and load_matchlinks() — query generation lives here |
cartography/graph/job.py |
GraphJob cleanup operations |
cartography/models/core/common.py |
PropertyRef definition |
cartography/models/core/nodes.py |
CartographyNodeSchema, CartographyNodeProperties, ExtraNodeLabels, etc. |
cartography/models/core/relationships.py |
CartographyRelSchema, LinkDirection, matchers, MatchLinks |
cartography/config.py |
Config object — check missing fields here |
cartography/cli.py |
Typer CLI with help panels |
cartography/data/indexes.cypher |
Manual index definitions (legacy) |
cartography/data/jobs/cleanup/ |
Legacy cleanup JSON files |
cartography/analysis/*/analysis.py |
Typed analysis jobs (see analysis-jobs skill) |
cartography/data/jobs/analysis/ |
Legacy migration/cleanup JSON jobs |
cartography/data/jobs/scoped_analysis/ |
Legacy scoped migration/cleanup JSON jobs |
Test utilities
from tests.integration.util import check_nodes, check_rels
# Nodes
expected_nodes = {
("user-123", "alice@example.com"),
("user-456", "bob@example.com"),
}
assert check_nodes(neo4j_session, "YourServiceUser", ["id", "email"]) == expected_nodes
# Relationships
expected_rels = {
("user-123", "tenant-123"),
("user-456", "tenant-123"),
}
assert check_rels(
neo4j_session,
"YourServiceUser", "id",
"YourServiceTenant", "id",
"RESOURCE",
rel_direction_right=True,
) == expected_rels
Error message reference
| Error message | Likely cause | Solution |
|---|---|---|
PropertyRef validation failed |
Missing type annotation or frozen=True |
Check dataclass definition |
Node not found for relationship |
Target node does not exist | Load parent nodes first |
GraphJob failed |
Wrong common_job_parameters |
Check UPDATE_TAG and tenant ID |
KeyError: 'field_name' |
Required field missing in API response | Use .get() for optional fields |
ModuleNotFoundError |
Missing __init__.py |
Add __init__.py to all directories |
Relationship not created |
Matcher property mismatch | Verify property names match exactly |
When to ask for help
Stop and ask the user when:
- Legacy Cypher queries contain unclear business logic.
- Complex relationships do not map clearly to the data model.
- Tests keep failing after multiple attempts.
- Multiple modules look interdependent.
- Performance issues persist after adding indexes.
- The graph contains unexpected data after sync.