Construction — site operations, safety & documents
The problem. A construction site runs on documents as much as data: drawings, permits, method statements, inspection sign-offs — each belonging to a specific zone of the site, each needing to be on hand the moment an inspector or crew lead asks. On top of that, equipment utilisation and safety incidents need tracking. The challenge is keeping the paperwork, the machines, and the incidents all tied to the right part of the site.
This scenario pairs document management with safety events on a modelled site.
Set up demo data
New workspace? Run this once (Python) to create the site graph and a crane-utilisation series. (The drawing upload in step 1 needs a local file — any PDF works.) Safe to re-run.
import datahub_sdk, numpy as np, pandas as pd
client = datahub_sdk.DataHubClient.from_env()
client.resources.create(
[datahub_sdk.Resource(external_id=x, name=x, labels=[lbl]) for x, lbl in
[("site_harbour_tower", "Site"), ("zone_level_12", "Zone")]],
[datahub_sdk.RelForm.by_external_ids("site_harbour_tower", "zone_level_12", "contains")])
client.timeseries.create([datahub_sdk.TimeSeries(external_id="crane_02_hours", name="Crane 02 hours", unit="h", value_type="float")])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=14, freq="1d")
client.timeseries.insert_from_lists(timestamps=idx, values=np.random.uniform(4, 9, 14), ts="crane_02_hours")
1. Model the site and attach its documents
A site contains zones. Each zone's drawings and permits live in its folder, tagged so a crew can find the current revision instantly. See Attach files.
- Java
- Python
- Rust
ResourceForm site = new ResourceForm();
site.setExternalId("site_harbour_tower");
site.setName("Harbour Tower");
site.setLabels(List.of("Site"));
ResourceForm zone = new ResourceForm();
zone.setExternalId("zone_level_12");
zone.setName("Level 12");
zone.setLabels(List.of("Zone"));
RelForm contains = new RelForm();
contains.setName("contains");
contains.setFromExternalId("site_harbour_tower");
contains.setToExternalId("zone_level_12");
client.resources().create(List.of(site, zone), List.of(contains));
byte[] drawing = Files.readAllBytes(Path.of("level_12_rev_c.pdf"));
client.files().upload(
FileUploadRequest.builder()
.path("harbour_tower/level_12/drawings/structural_rev_c.pdf")
.content(drawing)
.contentType("application/pdf")
.externalId("drawing_level_12_structural_rev_c")
.description("Level 12 structural — revision C")
.build());
import datahub_sdk
client.resources.create(
[datahub_sdk.Resource(external_id="site_harbour_tower", name="Harbour Tower", labels=["Site"]),
datahub_sdk.Resource(external_id="zone_level_12", name="Level 12", labels=["Zone"])],
[datahub_sdk.RelForm.by_external_ids("site_harbour_tower", "zone_level_12", "contains")])
client.files.upload_file(datahub_sdk.FileUpload(
path="level_12_rev_c.pdf",
destination_path="/harbour_tower/level_12/drawings/",
external_id="drawing_level_12_structural_rev_c",
name="structural_rev_c.pdf"))
use dataplatform_rust_sdk::resources::Resource;
use dataplatform_rust_sdk::relations::RelForm;
use dataplatform_rust_sdk::files::FileUpload;
let mut site = Resource::new();
site.external_id = "site_harbour_tower".into();
site.name = "Harbour Tower".into();
site.labels = Some(vec!["Site".into()]);
let mut zone = Resource::new();
zone.external_id = "zone_level_12".into();
zone.name = "Level 12".into();
zone.labels = Some(vec!["Zone".into()]);
api.resources.create(
vec![site, zone],
vec![RelForm::by_external_ids("site_harbour_tower", "zone_level_12", "contains")]).await?;
let mut upload = FileUpload::new_with_destination_path(
"level_12_rev_c.pdf", "/harbour_tower/level_12/drawings/");
upload.external_id = "drawing_level_12_structural_rev_c".into();
upload.name = "structural_rev_c.pdf".into();
api.files.upload_file(upload).await?;
2. Record a safety incident against its zone
A near-miss or incident becomes a safety_incident event, tied to the zone via
metadata so the HSE team can see a zone's full history and trend.
- Java
- Python
- Rust
EventModel incident = new EventModel();
incident.setExternalId("safety_incident_l12_" + System.currentTimeMillis());
incident.setType("safety_incident");
incident.setSubType("near_miss");
incident.setStatus("open");
incident.setMetadata(Map.of("zone", "zone_level_12", "category", "dropped_object"));
incident.setEventTime(ZonedDateTime.now());
client.events().create(List.of(incident));
client.events.create([datahub_sdk.Event(
external_id=f"safety_incident_l12_{int(pd.Timestamp.now().timestamp())}",
type="safety_incident", sub_type="near_miss", status="open",
event_time=pd.Timestamp.now(tz="UTC"),
metadata={"zone": "zone_level_12", "category": "dropped_object"})])
use dataplatform_rust_sdk::events::Event;
use chrono::Utc;
let mut incident = Event::new(format!("safety_incident_l12_{}", Utc::now().timestamp()));
incident.r#type = Some("safety_incident".into());
incident.sub_type = Some("near_miss".into());
incident.status = Some("open".into());
incident.add_metadata("zone".into(), "zone_level_12".into());
incident.add_metadata("category".into(), "dropped_object".into());
incident.set_event_time(Utc::now());
api.events.create(&vec![incident]).await?;
3. Track equipment use
Crane and hoist utilisation are series (crane_02_hours,
hoist_north_cycles); roll them up to see idle plant and plan hire returns with
Query & aggregate.
See the result
The recorded near-miss lands as an event against its zone:
safety_incident_l12_… → open (dropped-object near-miss on zone_level_12)
See also
- Attach files to assets — drawings, permits, sign-offs.
- Turn readings into events — recording incidents.
- Model assets as a graph — site/zone/equipment structure.