Skip to main content

The Speckle Data Model

Speckle uses an object-based approach rather than a file-based one. This is a fundamental shift from traditional CAD/BIM workflows.

Traditional (File-Based)

  • Data locked in files (.rvt, .3dm, .ifc)
  • Full file transfer required
  • Version control through file copies
  • Large files, slow transfers

Speckle (Object-Based)

  • Data is individual objects
  • Incremental transfers (only changes)
  • Built-in version control
  • Fast, efficient streaming

Key Concepts

1. Objects

Everything in Speckle is an object that inherits from the Base class.
from specklepy.objects import Base
from specklepy.objects.geometry import Point

# All Speckle objects inherit from Base
point = Point(x=10, y=20, z=5)

# Base allows dynamic properties (added after creation)
custom = Base()
custom.name = "My Object"
custom.data = [1, 2, 3]
custom.nested = Base()
custom.nested.value = 42
Objects are immutable once sent - their hash (object ID) uniquely identifies their content.

Static vs Dynamic Properties

Speckle objects support both static (typed) and dynamic (untyped) properties. Static properties are defined in the class with type hints and provide IDE autocomplete, while dynamic properties can be added at runtime for application-specific data.
from specklepy.objects.geometry import Point

# Static properties (defined in class)
point = Point(x=1.0, y=2.0, z=3.0)

# Dynamic properties (added at runtime)
point.custom_id = "POINT_001"
point.metadata = {"author": "Alice"}
Learn more about Static vs Dynamic Properties including type checking, required vs optional properties, and how to work with both types.

2. Projects, Models, and Versions

Speckle organizes data in a three-level hierarchy:
Project
  └─ Model
      └─ Version
          └─ Object (Root object + children)
Projects are top-level containers for related work.
  • Contain multiple models
  • Have members with roles (owner, contributor, reviewer)
  • Unique ID and name
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput

project = client.project.create(ProjectCreateInput(
    name="Office Building Renovation",
    description="Main project for the renovation"
))
project_id = project.id
Models are branches within a project, representing different variants or disciplines.
  • Examples: “site-boundary”, “design-option-a”, “structural”, “MEP”
  • Each model has its own version history
from specklepy.core.api.inputs.model_inputs import CreateModelInput

model = client.model.create(CreateModelInput(
    project_id=project_id,
    name="Structural Model",
    description="Structural analysis model"
))
model_id = model.id
Versions are snapshots of data at a point in time.
  • Immutable - cannot be changed after creation
  • Reference a root object by its ID (hash)
  • Have a message describing the changes
  • Include metadata (author, timestamp, source application)
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="Updated beam sizing"
)
version = client.version.create(version_input)

3. Object IDs (Hashes)

Every object has a unique object ID (also called a hash) that’s deterministically generated from its content.
from specklepy.objects.geometry import Point

# Same content = same ID
p1 = Point(x=1, y=2, z=3)
p2 = Point(x=1, y=2, z=3)

print(p1.get_id())  # "abc123..."
print(p2.get_id())  # "abc123..." - same!

# Different content = different ID
p3 = Point(x=1, y=2, z=4)
print(p3.get_id())  # "xyz789..." - different!
This enables:
  • Deduplication - Identical objects stored once
  • Incremental sync - Only new/changed objects transferred
  • Content addressing - Objects referenced by their hash

4. Transports

Transports are the mechanism for moving objects between locations.
  • ServerTransport
  • SQLiteTransport
  • MemoryTransport
Communicates with Speckle Server
from specklepy.transports.server import ServerTransport

transport = ServerTransport(
    project_id=project_id,
    client=client
)

5. Operations: Send and Receive

The operations module provides the core functions for data transfer.

send()

Serializes an object and sends it via transports:
from specklepy.api import operations
from specklepy.transports.server import ServerTransport

transport = ServerTransport(stream_id=project_id, client=client)

# Send returns the object ID
object_id = operations.send(
    base=my_object,
    transports=[transport]
)
send() automatically handles:
  • Object serialization
  • Child object detection
  • Chunking large arrays
  • Deduplication
  • Parallel uploads

receive()

Receives an object by its ID:
# Receive from server
received_object = operations.receive(
    obj_id=object_id,
    remote_transport=transport
)

# Data is automatically reconstructed
print(received_object.some_property)

6. Serialization

Objects are serialized to JSON for storage and transport:
from specklepy.api import operations

# Serialize to JSON string
json_string = operations.serialize(my_object)

# Deserialize from JSON string
reconstructed_object = operations.deserialize(json_string)
Most developers won’t need to know the serialization and deserialization functions directly. The send() and receive() operations handle serialization and deserialization implicitly. Use these functions only when you need custom workflows like saving objects to files or working with JSON representations directly.
Serialization automatically handles:
  • Nested objects (converted to references)
  • Large arrays (chunked and detached)
  • Type information (preserved via speckle_type)
  • Units (tracked and converted)

7. Orphaned Objects

Objects sent to the server but not referenced by any version are called “orphaned” or “zombie” objects. While they exist in the database and have object IDs, they are effectively unreachable and cannot be browsed or retrieved through the normal Speckle UI or workflows.
# This object is sent but becomes orphaned
object_id = operations.send(base=my_object, transports=[transport])
# ⚠️ Without creating a version, this object is a "zombie"

# Make it reachable by creating a version
version = client.version.create(CreateVersionInput(
    project_id=project_id,
    model_id=model_id,
    object_id=object_id,  # Now reachable via this version
    message="My data"
))
Key Points:
  • Orphaned objects remain in the database but are not discoverable
  • You can still retrieve them if you have the object ID: operations.receive(obj_id=object_id)
  • Always create a version after sending important data
  • The server may eventually clean up orphaned objects (implementation-dependent)

The Object Lifecycle

Here’s how objects flow through a typical workflow:

Send Workflow

Receive Workflow

1

Create

Create Python objects using specklepy classes
point = Point(x=1, y=2, z=3)
2

Send

Send objects via a transport
object_id = operations.send(base=point, transports=[transport])
3

Version

Create a version referencing the object
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="Description of changes"
)
version = client.version.create(version_input)
4

Get Version

Retrieve a version to access its data
# Get the version by ID
version = client.version.get(project_id=project_id, version_id=version.id)

# Access the referenced object ID
object_id = version.referencedObject
5

Receive

Receive objects by ID
received = operations.receive(obj_id=object_id, remote_transport=transport)

The Type System

Speckle uses a type system to preserve object types across different platforms:
from specklepy.objects.geometry import Point

point = Point(x=1, y=2, z=3)

# Every object has a speckle_type
print(point.speckle_type)  # "Objects.Geometry.Point"

# This allows proper deserialization
received = operations.receive(...)  # Comes back as Point, not Base

Built-in Types

  • Point, Vector, Line, Polyline, Arc, Circle
  • Curve, Polycurve, Mesh, Surface
  • Plane, Box, Region, Spiral
  • Ellipse, PointCloud, ControlPoint
from specklepy.objects.geometry import Point, Line, Mesh, Plane
  • DataObject - Generic data container with name, properties, and displayValue
  • BlenderObject - Blender-specific data object
  • QgisObject - QGIS-specific data object
from specklepy.objects.data_objects import DataObject, BlenderObject
  • Text - Text annotations with alignment and positioning
from specklepy.objects.annotation.text import Text
  • Interval - Numeric interval with start and end values
from specklepy.objects.primitive import Interval
  • RenderMaterial - Physically based material properties
  • LevelProxy - Level reference proxy
  • RenderMaterialProxy - Material reference proxy
from specklepy.objects.other import RenderMaterial

Custom Types

You can create your own types:
from specklepy.objects import Base

class Wall(Base, speckle_type="MyCompany.Wall"):
    height: float
    thickness: float
    material: str

wall = Wall(height=3.0, thickness=0.2, material="Concrete")
print(wall.speckle_type)  # "MyCompany.Wall"
Connector CompatibilityMost Speckle connectors will not recognize custom types when receiving data into host applications. Your custom objects will likely be ignored during conversion to native application objects.Best Practice: Include a displayValue property with mesh geometry to ensure your custom objects are at least visible in the Speckle viewer and can be converted to generic native objects with geometry:
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh

class CustomStructure(Base, speckle_type="MyCompany.Structure"):
    load_capacity: float
    material: str
    displayValue: list[Mesh]  # Makes it visible and convertible

# Create with display value
structure = CustomStructure(
    load_capacity=5000.0,
    material="Steel"
)
structure.displayValue = [mesh_representation]  # Add after creation
See Display Values for more details on making your objects visible and interoperable.

Local Cache

specklepy maintains a local SQLite cache:
  • Location:
    • Windows: %APPDATA%\Speckle
    • macOS: ~/.config/Speckle
    • Linux: ~/.local/share/Speckle
  • Purpose:
    • Cache received objects for faster access
    • Enable offline work
    • Reduce network transfers
  • Automatic: Used by default in send() and receive()
# By default, objects are cached locally
object_id = operations.send(
    base=my_object,
    transports=[server_transport],
    use_default_cache=True  # Default
)

# Skip cache if needed
object_id = operations.send(
    base=my_object,
    transports=[server_transport],
    use_default_cache=False
)

Next Steps

Now that you understand the concepts, explore the API and apply your knowledge: