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