Skip to main content

Overview

This guide covers patterns for working with Type 1 (Custom Data) and Type 2 (Simple Model Data) - the simpler data structures you’ll encounter or create in Speckle.
from specklepy.api import operations
from specklepy.objects import Base
from specklepy.objects.geometry import Point, Mesh

# Custom data - you create everything
custom = Base()
custom.name = "My Data"
custom.values = [1, 2, 3]

# Simple model data - geometry with properties
mesh = Mesh(vertices=[...], faces=[...])
mesh.properties = {"Material": "Steel", "Thickness": 10}

Custom Data Patterns

Creating Custom Data

When you control the entire data structure:
from specklepy.objects import Base
from specklepy.objects.geometry import Point

# Create a custom survey data structure
survey = Base()
survey.name = "Site Survey 2024-01"
survey.date = "2024-01-15"
survey.surveyor = "Jane Smith"
survey.location = Point(x=500000, y=4000000, z=0)
survey.location.units = "m"

# Add measurements
survey.measurements = []
for i in range(10):
    measurement = Base()
    measurement.id = f"SP-{i:03d}"
    measurement.position = Point(x=i * 10, y=i * 5, z=2.5)
    measurement.elevation = 100 + i * 0.5
    measurement.accuracy = 0.01
    survey.measurements.append(measurement)

survey.count = len(survey.measurements)

Nested Custom Structures

Build hierarchies for complex data:
from specklepy.objects import Base

# Create a project structure
project = Base()
project.name = "Building A"
project.phases = []

# Phase 1
phase1 = Base()
phase1.name = "Foundation"
phase1.startDate = "2024-01-01"
phase1.endDate = "2024-03-01"
phase1.tasks = []

# Add tasks
for i, task_name in enumerate(["Excavation", "Formwork", "Concrete Pour"]):
    task = Base()
    task.id = f"T{i+1}"
    task.name = task_name
    task.duration = 10 + i * 5
    task.status = "planned"
    phase1.tasks.append(task)

project.phases.append(phase1)

Custom Data with Geometry

Combine your structure with geometry:
from specklepy.objects import Base
from specklepy.objects.geometry import Point, Line

# Custom road analysis data
road = Base()
road.name = "Highway 101"
road.length = 1500  # meters
road.lanes = 4

# Add geometry
road.centerline = []
for i in range(0, 1600, 100):
    point = Point(x=i, y=0, z=0)
    road.centerline.append(point)

# Add analysis results
road.analysis = Base()
road.analysis.maxSlope = 0.08
road.analysis.minRadius = 300
road.analysis.averageWidth = 14.5

# Add sections
road.sections = []
for i in range(15):
    section = Base()
    section.station = i * 100
    section.width = 14.0 + (i % 3) * 0.5
    section.elevation = 100 + i * 0.3
    road.sections.append(section)

Simple Model Data Patterns

Pattern 1: Properties Dictionary

The most common pattern for simple model data:
from specklepy.objects.geometry import Mesh

# Create mesh with properties
mesh = Mesh(
    vertices=[0, 0, 0, 10, 0, 0, 10, 10, 0, 0, 10, 0],
    faces=[4, 0, 1, 2, 3]
)
mesh.units = "m"

# Properties as dictionary
mesh.properties = {
    "Material": "Concrete",
    "Thickness": 200,
    "LoadBearing": True,
    "FireRating": "2 hour",
    "Cost": 1500.00
}

# Access properties
material = mesh.properties.get("Material")
thickness = mesh.properties.get("Thickness", 0)  # With default
Properties can be any type (string, number, bool, list). Always use .get() with defaults for safety.
Properties Dictionary vs. Direct Attributes:Use the properties dictionary when:
  • Creating data that needs to be queryable by other applications
  • Working with metadata that follows BIM/connector conventions
  • You want a clear separation between data and structure
Use direct attributes when:
  • Creating custom application-specific data structures
  • Building organizational hierarchies (e.g., obj.phases, obj.elements)
  • You need Python-level attribute access
Both approaches work with Speckle - the properties dict is just a convention that makes data more portable and searchable.

Pattern 2: Multiple Objects with Properties

Collections of geometry with metadata:
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh

def create_wall_panel(x, y, width, height, material):
    """Create a wall panel with properties."""
    mesh = Mesh(
        vertices=[
            x, y, 0,
            x + width, y, 0,
            x + width, y, height,
            x, y, height
        ],
        faces=[4, 0, 1, 2, 3]
    )
    mesh.units = "m"
    mesh.properties = {
        "Type": "Wall Panel",
        "Material": material,
        "Width": width,
        "Height": height,
        "Area": width * height
    }
    return mesh

# Create a collection
building = Base()
building.name = "Prefab Building"
building.panels = []

# Add panels
for i in range(10):
    panel = create_wall_panel(
        x=i * 3,
        y=0,
        width=3.0,
        height=2.7,
        material="Concrete" if i % 2 == 0 else "Steel"
    )
    building.panels.append(panel)

Pattern 3: Material References

Simple material system:
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh

# Define materials
materials = Base()
materials.concrete = Base()
materials.concrete.name = "Concrete C30/37"
materials.concrete.color = 0xFF808080
materials.concrete.density = 2400  # kg/m³

materials.steel = Base()
materials.steel.name = "Steel S355"
materials.steel.color = 0xFFC0C0C0
materials.steel.density = 7850  # kg/m³

# Use material references
building = Base()
building.materials = materials
building.elements = []

# Element with material reference
slab = Mesh(vertices=[...], faces=[...])
slab.units = "m"
slab.materialId = "concrete"  # Reference by ID
slab.properties = {
    "Type": "Slab",
    "Thickness": 0.2
}

building.elements.append(slab)

# Later, resolve material
def get_material(element, materials_dict):
    """Get material object for an element."""
    material_id = getattr(element, "materialId", None)
    if material_id:
        if isinstance(materials_dict, dict):
            return materials_dict.get(material_id)
        else:
            return getattr(materials_dict, material_id, None)
    return None

mat = get_material(slab, building.materials)
if mat:
    print(f"Material: {mat.name}, Density: {mat.density}")

Pattern 4: Layer Organization

Organize objects by layers:
from specklepy.objects import Base
from specklepy.objects.geometry import Line, Point

# Create layered structure
model = Base()
model.name = "Floor Plan - Level 1"

# Define layers
layers = ["Walls", "Doors", "Windows", "Furniture"]
for layer_name in layers:
    layer = Base()
    layer.name = layer_name
    layer.visible = True
    layer.locked = False
    layer.color = 0xFF000000
    layer.objects = []
    setattr(model, layer_name.lower(), layer)

# Add objects to layers
wall_line = Line(
    start=Point(x=0, y=0, z=0),
    end=Point(x=10, y=0, z=0)
)
wall_line.layer = "Walls"
model.walls.objects.append(wall_line)

# Query by layer
def get_objects_by_layer(model, layer_name):
    """Get all objects on a specific layer."""
    layer = getattr(model, layer_name.lower(), None)
    if layer and hasattr(layer, "objects"):
        return layer.objects
    return []

walls = get_objects_by_layer(model, "Walls")
print(f"Found {len(walls)} wall objects")

Pattern 5: DisplayValue for Visualization

Add display geometry to custom objects:
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh

# Parametric object (your domain logic)
column = Base()
column.speckle_type = "MyApp.Column"
column.height = 3.0
column.width = 0.3
column.depth = 0.3
column.profile = "Rectangular"

# Add display geometry for viewers
column.displayValue = Mesh(
    vertices=[
        0, 0, 0,
        0.3, 0, 0,
        0.3, 0.3, 0,
        0, 0.3, 0,
        0, 0, 3,
        0.3, 0, 3,
        0.3, 0.3, 3,
        0, 0.3, 3,
    ],
    faces=[
        4, 0, 1, 2, 3,  # Bottom
        4, 4, 5, 6, 7,  # Top
        4, 0, 1, 5, 4,  # Side 1
        4, 1, 2, 6, 5,  # Side 2
        4, 2, 3, 7, 6,  # Side 3
        4, 3, 0, 4, 7,  # Side 4
    ]
)
column.displayValue.units = "m"
Always provide displayValue for custom objects so they’re visible in Speckle viewers, even without native support.

Working with Received Simple Data

Extracting Properties

from specklepy.api import operations

obj = operations.receive(object_id, remote_transport=transport)

# Check for properties dictionary
if hasattr(obj, "properties") and isinstance(obj.properties, dict):
    # Extract specific properties
    material = obj.properties.get("Material", "Unknown")
    thickness = obj.properties.get("Thickness", 0)
    
    print(f"Material: {material}")
    print(f"Thickness: {thickness}mm")
    
    # List all properties
    for key, value in obj.properties.items():
        print(f"{key}: {value}")

Finding Objects by Property

from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule

def filter_by_property(root, property_name, property_value):
    """Find all objects with a specific property value using SDK traversal."""
    results = []
    
    # Define a rule to traverse all members and return all objects
    traverse_all_rule = TraversalRule(
        conditions=[lambda _: True],
        members_to_traverse=lambda obj: obj.get_member_names(),
        should_return_to_output=True
    )
    
    traversal = GraphTraversal([traverse_all_rule])
    
    # Traverse and filter
    for context in traversal.traverse(root):
        obj = context.current
        if hasattr(obj, "properties") and isinstance(obj.properties, dict):
            if obj.properties.get(property_name) == property_value:
                results.append(obj)
    
    return results

# Use it
steel_elements = filter_by_property(obj, "Material", "Steel")
print(f"Found {len(steel_elements)} steel elements")

Extracting Display Geometry

from specklepy.objects.graph_traversal.default_traversal import create_default_traversal_function

def get_displayable_geometry(obj):
    """
    Extract all displayValue meshes for visualization using SDK's default traversal.
    The default traversal returns objects with displayValue (atomic/viewer-selectable objects).
    """
    meshes = []
    
    traversal = create_default_traversal_function()
    
    for context in traversal.traverse(obj):
        current = context.current
        if hasattr(current, "displayValue"):
            display = current.displayValue
            if isinstance(display, list):
                meshes.extend(display)
            else:
                meshes.append(display)
    
    return meshes

# Use it
display_meshes = get_displayable_geometry(obj)
print(f"Found {len(display_meshes)} display meshes")

# Calculate total vertices
total_vertices = sum(m.vertices_count for m in display_meshes if hasattr(m, 'vertices_count'))
print(f"Total vertices: {total_vertices}")

Converting to Common Formats

To Pandas DataFrame

import pandas as pd
from specklepy.objects import Base

def properties_to_dataframe(objects):
    """Convert objects with properties to DataFrame."""
    rows = []
    
    for obj in objects:
        if hasattr(obj, "properties") and isinstance(obj.properties, dict):
            row = {
                "id": obj.id,
                "type": obj.speckle_type,
                **obj.properties  # Unpack properties
            }
            rows.append(row)
    
    return pd.DataFrame(rows)

# Use it
elements = [...]  # Your objects
df = properties_to_dataframe(elements)
print(df.head())

# Filter and analyze
steel_elements = df[df["Material"] == "Steel"]
print(f"Steel elements: {len(steel_elements)}")
print(f"Average thickness: {steel_elements['Thickness'].mean()}")

To GeoJSON

import json
from specklepy.objects.geometry import Point

def points_to_geojson(points, properties_func=None):
    """Convert Speckle points to GeoJSON."""
    features = []
    
    for point in points:
        if not isinstance(point, Point):
            continue
        
        feature = {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [point.x, point.y, point.z]
            },
            "properties": {}
        }
        
        # Add properties if function provided
        if properties_func:
            feature["properties"] = properties_func(point)
        elif hasattr(point, "properties"):
            feature["properties"] = point.properties
        
        features.append(feature)
    
    geojson = {
        "type": "FeatureCollection",
        "features": features
    }
    
    return json.dumps(geojson, indent=2)

# Use it
survey_points = [...]  # Your points
geojson = points_to_geojson(
    survey_points,
    properties_func=lambda p: {"elevation": p.z, "label": getattr(p, "label", "")}
)

# Save to file
with open("survey.geojson", "w") as f:
    f.write(geojson)

Best Practices

The SDK provides built-in traversal utilities - use them instead of writing custom traversal:
from specklepy.objects.graph_traversal.default_traversal import create_default_traversal_function

# Good - use SDK traversal
traversal = create_default_traversal_function()
for context in traversal.traverse(root):
    obj = context.current
    # Process objects...

# Avoid - custom traversal
def my_traverse(obj):  # Don't reinvent the wheel
    # ...custom traversal code...
Benefits: handles edge cases, consistent with SDK patterns, tested and maintained.
get_member_names() already filters out private members (starting with _) and methods:
# get_member_names() already excludes private members
for name in obj.get_member_names():
    value = getattr(obj, name, None)  # No need to check startswith("_")

# Only exclude specific properties you want to skip
for name in obj.get_member_names():
    if name != "displayValue":  # Skip specific property
        value = getattr(obj, name, None)
This makes traversal code cleaner and more efficient.
Make data queryable by using consistent property names:
# Good - consistent properties
mesh.properties = {
    "Material": "Steel",
    "Thickness": 10,
    "Type": "Wall"
}

# Bad - inconsistent
mesh.material = "Steel"  # Direct attribute
mesh.properties = {"thick": 10}  # Different key
Follow conventions for common properties:
# Standard names (easier to query)
properties = {
    "Material": "Concrete",  # Not "material" or "mat"
    "Type": "Slab",          # Not "ElementType" or "type"
    "Area": 100.0,           # Not "area" or "SurfaceArea"
    "Volume": 20.0,          # Not "vol"
    "Cost": 5000.0           # Not "price"
}
Make units explicit for measurements:
# Good - units clear
properties = {
    "Thickness_mm": 200,
    "Weight_kg": 150,
    "Temperature_C": 22
}

# Or use nested structure
properties = {
    "Thickness": {"value": 200, "units": "mm"},
    "Weight": {"value": 150, "units": "kg"}
}
Always include visualization geometry:
# Create custom object
custom = Base()
custom.speckle_type = "MyApp.CustomElement"
custom.parameter1 = 10
custom.parameter2 = "value"

# Add displayValue
custom.displayValue = create_mesh_representation(custom)

# Now it's visible in all Speckle viewers

Complete Example: Survey Data Pipeline

from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.transports.server import ServerTransport
from specklepy.objects import Base
from specklepy.objects.geometry import Point

# 1. Create custom survey data
survey = Base()
survey.name = "Construction Site Survey"
survey.date = "2024-01-15"
survey.coordinate_system = "UTM Zone 32N"

survey.points = []
survey_data = [
    {"id": "SP001", "x": 500100, "y": 4000100, "z": 125.5, "type": "Control"},
    {"id": "SP002", "x": 500120, "y": 4000110, "z": 125.8, "type": "Control"},
    {"id": "SP003", "x": 500150, "y": 4000150, "z": 126.2, "type": "Detail"},
]

for data in survey_data:
    point = Point(x=data["x"], y=data["y"], z=data["z"])
    point.units = "m"
    point.properties = {
        "ID": data["id"],
        "Type": data["type"],
        "Accuracy": 0.01,
        "Method": "Total Station"
    }
    survey.points.append(point)

# 2. Send to Speckle
client = SpeckleClient(host="app.speckle.systems")
client.authenticate_with_token(token)

transport = ServerTransport(stream_id=project_id, client=client)
object_id = operations.send(survey, [transport])

# 3. Create version
from specklepy.core.api.inputs.version_inputs import CreateVersionInput

version_input = CreateVersionInput(
    project_id=project_id,
    model_id=model_id,
    object_id=object_id,
    message="Initial site survey data"
)
version = client.version.create(version_input)

print(f"✓ Survey data sent: {version.id}")

# 4. Later... receive and analyze
received = operations.receive(object_id, remote_transport=transport)

control_points = [
    p for p in received.points
    if p.properties.get("Type") == "Control"
]

print(f"Control points: {len(control_points)}")
for point in control_points:
    print(f"  {point.properties['ID']}: ({point.x}, {point.y}, {point.z})")

Summary

Simple data patterns are:
  • Easy to create - Direct property access
  • Easy to query - Properties dictionary
  • Self-documenting - Clear structure
  • Portable - No application dependencies
  • Flexible - Add any properties needed
Use them for:
  • Custom analysis pipelines
  • Simple geometry exports
  • Data you control end-to-end
  • When you don’t need BIM complexity

Next Steps

BIM Data Patterns

Work with complex BIM and connector data

Data Traversal

Learn traversal techniques for any structure

Data Types

Understand the three types of data

Working with Geometry

Complete geometry guide
Last modified on January 28, 2026