First data product tutorial
You will define a minimal data product in the Spine language, validate it with the Dataspine CLI, and optional JSON golden tests using a conventional ingests/ / expected/ layout. This tutorial does not use dataspine.yaml, embedded Rust, or SQL—only .spine sources.
Prerequisites
- Dataspine CLI installed
- A shell and a text editor
Step 1: Project layout
Create a directory and a spine-src folder:
mkdir -p hello-product/spine-src
cd hello-product
Step 2: Define the product in main.spine
Create spine-src/main.spine with a com.example namespace:
namespace com.example {
Struct Event {
message: String,
}
Ingest EventIngest {
name: "EventIngest",
type: Event,
}
Outlet EventOutlet {
name: "EventOutlet",
type: Event,
}
flow = EventIngest.map(e -> e).publish(EventOutlet)
}
Wire name strings must stay consistent with JSON test files in later steps.
Step 3: Validate with the CLI
dataspine check --source-dir ./spine-src --root-namespace com.example
Fix any diagnostics, then produce an artifact:
mkdir -p build
dataspine compile \
--source-dir ./spine-src \
--root-namespace com.example \
--out ./build/artifact.json
See dataspine check and dataspine compile for --in-file, --lib, and auth options.
Step 4: Optional golden-test style data
To exercise the product with ordered JSON fixtures, add files whose names encode step order and ingest/outlet names:
hello-product/
├── spine-src/
│ └── main.spine
├── ingests/
│ └── 001-EventIngest.json
└── expected/
└── 001-EventOutlet.json
001-EventIngest.json (payload for the first ingest step):
{
"message": "hello world"
}
001-EventOutlet.json (outlet payload expected after that step):
{
"message": "hello world"
}
The numeric prefix sorts steps; the part after - must match name in the Spine declaration (EventIngest, EventOutlet). Run these comparisons in your own CI so regressions are caught when the flow changes.
Step 5: Add a filter
Replace the flow line with a filtered pipeline. First extend the struct and names if you want to keep tutorial copy-paste small; here we only change flow and add fields to illustrate numeric filtering:
namespace com.example {
Struct Event {
category: String,
value: Integer32,
}
Ingest EventIngest {
name: "EventIngest",
type: Event,
}
Outlet EventOutlet {
name: "EventOutlet",
type: Event,
}
flow = EventIngest.filter(e -> e.value > 10).publish(EventOutlet)
}
Example multi-step JSON for that filter:
001-EventIngest.json:{"category":"low","value":5}— filtered out002-EventIngest.json:{"category":"medium","value":15}— passes002-EventOutlet.json:{"category":"medium","value":15}— outlet after step 002
Re-run dataspine check after each edit.
What you learned
- Declaring types, Ingest / Outlet, and a
flowin Spine - Checking and compiling with
dataspineand--root-namespacealigned to yournamespace - How ingests / expected JSON files are named for golden-style checks
Further reading
- Spine language — operators, joins,
KeyValueApi, imports, types - Data product lifecycle — phases from design to iteration
- Internal binary serialization — platform encoding of values (only if you work on runtimes, codegen, or low-level tooling)