Composed Entities

Modified on Tue, 14 Jul at 11:35 AM

Composed entities let you model nested structures by storing one MaltegoEntity inside another. This is useful when an upstream API returns a record that naturally contains related child entities, such as an account owner, aliases, identifiers, or media.

Full composed-entity support is currently available only in the Maltego
Graph Browser client. Maltego Desktop does not support composed entities,
so transforms that rely on ``composite_entities=True`` should be treated as
Browser-only.

Defining entity-typed properties

Entity-typed properties use the same property helper as primitive fields, but the annotation is another entity class:

from typing import List
from maltego.entities import Alias, Image, Person, UniqueIdentifier
from maltego.server import (
    LinkProperties,
    MATCHING_RULE_LOOSE,
    MaltegoEntity,
    MaltegoEntityConfig,
    MaltegoEntityProperty as MEF,
    register_entity,
)

@register_entity
class AffiliationComposite(MaltegoEntity):
    Config = MaltegoEntityConfig(
        value_property="uid",
        value_key="properties.uniqueidentifier",
        display_property="person",
        display_key="person.fullname",
        display_name="Affiliation",
    )

    person: Person = MEF(
        name="person",
        display_name="Account Owner",
        matching_rule=MATCHING_RULE_LOOSE,
        # False is the default; shown here to make the entity-link direction
        # explicit at the entity definition level.
        link_properties=LinkProperties(is_reversed=False, label="To Owner"),
    )
    alias: List[Alias] = MEF(
        name="alias",
        display_name="Aliases",
        matching_rule=MATCHING_RULE_LOOSE,
    )
    uid: UniqueIdentifier = MEF(
        name="uid",
        display_name="UID",
    )
    profile_image: Image = MEF(
        name="profile_image",
        display_name="Profile Image",
    )

How nested keys resolve

value_key and display_key target the child entity property names declared by the nested entity, not the Python attribute name on the parent.

The example above uses standard entities from maltego.entities. The relevant parts of those child schemas look like this:

from maltego.server import MaltegoEntity, MaltegoEntityProperty as MEF

# abridged to the fields that matter for key resolution
class Person(MaltegoEntity):
    fullname: str = MEF(name="person.fullname", display_name="Full Name")
    firstnames: str = MEF(name="person.firstnames", display_name="First Names")
    lastname: str = MEF(name="person.lastname", display_name="Surname")

class UniqueIdentifier(MaltegoEntity):
    uniqueidentifier: str = MEF(
        name="properties.uniqueidentifier",
        display_name="Unique Identifier",
    )
    identifier_type: str = MEF(
        name="identifierType",
        display_name="Identifier Type",
    )

That means the composite config resolves like this:

  • display_property="person" selects the nested Person entity
  • display_key="person.fullname" reads the child's person.fullname property
  • value_property="uid" selects the nested UniqueIdentifier entity
  • value_key="properties.uniqueidentifier" reads the child's properties.uniqueidentifier property

Returning composed entities from transforms

Any transform that returns or mutates composed entities should opt in with composite_entities=True:

from typing import List
from maltego.entities import Alias, Person, Phrase, UniqueIdentifier
from maltego.model.context import MaltegoContext
from maltego.model.graph import MaltegoGraph
from maltego.server import register_transform

@register_transform(composite_entities=True)
async def composed_affiliation_transform(
    input_entity: Phrase,
    context: MaltegoContext,
) -> MaltegoGraph:
    person_entity = Person("John Doe")
    alias_entities: List[Alias] = [Alias("johndoe42"), Alias("john.d")]
    uid_entity = UniqueIdentifier("1477245957")

    affiliation = AffiliationComposite(uid_entity)
    affiliation.person = person_entity
    affiliation.set_property("alias", alias_entities)

    context.graph.add_child(input_entity, affiliation)
    return context.graph

Where this fits in a transform server project

The starter project does not ship a composed-entity module by default. When you need this pattern, add a dedicated module under transforms/ and keep the entity classes next to the transform that returns them, or move them into a shared entities.py module once several transforms reuse them.

Choosing values and labels

MaltegoEntityConfig gives you control over how a composed entity is shown:

  • value_property: the property that owns the entity's main value
  • value_key: the nested child property name to read from that property
  • display_property: the property shown in the graph
  • display_key: the nested child property name used for that display value

If you omit value_key or display_key, the nested entity's own value is used.

When to use this pattern

Use composed entities when:

  • your upstream API returns nested records that belong together
  • you want one graph node to carry structured child entities
  • you need typed child properties instead of flattening everything into strings

If you only need flat metadata, regular primitive properties are usually simpler.

  • See the SDK API Reference article for the custom-entity API.
  • See the Standard Entities Overview article.
  • See the Pydantic Mapping Patterns article.

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article