Skip to main content
This guide teaches building Speckle object graphs inside Grasshopper — computed metadata, nested collections, and local inspection — without publishing to a server first. When to use it: enrich Grasshopper geometry with SDK types before or alongside the connector. Read next: Grasshopper Objects for connector basics, then Work with model objects.

When to use the SDK vs connector components

The Speckle Grasshopper connector (Publish, Load, passthrough nodes) is the right tool for moving data to and from Speckle. SpeckleSharp in a C# Script is the right tool when you need:
  • Custom analysis that connector nodes do not provide
  • Extracting or restructuring model data programmatically
  • Creating reports with computed fields derived from geometry
  • Combining Speckle data with Grasshopper logic in one pass
  • Prototyping workflows before turning them into a plugin or automation
Use connector nodes for transport. Use the SDK for construction, enrichment, and analysis. The SDK does not replace Publish and Load — it complements them.
Grasshopper Speckle componentsSpeckle.Sdk in a C# Script
RoleMove dataConstruct, enrich, analyse data
ExamplesPublish, Load, passthrough params, DeconstructDataObject + derived properties, Collection hierarchy, Flatten

When connector nodes are enough

Passthrough nodes move and lightly edit data on wires. They do not execute graph-wide computation, derive per-element metadata from geometry, or assemble nested collections from grouping rules. Deconstruct inspects one param at a time; Speckle.Sdk constructs and analyses the whole graph; Publish exchanges it.

Ecosystem progression

Developer Nodes     →  SDK (this guide)  →  Publish / Load
inspect             construct, enrich     exchange
LayerToolRole
InspectDeveloper Nodes — DeconstructSee what is on a wire; one object at a time
ConstructSpeckle.Sdk in C# Scriptthis guideBuild graphs, computed metadata, hierarchy, traverse at scale
ExchangePublish / LoadMove data to and from Speckle — Grasshopper connector; not taught in the main arc
Solid lines = main guide. Dotted = ecosystem / appendix only.
Grasshopper C# Script component with NuGet package references

Mission and when you need the SDK

Passthrough nodes move geometry and attach static attributes. They do not run graph-wide computation or build nested structures from rules. The use cases above cover when to reach for Speckle.Sdk in a C# Script.
The same workflow applies to room footprints (floorArea, occupancy, carbonEstimate, qaStatus) or any case where per-element computed fields do not scale on wires.

Prerequisites

  • Rhino 8 and a Grasshopper C# Script component (Maths > Script)
  • Familiarity with Developer Nodes — especially Deconstruct for the inspect layer
  • Awareness that Publish exists on the connector (exchange layer) — not required to complete this exercise
  • DataObject and Collection — the containers this guide builds

Set up the C# Script component

Add a C# Script to your canvas and declare these inputs and outputs. Geometry comes from Grasshopper; Speckle-facing metadata is computed in the script, not wired in as static lists.
In / outTypeRole
ptsList<Point3d>Pier or footing locations
gridLinesList<Line>Closest-line index for nearestGridLine
capacitydoubleDenominator for utilization
elementCountintCount after Flatten
summarystringReport of computed fields
statusstringValidation message
Do not add category or loads as dumb inputs — those values are derived in code.

Install packages and minimal bootstrap

Install NuGet packages from the C# Script editor (box icon > Specify Package(s)). Copy the #r lines from the Speckle.Sdk and Speckle.Objects Script & Interactive tabs (version numbers will match the latest release):
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251NuGet references
// Grasshopper C# Script — NuGet references
#r "nuget: Speckle.Sdk, 3.21.1"
#r "nuget: Speckle.Objects, 3.21.1"
Paste a minimal bootstrap once per script. It registers the object model and gives you Flatten helpers plus optional Serializeno account or IClientFactory required for the main exercise:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Bootstrap
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk;
using Speckle.Sdk.Api;

static class SpeckleBootstrap
{
    static readonly ServiceProvider Provider = new ServiceCollection()
        .AddSpeckleSdk(
            new Application("GH Enrichment", "gh-enrichment"),
            "1.0.0",
            typeof(Speckle.Objects.Geometry.Point).Assembly
        )
        .BuildServiceProvider();

    public static IOperations Operations => Provider.GetRequiredService<IOperations>();
}
The Grasshopper connector converts Rhino points the same way — see PointToSpeckleConverter.cs in speckle-sharp-connectors.

Grasshopper execution model

Grasshopper recomputes the script whenever upstream inputs change. Building a Collection of a few hundred DataObject instances is cheap — there is no network call in the main exercise. On large lists, cache the summary string in a script field and only rebuild it when pts count changes, so preview panels stay responsive. The object graph itself should still rebuild each solve so outputs stay correct.

Enrich a structural layout

Worked example: build a publish-ready structural data model from pier locations and grid lines. Inputs are geometry only; every Speckle property below is computed.
Computed propertyDerivation
distanceFromOriginpt.DistanceTo(origin)
quadrantSign of X and Y relative to origin
bearingAtan2 converted to degrees
utilizationSimplified demand / capacity (replace with upstream analysis when available)
nearestGridLineIndex of closest line in gridLines

Complete example

https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Complete example
// Grasshopper C# Script — structural layout enrichment
#r "nuget: Speckle.Sdk, 3.21.1"
#r "nuget: Speckle.Objects, 3.21.1"

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Rhino.Geometry;
using Speckle.Objects.Data;
using Speckle.Objects.Geometry;
using Speckle.Sdk;
using Speckle.Sdk.Api;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Extensions;

static class SpeckleBootstrap
{
    static readonly ServiceProvider Provider = new ServiceCollection()
        .AddSpeckleSdk(
            new Application("GH Enrichment", "gh-enrichment"),
            "1.0.0",
            typeof(Point).Assembly
        )
        .BuildServiceProvider();

    public static IOperations Operations => Provider.GetRequiredService<IOperations>();
}

private void RunScript(
    List<Point3d> pts,
    List<Line> gridLines,
    double capacity,
    ref object elementCount,
    ref object summary,
    ref object status)
{
    if (pts == null || pts.Count == 0)
    {
        status = "Provide at least one point.";
        elementCount = 0;
        summary = "";
        return;
    }

    var origin = Point3d.Origin;
    var dataObjects = pts.Select((pt, i) => BuildFooting(pt, i, origin, gridLines, capacity)).ToList();
    var root = BuildRootByQuadrant(dataObjects);

    var flat = root.Flatten().ToList();
    elementCount = flat.Count;
    summary = BuildSummary(dataObjects);
    status = "OK";
}

static DataObject BuildFooting(Point3d pt, int index, Point3d origin, List<Line> gridLines, double capacity)
{
    var dx = pt.X - origin.X;
    var dy = pt.Y - origin.Y;
    var distance = pt.DistanceTo(origin);
    var bearing = Math.Round(Math.Atan2(dy, dx) * (180.0 / Math.PI), 1);
    var demand = distance * 10.0; // stand-in; pipe real analysis from upstream when available
    var utilization = capacity > 0 ? Math.Round(demand / capacity, 3) : 0;

    var specklePoint = new Point(pt.X, pt.Y, pt.Z, Units.Meters);

    return new DataObject
    {
        name = $"Footing {index + 1}",
        properties = new Dictionary<string, object?>
        {
            ["distanceFromOrigin"] = Math.Round(distance, 3),
            ["quadrant"] = Quadrant(dx, dy),
            ["bearing"] = bearing,
            ["utilization"] = utilization,
            ["nearestGridLine"] = NearestGridLineIndex(pt, gridLines),
        },
        displayValue = new List<Base> { specklePoint },
    };
}

static Collection BuildRootByQuadrant(List<DataObject> footings)
{
    var root = new Collection { name = "Structural layout" };

    foreach (var group in footings.GroupBy(f => f.properties["quadrant"] as string ?? "Unknown"))
    {
        var quadrantCollection = new Collection { name = group.Key };
        quadrantCollection.elements.AddRange(group.Cast<Base>());
        root.elements.Add(quadrantCollection);
    }

    return root;
}

static string Quadrant(double dx, double dy)
{
    if (dx >= 0 && dy >= 0) return "NE";
    if (dx < 0 && dy >= 0) return "NW";
    if (dx < 0 && dy < 0) return "SW";
    return "SE";
}

static int NearestGridLineIndex(Point3d pt, List<Line> gridLines)
{
    if (gridLines == null || gridLines.Count == 0) return -1;

    return gridLines
        .Select((line, i) => (i, line.DistanceTo(pt, true)))
        .OrderBy(x => x.Item2)
        .First().i;
}

static string BuildSummary(List<DataObject> footings)
{
    var sb = new StringBuilder();
    sb.AppendLine($"Footings: {footings.Count}");
    sb.AppendLine($"Avg utilization: {footings.Average(f => (double)f.properties["utilization"]):F3}");
    sb.AppendLine("By quadrant:");
    foreach (var g in footings.GroupBy(f => f.properties["quadrant"]))
        sb.AppendLine($"  {g.Key}: {g.Count()}");
    return sb.ToString();
}
1

Put geometry on `displayValue`

Convert each Rhino Point3d to a Speckle Point and assign it to DataObject.displayValue. The viewer and connector expect renderable geometry here — the same field Deconstruct reads on a published param.In the script, BuildFooting creates specklePoint and sets displayValue = new List<Base> { specklePoint }. Each footing DataObject now carries geometry the viewer can render.
2

Compute properties on each `DataObject`

Store BIM-like fields in DataObject.properties, not on the root Base indexer. Derive every field from point position, grid proximity, and capacity — that is the justification for code over passthrough nodes.After this step, each footing has computed keys such as distanceFromOrigin, quadrant, bearing, utilization, and nearestGridLine in its properties dictionary.
3

Group footings into a `Collection` hierarchy

Nest footings into child collections by computed quadrant. Collection.elements mirrors how Create Collection nodes organize a canvas — except the grouping rule runs in C#.BuildRootByQuadrant returns a root Collection named Structural layout with one child collection per quadrant. The full graph is ready to traverse.
4

Inspect the graph locally

Call root.Flatten() to count every Base in the graph and wire elementCount. Build summary from aggregated computed fields. Optionally preview JSON:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
string json = SpeckleBootstrap.Operations.Serialize(root);
Use this to sanity-check property names and nesting before exchange. See Load and publish model data.
When status reads OK, elementCount matches the flattened graph and summary reports footing counts and average utilization.
You now have a publish-ready data model in memory. Success means a correct DataObject / Collection graph and a trustworthy summary. When you are ready to exchange it, use Publish on the connector (normal workflow) or Appendix A (programmatic). Do not rebuild the same graph with Collection nodes on the canvas.
The graph shape aligns with GrasshopperRootObjectBuilder — the same model Publish would pack. Connector wire casting uses SpeckleGeometryWrapperGoo.cs; Deconstruct reads the same fields via DeconstructSpeckleParam.cs.

Extend the graph

Nested collections by rule — the example already groups by quadrant. Swap the GroupBy key for nearestGridLine or a utilization band to change hierarchy without new inputs. Line geometry between pairs — add Speckle Line objects to displayValue or as sibling DataObject entries:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
var link = new DataObject
{
    name = "Brace",
    displayValue = new List<Base>
    {
        new Line
        {
            start = new Point(a.X, a.Y, a.Z, Units.Meters),
            end = new Point(b.X, b.Y, b.Z, Units.Meters),
            units = Units.Meters,
        },
    },
};
Area-like scalar from a bounding box — for closed curves or breps, derive floorArea from AreaMassProperties and store it on properties the same way as distanceFromOrigin. See Working with Geometry for mesh and unit details.

How this relates to the Speckle Grasshopper connector

When connector nodes are enough, use them for transport and light edits. Speckle.Sdk in a C# Script is for graph-wide computation, derived per-element metadata, and nested collections from programmatic rules. Deconstruct inspects one param at a time; Speckle.Sdk constructs and analyses the whole graph; Publish exchanges it. Both paths use the same object model (DataObject, Collection, displayValue). The connector casts native Grasshopper geometry through wrapper types; your script converts explicitly — same serialized shape on the wire. Production connector code lives in speckle-sharp-connectors under Speckle.Connectors.GrasshopperShared and Speckle.Converters.RhinoShared.

Common pitfalls

PitfallFix
Rebuilding the graph with nodes after scriptingMain arc ends at inspect; use Publish on existing wires or Appendix A
Static sliders instead of computed propertiesDerive metadata in code — that is the point of this guide
properties on wrong layerUse DataObject.properties for BIM-like fields
Treating SDK as a Publish replacementTransport = connector or appendix
Deconstruct vs FlattenDeconstruct = one wire; SDK = whole graph
v2 Speckle.Corev3 only — Speckle.Sdk + Speckle.Objects

Next Steps

Objects and Traversal

DataObject, Collection, displayValue

Automate with scripts

Send2 when ready to publish

Operations API

Send2 and Receive2 signatures

Developer Nodes

Deconstruct and inspect published params

Grasshopper Objects

Passthrough geometry and static attributes

Collections

Canvas-side collection hierarchy

Objects and traversal

DataObject, Collection, Flatten

Automate with scripts

Send2 / Receive2 when you need transport from code

Grasshopper connector

Publish and Load on the canvas

Appendices

If you already built root in the script and need to send it without the Publish component, add account bootstrap and follow Automate with scripts:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
var sendResult = await SpeckleBootstrap.Operations.Send2(
    client.ServerUrl,
    project.id,
    account.token,
    root,
    onProgressAction: null,
    cancellationToken: default
);

await client.Version.Create(
    new CreateVersionInput(sendResult.RootId, model.id, project.id, "From Grasshopper script")
);
See Load and publish model data and Authentication (Appendix C).
After Load on the connector, traverse received graphs with Find objects by property. Server access requires Authentication.
PATs, environment variables, and desktop accounts: Authentication. Only needed for appendices A and B — not for the main data-modelling exercise.
Last modified on July 10, 2026