Writing Your First Transform (Quickstart)

Modified on Tue, 14 Jul at 11:38 AM

Examples in this guide are available as runnable transforms in the template project. Run maltego-transforms start my_project and see transforms/quickstart_example.py to experiment with these patterns directly. A full working copy of that example file is included at the bottom of this article, in Full Quickstart Example File.

@register_transform decorator

To create a transform function with maltego-transforms, you simply decorate a Python function using the register_transform decorator. This decorator tells the library to treat the function as a transform function, which can be automatically discovered by the maltego-transforms built-in transform server.

Once your transform function has been defined and decorated, it can be used to perform custom data transformations in Maltego. When a transform execution request is received by the maltego-transforms transform server, it will automatically discover and execute the appropriate transform function, which will return the transformed data in the form of Maltego Entities.

register_transform reference

register_transform (from maltego.server import register_transform) registers a function as a Maltego transform, making it discoverable by the Maltego client. It can be used bare (@register_transform) or called with keyword arguments (@register_transform(display_name=..., ...)).

Parameters:

  • tf_function: The function to register as a Maltego transform (supplied automatically when used as a bare @register_transform).

  • display_name: The display name shown in the Maltego Client. If not specified, it is derived from the transform function name.

  • name: The unique name for the transform. If not specified, the transform function name is used.

  • description: A brief description of the transform's purpose and functionality.

  • author: The author's email or identifier. Defaults to the value in the server settings.

  • location_relevance: Metadata for specifying the transform's relevance to geographic locations.

  • ns: The namespace for the transform. Defaults to the server settings.

  • owner: The owner of the transform. Defaults to the server settings.

  • disclaimer: A disclaimer for using the transform, shown in the Maltego Client.

  • version: The version of the transform. Defaults to the value in the server settings.

  • settings: A list of configurable settings for the transform, such as API keys or toggles (list of TransformSetting).

  • transform_set: The transform set to which this transform belongs.

  • authenticator: An OAuth authenticator for securing transform requests.

  • metadata: Additional metadata for tagging or categorizing the transform during discovery.

  • client_filter: Filter for Maltego client versions supported for this transform. Example:

    client_filter = MaltegoClientFilter(
        min_clients=[
            MaltegoClient(name="Maltego Desktop", version=(2, 5, 0)),
            MaltegoClient(name="Maltego Web Browser", version=(1, 0, 0)),
        ],
        max_clients=[
            MaltegoClient(name="Maltego Web Browser", version=(3, 0, 0)),
        ],
    )
  • any_properties: A list of properties where the transform can run if any are present in the entity.
  • all_properties: A list of properties where the transform can run only if all are present in the entity.
  • input_constraint: Logical constraints for filtering transform inputs.
  • interactive: Whether the transform uses prompts or not.
  • composite_entities: Whether the transform uses composite entities.

Raises ValueError if the transform name is invalid, or if mutual exclusivity between any_properties and all_properties is violated. Raises TypeError if metadata or client version parameters are not valid types. Returns the decorated function, unmodified, so it can still be called/imported/tested like a normal Python function.

Writing Transforms

In order to identify Python functions that are intended to serve as Maltego transforms, we refer to them as 'transform functions' and annotate them using the register_transform decorator.

When a transform execution request is received by the maltego-transforms built-in transform server, it identifies the appropriate transform function based on the input data, and calls it with the relevant arguments.

The interface of the transform function specifies the expected behavior, both on the client and on the server sides, when executing a transform.

In order to specify which types of entities a Maltego Transform can process, we use Python type annotations. There are three types of input annotations:

  • Single Entity as Input: The transform processes a single entity of a given type
  • List Inputs: The transform processes 1 to n entities of a given type
  • Graph Inputs: The transform processes a graph including 1 to n entities and links

Input Annotations

Single Entity as Input

from maltego.server import register_transform, MaltegoEntity, MaltegoContext

@register_transform
async def transform_function(entity: MaltegoEntity, context: MaltegoContext) -> MaltegoEntity:
    return entity

The simplest type of Maltego Transform interface accepts any entity type as input and produces a single output entity of any type. This type of transform can be executed by the Maltego client on any entity in a graph. If multiple entities are selected, the transform will be executed concurrently on each entity.

from maltego.server import register_transform, register_entity, MaltegoEntity, MaltegoContext


@register_entity
class Number(MaltegoEntity):
    pass

@register_transform
async def transform_function(entity: Number, context: MaltegoContext) -> Number:
    return Number(5)

In this case, the transform function's input annotation is set to a new entity type called "Number". As a result, the transform can only be executed on entities of type "Number". maltego-transforms, with its implemented type checking, ensures that this behavior is enforced at runtime. Therefore, if the transform is executed on an entity of a different type, it will result in a type error.

List Inputs

**Client availability:** Maltego Graph Desktop sends selected entities together in one request. Maltego Graph Browser runs the transform once per selected entity, so do not use a list input when your transform depends on receiving the selection as one batch.

Maltego Transforms can be defined to accept a list of entities as input, allowing for faster execution when making requests that can handle multiple inputs. Additionally, this feature enables the combination of results from multiple entities, as seen in the sum_numbers transform function example.

from typing import List
from maltego.server import register_transform, MaltegoContext

@register_transform
async def sum_numbers(entities: List[Number], context: MaltegoContext) -> Number:
    return Number(sum([number.value for number in entities]))

One of the key benefits of using list inputs in transform functions is the ability to combine multiple inputs, thereby allowing for a more comprehensive transform execution that leverages more information. This can lead to more accurate results and more sophisticated transformations.

Graph Inputs

**Client availability:** Graph-input transforms are currently supported in Maltego Graph Desktop only. Do not use ``MaltegoGraph`` input for a Maltego Graph Browser workflow.
from maltego.server import register_transform, MaltegoGraph, MaltegoContext

@register_transform
async def transform_function(graph: MaltegoGraph, context: MaltegoContext):
    pass

The MaltegoGraph annotation is a unique input type that provides access to the Maltego graph object. This object contains not only the entities in the selected graph, but also the links between them. With this information, transform functions using this input type can perform powerful investigative workflows by combining entire graphs and extracting information from their links. The example mentioned above can be executed on any grouping of entities within the graph.

from maltego.server import register_transform, MaltegoGraph, MaltegoContext

@register_transform
async def transform_function(entities: MaltegoGraph[Number], context: MaltegoContext):
    pass

This example allows for a more fine-grained control over the type of entities the transform accepts. By using a Generic pattern we only allow Graphs that exclusively consist of entities with the type Number.

Input Constraints

Input constraints control which entities can trigger a transform. They're evaluated by the Maltego client before showing the transform to users.

See the "Input Constraints" article in this same folder for the complete guide with examples.

Output Annotations

There are three options available for specifying the output of a transform function. When a transform produces output entities, the Maltego Client automatically creates links between them based on the output annotation of the transform function.

No Output

If a transform does not produce any entity as output, then the return annotation should be set to None.

from maltego.server import register_transform, MaltegoEntity, MaltegoContext

@register_transform
async def transform_function(entity: MaltegoEntity) -> None:
    pass

In the example above no entities are added to the graph and, therefore, no links will be created between the entities.

Single Entity Output

If the transform produces a single entity, the output annotation should be a class that derives from MaltegoEntity. However, the output annotation can also be declared as Optional to signify that the transform may or may not produce any entity at all.

from typing import Optional, List
from maltego.server import register_transform, MaltegoEntity, MaltegoContext

@register_transform
async def transform_function(entities: List[MaltegoEntity]) -> Optional[Number]:
    return Number(len(entities))

In this example a single entity is returned for a list of input entities. A link from all input entities to the generated output entity gets created.

List Output

If the transform produces multiple entities, the output annotation should be a list of classes that derive from MaltegoEntity. As with the single entity output, it can also be declared as Optional.

from typing import List, Optional
from maltego.server import register_transform, MaltegoEntity, MaltegoContext

@register_transform
async def transform_function(entities: List[MaltegoEntity]) -> Optional[List[Number]]:
    return [Number(i) for i in range(0,len(entities))]

In this example a cross product of links between each input entity and each output entity is created.

Possible combinations of entity input and outputs:

Single OutputMultiple Output
Single InputA->BA->B1, A->B2, ..., A->Bn
Multiple InputA1->B, A2->B, ..., An->BAxB

A single Entity or List annotation can be used as the output annotation. The transform needs to make sure this output condition is met.

Streaming Output (AsyncGenerator)

For long-running transforms that produce results incrementally, use an AsyncGenerator to stream entities back to the Maltego client as they become available:

import asyncio
from typing import AsyncGenerator
from maltego.server import register_transform, MaltegoContext
from maltego.entities import Phrase, URL

@register_transform
async def stream_results(
    input_entity: URL,
    context: MaltegoContext,
) -> AsyncGenerator[Phrase, None]:
    # Results are sent to Maltego as they are yielded
    for i in range(10):
        context.log.inform(f"Processing item {i}")
        yield Phrase(f"Result {i}")
        await asyncio.sleep(1)  # Simulate slow processing

Benefits of streaming:

  • Real-time feedback: Users see results as they arrive, not all at once
  • Memory efficient: No need to collect all results in memory before returning
  • Better UX for slow APIs: First results appear quickly while fetching continues

Streaming is particularly useful when:

  • Fetching paginated data from external APIs
  • Processing large datasets
  • Making multiple slow API calls

Example with API pagination:

from typing import AsyncGenerator
from maltego.server import register_transform, IntegrationClient, MaltegoContext
from maltego.entities import Phrase

client = IntegrationClient()

@register_transform
async def fetch_all_pages(
    input_entity,
    context: MaltegoContext,
) -> AsyncGenerator[Phrase, None]:
    page = 1
    while True:
        response = await client.get(
            f"https://api.example.com/items?page={page}",
            context=context,
        )
        items = response.json().get("results", [])

        if not items:
            break  # No more pages

        context.log.inform(f"Processing page {page}")
        for item in items:
            yield Phrase(item["name"])

        page += 1

See the "Pagination" article in this same folder for the built-in pagination utilities that handle this pattern automatically.

Transform Settings

It is possible to define settings for transforms that get discovered when the transform server is installed.

For naming rules, global settings, and OAuth-related settings, see the "Transform Settings" article in this same folder. Even when the client stores a serialized setting name, the SDK normalizes it back to the original TransformSetting.name inside the transform's settings dictionary.

from typing import Dict, Any
from maltego.server import register_transform, MaltegoContext, TransformSetting
from maltego.entities import Phrase

@register_transform(
    display_name="Settings Example [My Transforms]",
    settings=[
        TransformSetting(
            name="api_key",
            display_name="API Key",
            type="string",
            optional=False,
            popup=True,
        ),
        TransformSetting(
            name="max_results",
            display_name="Maximum Results",
            type="int",
            default_value=100,
        ),
    ]
)
async def transform_with_settings(
    input_entity: Phrase,
    settings: Dict[str, Any],
    context: MaltegoContext
) -> Phrase:
    api_key = settings.get("api_key")
    max_results = settings.get("max_results")
    return Phrase(f"Using key: {api_key[:4]}... with limit {max_results}")

When the settings variable is defined in the register_transform decorator, users can send settings with each transform execution.

The TransformSetting class is used to define the settings. The name and value of the settings can be accessed in the transform function through an input variable named "settings".

Available Setting Types

Transform settings support multiple data types:

from maltego.server import register_transform, TransformSetting

@register_transform(
    display_name="All Setting Types [My Transforms]",
    settings=[
        # String input
        TransformSetting(
            name="str_setting",
            display_name="Text Input",
            type="string",
            default_value="default text",
            popup=True,
        ),
        # Integer input
        TransformSetting(
            name="int_setting",
            display_name="Number Input",
            type="int",
            default_value=10,
            popup=True,
        ),
        # Floating point input
        TransformSetting(
            name="double_setting",
            display_name="Decimal Input",
            type="double",
            default_value=3.14,
            popup=True,
        ),
        # Boolean checkbox
        TransformSetting(
            name="bool_setting",
            display_name="Enable Feature",
            type="boolean",
            default_value=True,
            popup=True,
        ),
        # Date picker (YYYY-MM-DD)
        TransformSetting(
            name="date_setting",
            display_name="Select Date",
            type="date",
            popup=True,
        ),
        # DateTime picker
        TransformSetting(
            name="datetime_setting",
            display_name="Select Date and Time",
            type="datetime",
            popup=True,
        ),
        # Date range picker
        TransformSetting(
            name="daterange_setting",
            display_name="Select Date Range",
            type="daterange",
            popup=True,
        ),
    ],
)
async def transform_all_settings(input_entity, settings):
    # Access settings by name
    text = settings.get("str_setting")
    number = settings.get("int_setting")
    enabled = settings.get("bool_setting")
    # ...

List Setting Types

Transform settings also support list types for collecting multiple values:

from maltego.server import register_transform, TransformSetting

@register_transform(
    display_name="List Settings Example",
    settings=[
        # Multiple strings
        TransformSetting(
            name="tags",
            display_name="Tags",
            type=TransformSetting.Types.str_list,
            default_value=["tag1", "tag2"],
            popup=True,
        ),
        # Multiple integers
        TransformSetting(
            name="ports",
            display_name="Ports",
            type=TransformSetting.Types.int_list,
            default_value=[80, 443, 8080],
            popup=True,
        ),
        # Multiple floats
        TransformSetting(
            name="thresholds",
            display_name="Thresholds",
            type=TransformSetting.Types.float_list,
            default_value=[0.5, 0.75, 0.9],
            popup=True,
        ),
        # Multiple booleans
        TransformSetting(
            name="flags",
            display_name="Flags",
            type=TransformSetting.Types.boolean_list,
            default_value=[True, False, True],
            popup=True,
        ),
        # Multiple dates
        TransformSetting(
            name="important_dates",
            display_name="Important Dates",
            type=TransformSetting.Types.date_list,
            popup=True,
        ),
    ],
)
async def transform_with_lists(input_entity, settings):
    tags = settings.get("tags", [])
    # ...

Date Range Settings

For date ranges, you can use the daterange helper with either relative or absolute values:

from maltego.server import register_transform, TransformSetting, daterange
from datetime import datetime

@register_transform(
    settings=[
        # Relative date range (last 24 hours, 7 days, etc.)
        TransformSetting(
            name="time_window",
            display_name="Time Window",
            type=TransformSetting.Types.datetime_range,
            default_value=daterange(
                date_range=daterange.Ranges.last_24_hours
            ),
            popup=True,
        ),
        # Absolute date range
        TransformSetting(
            name="custom_range",
            display_name="Custom Range",
            type=TransformSetting.Types.datetime_range,
            default_value=daterange(
                start=datetime(2024, 1, 1),
                end=datetime(2024, 12, 31)
            ),
            popup=True,
        ),
    ],
)
async def transform_with_daterange(input_entity, settings):
    time_window = settings.get("time_window")
    # ...

Available relative date ranges:

  • daterange.Ranges.today
  • daterange.Ranges.yesterday
  • daterange.Ranges.week_to_date
  • daterange.Ranges.business_week_to_date
  • daterange.Ranges.month_to_date
  • daterange.Ranges.year_to_date
  • daterange.Ranges.previous_week
  • daterange.Ranges.previous_business_week
  • daterange.Ranges.previous_month
  • daterange.Ranges.previous_year
  • daterange.Ranges.last_15_minutes
  • daterange.Ranges.last_30_minutes
  • daterange.Ranges.last_4_hours
  • daterange.Ranges.last_12_hours
  • daterange.Ranges.last_24_hours
  • daterange.Ranges.last_7_days
  • daterange.Ranges.last_30_days
  • daterange.Ranges.last_60_days
  • daterange.Ranges.last_90_days
  • daterange.Ranges.last_6_months
  • daterange.Ranges.last_1_year
  • daterange.Ranges.last_2_years
  • daterange.Ranges.last_5_years
  • daterange.Ranges.last_10_years
  • daterange.Ranges.since_unix_epoch_time

Global and Auth Settings

For settings shared across transforms or used for authentication:

from maltego.server import register_transform, TransformSetting

# Define reusable settings
API_KEY_SETTING = TransformSetting(
    name="api_key",
    display_name="API Key",
    auth=True,          # Mark as authentication setting
    is_global=True,     # Share value across all transforms
)

AUTH_POPUP_SETTING = TransformSetting(
    name="credentials",
    display_name="Credentials",
    auth=True,
    popup=True,         # Prompt before each transform run
)

@register_transform(
    display_name="Transform with Auth",
    settings=[API_KEY_SETTING, AUTH_POPUP_SETTING],
)
async def transform_with_auth(input_entity, settings):
    api_key = settings.get("api_key")
    # ...

Setting Options

Each TransformSetting supports these options:

  • name: Internal identifier (required)
  • display_name: Label shown in Maltego UI (required)
  • type: Setting type (see Available Types below)
  • default_value: Default value if not provided
  • optional: Whether the setting can be empty (default: True)
  • popup: Show in popup dialog before transform runs (default: False)
  • auth: Mark as authentication/sensitive setting (default: False)
  • is_global: Share value across all transforms in the integration (default: False)
  • is_global_setting: Compatibility flag for the older global-setting naming shape (default: False)

is_global=True is the current SDK flag for one shared namespace-wide value. is_global_setting=True is a compatibility flag for integrations that already rely on the older global-setting naming shape. Runtime access remains settings.get("<name>") for both flags because the SDK deserializes settings back to the original name.

Available Types

Scalar types:

  • "string" / TransformSetting.Types.str
  • "int" / TransformSetting.Types.int
  • "double" / TransformSetting.Types.float
  • "boolean" / TransformSetting.Types.boolean
  • "date" / TransformSetting.Types.date
  • "datetime" / TransformSetting.Types.datetime
  • "daterange" / TransformSetting.Types.datetime_range

List types:

  • TransformSetting.Types.str_list
  • TransformSetting.Types.int_list
  • TransformSetting.Types.float_list
  • TransformSetting.Types.boolean_list
  • TransformSetting.Types.date_list

Defining Entities

We can define and announce new entities by using the register_entity decorator. Any newly defined entity must be a subclass of MaltegoEntity.

register_entity reference

register_entity (from maltego.server import register_entity) has no separate docstring in the SDK source, but its real implementation (MaltegoTransformServer.register_entity in maltego/server/__init__.py) does the following when you decorate an entity class:

  • Requires the transform server to be set up for dynamic paired-configuration generation; otherwise it raises ValueError.
  • Requires the entity class to define a class attribute named Config of type MaltegoEntityConfig; otherwise it raises ValueError ("please add an attribute 'Config' of type MaltegoEntityConfiguration inside your Entity class definition").
  • If the entity's TYPE_NAME collides with a parent class's TYPE_NAME (which happens by default, since TYPE_NAME is auto-generated from the class name), it rewrites TYPE_NAME to maltego.<ClassName> so subclasses get their own distinct type.
  • Registers the entity's TYPE_NAME in the internal entity type registry.
  • Normalizes any overlay enum values (OverlayTypes, OverlayPositions) on the entity's Config.overlays to their plain string values.
  • Adds the entity's category to the server's known entity categories and adds the entity to the paired configuration that gets published to the Maltego client.
  • Returns the entity class unchanged, so it can still be imported and used normally in your code.

Usage example:

from maltego.server import register_entity, MaltegoEntity

@register_entity
class HelloMaltego(MaltegoEntity):
    pass

This annotation exposes the class HelloMaltego as a new entity to the Maltego Client.

Using Custom Entity Id's

An entity in Maltego always has an attached ID. By default, the entity ID is set to the name of the entity's class and can be accessed using the TYPE_NAME variable.

from maltego.server import register_entity, MaltegoEntity

@register_entity
class HelloMaltego(MaltegoEntity):
    pass

print(HelloMaltego.TYPE_NAME)
>>maltego.HelloMaltego

Defining Properties

To add properties to an entity's definition, annotate them as class variables:

from maltego.server import register_entity, MaltegoEntity, MaltegoEntityProperty

@register_entity
class HelloMaltego(MaltegoEntity):
    str_property: str = "Hello World"

The annotation defines the properties data type. In this case a string property is used. Additional available types are:

  • float
  • int
  • boolean
  • datetime.datetime
  • datetime.date
  • daterange
  • MaltegoEntity

Defining Property Metadata

Additional configuration of entity properties can be done by using MaltegoEntityProperty or MEF functions.

from maltego.server import register_entity, MaltegoEntity, MaltegoEntityProperty, MEF

@register_entity
class HelloMaltego(MaltegoEntity):
    str_property: str = MaltegoEntityProperty(
        display_name="String",
        sample_value="Hello World",
        description="String Test Property",
    )

    int_property: int = MEF(
        display_name="Integer",
        sample_value=42,
        description="Integer Test Property",
    )

Entity Configuration

The code snippet below shows an example of how entities can be configured using the MaltegoEntityConfig class.

from maltego.server import register_entity, MaltegoEntity, MaltegoEntityProperty, MaltegoEntityConfig

@register_entity
class HelloMaltego(MaltegoEntity):
    Config = MaltegoEntityConfig(
        value_property="str_property",
        icon_resource="Phrase",
        display_name="HelloMaltego"
    )
    str_property: str = MaltegoEntityProperty(
        display_name="String",
        sample_value="Hello World",
        description="String Test Property",
    )

In this example, we define the following configurations for our entity:

  • `value_property`: This is the property that will be used as the display value for the entity in the Maltego Client.
  • `icon_resource`: This defines the icon that will be used to display the entity in the Maltego Client.
  • `display_name`: This is the display name that will be used for the entity in the Maltego Client's entity palette.

Accessing Entity Values

To retrieve the main value of an entity, use entity.value, which returns the value of the property specified as value_property in the configuration. Alternatively, you can directly access the property by its name, such as entity.str_property.

# Accessing the main entity value
main_value = entity.value

# Directly accessing the property
string_value = entity.str_property

Entity Inheritance

It is also possible to inherit from entities to re-use transforms available for them while extending the existing properties.

In the following example we want to implement a Employee entity and extend it with a employee id:

from maltego.server import register_entity, MaltegoEntity, MaltegoEntityProperty, MaltegoEntityConfig
from maltego.entities import Person

@register_entity
class AcmeEmployee(Person):
    employee_id: int = MaltegoEntityProperty(
        display_name="Employee ID",
        sample_value=1234,
        description="A unique ID for each employee",
    )

Now we can define a transform to either accept a AcmeEmployee entity or a Person entity and run both transforms on our newly defined entity:

from maltego.server import register_transform, MaltegoGraph, MaltegoContext, TransformSetting
from maltego.entities import Phrase

@register_transform
async def transform_employee(input_entity: AcmeEmployee):
    return Phrase(str(input_entity.employee_id))

@register_transform
async def transform_person(input_entity: Person):
    return Phrase(str(input_entity.fullname))

The first transforms returns the employee id as a Phrase while the second one returns the person full name as a Phrase

Entity-Typed Properties

Entities can have properties that are themselves other entities. This allows building composite structures where an entity can contain nested entities.

For a dedicated guide to composed entities, transform flags, and additional examples, see the "Composed Entities" article in this same folder.

As of 2026-04-27, this pattern is supported only in the Maltego Graph Browser client. Maltego Desktop does not support composed entities.

Entity-typed properties are defined using the MaltegoEntityProperty (MEF) function, the same as simple properties. When using entity types, a link is automatically created between the parent entity and the nested entity. The link can be customized with LinkProperties.

Example:

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

@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",
        description="The owner of this account.",
        matching_rule=MATCHING_RULE_LOOSE,
        link_properties=LinkProperties(is_reversed=False, label="To Owner"),
    )

    alias: List[Alias] = MEF(
        name="alias",
        display_name="Aliases",
        description="A list of aliases.",
        matching_rule=MATCHING_RULE_LOOSE,
    )

    uid: UniqueIdentifier = MEF(
        name="uid",
        display_name="UID",
        description="A unique identifier for the account.",
    )

    profile_image: Image = MEF(
        name="profile_image",
        display_name="Profile Image",
        description="The profile image of the account.",
    )

When working with entity-typed properties, it is possible to configure how values and display names are extracted from the property by using the value_key and display_key options in the entity's Config.

  • value_property specifies which property on the entity holds the main value.
  • value_key (optional) defines a specific property name inside the entity-typed property to use as the main value.
  • display_property defines which property is shown in the graph.
  • display_key (optional) defines a specific property from the entity-typed property to use as display.

If no value_key or display_key is given, the entity-typed property's value is used by default.

Assigning entity-typed properties is done by directly setting the attribute or using the set_property method.

# Assign a single entity
affiliation.person = Person("John Doe")

# Assign a list of entities
affiliation.set_property("alias", [Alias("johndoe42"), Alias("john.d")])

Accessing properties works the same way as normal attributes.

print(affiliation.person.fullname)

Dynamic Properties and Type Handling

Dynamic properties are properties that are not defined in the entity schema but can be set at runtime using the set_property method. The SDK handles type inference and validation for dynamic properties.

Supported Types

The Maltego desktop client supports the following property types:

  • Primitives: str, int, float, bool
  • Dates: datetime.date, datetime.datetime, daterange
  • Entities: MaltegoEntity and subclasses
  • Lists: Any list of the above types (e.g., List[str], List[int], List[MaltegoEntity])

Unsupported types include dict, tuple, set, and lists containing unsupported types (e.g., List[dict]).

Type Inference (Default Behavior)

When you set a dynamic property without specifying a type, the SDK infers the type from the value:

from maltego.entities import Phrase

entity = Phrase("Example")

# Integer property - type inferred as int
entity.set_property("count", 42, "Count")

# String property - type inferred as str
entity.set_property("name", "John Doe", "Name")

# List property - type inferred as List[int]
entity.set_property("scores", [85, 90, 95], "Scores")

# Float property - type inferred as float
entity.set_property("price", 19.99, "Price")

If you provide an unsupported type, the SDK will convert it to a string and log a warning:

# This will log a warning and convert to string
entity.set_property("metadata", {"key": "value"}, "Metadata")
# Result: property value becomes "{'key': 'value'}" (string)

Explicit Type Coercion with property_type

Use the property_type parameter when you need explicit control over type coercion:

from typing import List
from maltego.entities import Phrase

entity = Phrase("Example")

# Convert int to string
entity.set_property("count", 42, "Count", property_type=str)
# Result: property value is "42" (string)

# Convert string to int
entity.set_property("age", "25", "Age", property_type=int)
# Result: property value is 25 (int)

# Explicitly set list type
entity.set_property("tags", ["python", "maltego"], "Tags", property_type=List[str])

When type coercion fails, the SDK falls back to converting the value to a string:

# This will fail to coerce and fall back to string
entity.set_property("data", [1, 2, 3], "Data", property_type=dict)
# Warning logged, value converted to "[1, 2, 3]" (string)

Best Practices for Dynamic Properties

1. Use Supported Types

Always prefer using supported primitive types:

# Good - uses supported types
entity.set_property("count", 42, "Count")
entity.set_property("tags", ["tag1", "tag2"], "Tags")

# Avoid - uses unsupported types
entity.set_property("config", {"key": "value"}, "Config")  # Will be converted to string

2. Serialize Complex Data

If you need to store complex data structures, serialize them explicitly:

import json

# Explicitly serialize to JSON string
config = {"api_key": "abc123", "endpoint": "https://api.example.com"}
entity.set_property("config", json.dumps(config), "Config")

3. Create Separate Entities for Complex Data

For list of structured data, create separate entities instead:

from maltego.entities import URL

# Don't do this - list of dicts not supported
results = [
    {"title": "Result 1", "url": "https://example.com/1"},
    {"title": "Result 2", "url": "https://example.com/2"}
]

# Do this instead - create separate entities
url_entities = []
for result in results:
    url_entity = URL(result["url"])
    url_entity.set_property("title", result["title"], "Title")
    url_entities.append(url_entity)

return url_entities

4. Use Defined Properties for Frequently Used Properties

For properties used frequently with specific types, define them in your entity class:

from maltego.server import register_entity, MaltegoEntity, MEF

@register_entity
class MyEntity(MaltegoEntity):
    # Defined property with type annotation
    count: int = MEF(display_name="Count", default_value=0)
    tags: List[str] = MEF(display_name="Tags", default_value=[])

Using Entities

Entities support various visual features including links, overlays, notes, and display fields.

See the "Entity Features (Overlays, Links, Notes)" article in this same folder for the complete guide covering:

  • Overlays (color, text, image indicators)
  • Link customization (style, color, thickness, labels)
  • Display fields (HTML and Markdown)
  • Entity notes and weights

Full Quickstart Example File

The template project generated by maltego-transforms start my_project includes a runnable example file at transforms/quickstart_example.py that demonstrates every pattern covered in this article: entity definitions with inheritance, single/list/graph inputs, all transform setting types, date ranges, and streaming output. Its full contents are reproduced below.

import asyncio
from typing import Any, Dict, List, Optional

from maltego.entities import Person, Phrase
from maltego.model.context import MaltegoContext
from maltego.model.entity import MaltegoEntity, MaltegoEntityConfig
from maltego.model.entity.property import MaltegoEntityProperty as MEF
from maltego.server import (
    MaltegoGraph,
    MaltegoServerSettings,
    ServerHTTPSettings,
    TransformSetting,
    daterange,
    register_entity,
    register_transform,
    run_server,
)


@register_entity
class Number(MaltegoEntity):
    """A simple numeric entity for demonstration."""

    TYPE_NAME = "maltego.Number"

    Config = MaltegoEntityConfig(
        value_property="number",
        display_property="number",
        display_name="Number",
        display_name_plural="Numbers",
        description="An integer number",
        icon_resource="hashtag",
    )

    number: int = MEF(name="number", display_name="Number", value=42)


@register_entity
class StaffMember(Person):
    """
    Demonstrates entity inheritance with custom configuration.
    Extends Person with an employee ID and custom display settings.
    """

    TYPE_NAME = "maltego.StaffMember"

    Config = MaltegoEntityConfig(
        value_property="person.fullname",
        display_property="person.fullname",
        display_name="Acme Staff Member",
        display_name_plural="Acme Staff Members",
        description="A person who works at Acme corp.",
        icon_resource="License",
    )

    employee_id: str = MEF(
        name="employee_id",
        display_name="Employee ID",
        value="-1",
        sample_value="1234",
        description="A unique ID for each employee",
    )


TRANSFORM_SET = "New Maltego Integration"


@register_transform(
    display_name="Single Entity Demo [New Maltego Integration]",
    description="Demonstrates single entity input and output",
    transform_set=TRANSFORM_SET,
)
async def single_entity_demo(
    input_entity: Phrase, context: MaltegoContext
) -> Optional[Phrase]:
    """
    Simplest transform: takes one entity, returns one entity.

    If multiple entities are selected, the transform runs concurrently on each.
    """
    value = input_entity.value
    if not value:
        return None

    return Phrase(f"Processed: {value}")


@register_transform(
    display_name="Sum Numbers [New Maltego Integration]",
    description="Takes multiple Number entities and returns their sum",
    transform_set=TRANSFORM_SET,
)
async def sum_demo_numbers(
    entities: List[Number], context: MaltegoContext
) -> Optional[Number]:
    """
    Demonstrates list input - transform receives multiple entities at once.
    """
    if not entities:
        return None

    total = sum(int(entity.number or 0) for entity in entities)
    context.log.inform(f"Sum of {len(entities)} numbers: {total}")

    return Number(total)


@register_transform(
    display_name="Graph Info [New Maltego Integration]",
    description="Shows information about the selected graph",
    transform_set=TRANSFORM_SET,
)
async def graph_info(graph: MaltegoGraph, context: MaltegoContext) -> Phrase:
    """
    Demonstrates graph input - access to entities AND links.
    """
    entity_count = len(graph.entities)
    link_count = len(graph.links)

    context.log.inform(f"Graph has {entity_count} entities and {link_count} links")

    result = Phrase(f"Graph: {entity_count} entities, {link_count} links")
    result.set_property("entity_count", entity_count, display_name="Entities")
    result.set_property("link_count", link_count, display_name="Links")

    return result


@register_transform(
    display_name="Graph Numbers Only [New Maltego Integration]",
    description="Only accepts graphs containing Number entities",
    transform_set=TRANSFORM_SET,
)
async def graph_demo_numbers_only(
    graph: MaltegoGraph[Number], context: MaltegoContext
) -> Phrase:
    """
    Demonstrates typed graph input - only accepts graphs of specific entity type.
    """
    total = sum(int(entity.number or 0) for entity in graph.entities)
    return Phrase(f"Sum from graph: {total}")


@register_transform(
    display_name="All Setting Types Demo [New Maltego Integration]",
    description="Demonstrates all available transform setting types",
    transform_set=TRANSFORM_SET,
    settings=[
        # String
        TransformSetting(
            name="str_setting",
            display_name="Text Input",
            type="string",
            default_value="default text",
            popup=True,
        ),
        # Integer
        TransformSetting(
            name="int_setting",
            display_name="Number Input",
            type="int",
            default_value=10,
            popup=True,
        ),
        # Float
        TransformSetting(
            name="double_setting",
            display_name="Decimal Input",
            type="double",
            default_value=3.14,
            popup=True,
        ),
        # Boolean
        TransformSetting(
            name="bool_setting",
            display_name="Enable Feature",
            type="boolean",
            default_value=True,
            popup=True,
        ),
        # Date
        TransformSetting(
            name="date_setting",
            display_name="Select Date",
            type="date",
            popup=True,
        ),
        # Auth setting (global, shared across transforms)
        TransformSetting(
            name="api_key",
            display_name="API Key",
            auth=True,
            is_global=True,
            optional=True,
            popup=True,
        ),
    ],
)
async def all_settings_demo(
    input_entity: Phrase,
    settings: Dict[str, Any],
    context: MaltegoContext,
) -> Phrase:
    """
    Shows how to access different setting types.
    """
    text = settings.get("str_setting", "")
    number = settings.get("int_setting", 0)
    decimal = settings.get("double_setting", 0.0)
    enabled = settings.get("bool_setting", False)
    date = settings.get("date_setting")
    api_key = settings.get("api_key", "")

    result = Phrase(f"Settings received for: {input_entity.value}")
    result.set_property("text", text, display_name="Text")
    result.set_property("number", number, display_name="Number")
    result.set_property("decimal", decimal, display_name="Decimal")
    result.set_property("enabled", str(enabled), display_name="Enabled")
    result.set_property("date", str(date) if date else "Not set", display_name="Date")
    result.set_property(
        "has_api_key", "Yes" if api_key else "No", display_name="Has API Key"
    )

    return result


@register_transform(
    display_name="List Settings Demo [New Maltego Integration]",
    description="Demonstrates list-type settings",
    transform_set=TRANSFORM_SET,
    settings=[
        TransformSetting(
            name="tags",
            display_name="Tags",
            type=TransformSetting.Types.str_list,
            default_value=["tag1", "tag2"],
            popup=True,
        ),
        TransformSetting(
            name="ports",
            display_name="Ports",
            type=TransformSetting.Types.int_list,
            default_value=[80, 443, 8080],
            popup=True,
        ),
    ],
)
async def list_settings_demo(
    input_entity: Phrase,
    settings: Dict[str, Any],
    context: MaltegoContext,
) -> List[Phrase]:
    """
    Demonstrates list-type settings.
    """
    tags = settings.get("tags", [])
    ports = settings.get("ports", [])

    results = []
    for tag in tags:
        results.append(Phrase(f"Tag: {tag}"))
    for port in ports:
        results.append(Phrase(f"Port: {port}"))

    return results


@register_transform(
    display_name="Date Range Demo [New Maltego Integration]",
    description="Demonstrates date range settings",
    transform_set=TRANSFORM_SET,
    settings=[
        TransformSetting(
            name="time_window",
            display_name="Time Window",
            type=TransformSetting.Types.datetime_range,
            default_value=daterange(date_range=daterange.Ranges.last_24_hours),
            popup=True,
        ),
    ],
)
async def daterange_demo(
    input_entity: Phrase,
    settings: Dict[str, Any],
    context: MaltegoContext,
) -> Phrase:
    """
    Demonstrates date range settings with relative values.
    """
    time_window = settings.get("time_window")
    result = Phrase(f"Date range for: {input_entity.value}")
    result.set_property(
        "time_window",
        str(time_window) if time_window else "Not set",
        display_name="Time Window",
    )
    return result


@register_transform(
    display_name="Get Employee ID [New Maltego Integration]",
    description="Works only on StaffMember entities",
    transform_set=TRANSFORM_SET,
)
async def get_staff_member_id(input_entity: StaffMember) -> Phrase:
    """
    Demonstrates transform on inherited entity type.
    Only available for StaffMember entities.
    """
    return Phrase(f"Employee ID: {input_entity.employee_id}")


@register_transform(
    display_name="Get Person Name [New Maltego Integration]",
    description="Works on Person and any entity inheriting from Person",
    transform_set=TRANSFORM_SET,
)
async def get_person_name(input_entity: Person) -> Phrase:
    """
    Works on Person AND StaffMember (since StaffMember inherits from Person).
    """
    return Phrase(f"Name: {input_entity.fullname}")


@register_transform(
    display_name="Streaming Demo [New Maltego Integration]",
    description="Yields results one at a time",
    transform_set=TRANSFORM_SET,
)
async def streaming_demo(input_entity: Phrase, context: MaltegoContext):
    """
    Demonstrates streaming output using async generator.
    """
    for i in range(5):
        context.log.inform(f"Processing item {i + 1}/5")
        yield Phrase(f"Result {i + 1}: {input_entity.value}")
        await asyncio.sleep(0.5)  # Simulate slow processing


if __name__ == "__main__":
    import os

    key_file = os.path.join(os.path.dirname(__file__), "../server.key")
    cert_file = os.path.join(os.path.dirname(__file__), "../server.crt")
    server_settings = MaltegoServerSettings(
        server_name="Maltego Transform Server",
        ns="acme",
        author="Acme",
        http_settings=ServerHTTPSettings(
            http_addr="127.0.0.1",
            http_port=8080,
            protocol="https",
            cert_key=key_file,
            cert_file=cert_file,
        ),
    )
    run_server(
        settings=server_settings,
        log_level="INFO",
    )

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