KLIQ|Developers
Health, Safety & Environment

SafeScan (HSE)

Model health-and-safety incidents, near-misses, unsafe acts, and observations across projects and shipments. Advance events through a structured lifecycle, issue stop-work orders, attach root-cause analysis, broadcast safety alerts, and sync offline field data — all through a single tenant client.

Concepts

SafeScan organises HSE work around the following resources:

  • Projects — A construction, logistics, or industrial project site. Shipments belong to projects.
  • Shipments — A transport leg or cargo movement linked to a project, carrying a transport mode (road, rail, sea, barge, air, port, yard).
  • Events — The core record: an incident, near-miss, unsafe act, or general safety observation. Events carry severity, location, and a status that advances through a three-stage lifecycle.
  • Actions — Corrective or preventive tasks assigned to a user, each with a due date and priority.
  • RCA — Root-cause analysis attached to an event, modelled as a tree of why-nodes (5-Whys or similar).
  • Alerts — Safety broadcast messages derived from an event, published to a filtered audience.
  • Lessons — Lessons-learned entries that can be linked to an alert or reference an external source.
  • Media — Photos, videos, or audio clips attached to an event via a signed-upload flow.
  • Sync — Offline-first delta sync so field devices can push queued operations and pull server changes since a known sequence number.
Project ──┬── Shipment
          └── Event ──┬── Action (corrective task)
                      ├── RCA (why-tree)
                      ├── Alert → Lesson
                      └── Media (photo / video / audio)

Event lifecycle

Events move forward through three statuses. Use hseEvents.transition to advance them.

StatusDescription
openEvent reported; investigation not yet started
under_investigationRoot-cause analysis or actions in progress
closedAll actions resolved; event signed off

A stopWork flag is orthogonal to status — it can be set or cleared at any lifecycle stage via hseEvents.stopWork.

Endpoints

POST/v1/tenants/{tenantId}/hse/projectsCreate a project
GET/v1/tenants/{tenantId}/hse/projectsList projects (paginated)
GET/v1/tenants/{tenantId}/hse/projects/{id}Get a single project
PATCH/v1/tenants/{tenantId}/hse/projects/{id}Update project name, region, or status
POST/v1/tenants/{tenantId}/hse/shipmentsCreate a shipment
GET/v1/tenants/{tenantId}/hse/projects/{id}/shipmentsList shipments for a project
POST/v1/tenants/{tenantId}/hse/eventsUpsert an event (create or idempotent update)
GET/v1/tenants/{tenantId}/hse/eventsList events with filters and pagination
GET/v1/tenants/{tenantId}/hse/events/{id}Get event detail (includes actions, media, RCA, alerts)
POST/v1/tenants/{tenantId}/hse/events/{id}/transitionAdvance event status
POST/v1/tenants/{tenantId}/hse/events/{id}/stopworkSet or clear the stop-work flag
POST/v1/tenants/{tenantId}/hse/events/{eventId}/actionsCreate a corrective action
GET/v1/tenants/{tenantId}/hse/events/{eventId}/actionsList actions for an event
GET/v1/tenants/{tenantId}/hse/actions/{id}Get a single action
PATCH/v1/tenants/{tenantId}/hse/actions/{id}Update action status, due date, or assignee
PUT/v1/tenants/{tenantId}/hse/events/{eventId}/rcaUpsert root-cause analysis
GET/v1/tenants/{tenantId}/hse/events/{eventId}/rcaGet RCA for an event
POST/v1/tenants/{tenantId}/hse/events/{eventId}/alertCreate a safety alert from an event
POST/v1/tenants/{tenantId}/hse/alerts/{alertId}/publishPublish an alert to its audience
GET/v1/tenants/{tenantId}/hse/alertsList safety alerts
POST/v1/tenants/{tenantId}/hse/lessonsCreate a lesson learned
GET/v1/tenants/{tenantId}/hse/lessonsList lessons learned
POST/v1/tenants/{tenantId}/hse/media/signed-urlGet a signed upload URL for event media
POST/v1/tenants/{tenantId}/hse/media/{mediaId}/completeMark a media upload complete
POST/v1/tenants/{tenantId}/hse/sync/pullPull server changes since a sequence cursor
POST/v1/tenants/{tenantId}/hse/sync/pushPush offline operations to the server

Projects and shipments

Create a project

const project = await tenant.hseProjects.create({
code: 'PRJ-2026-BXL',
name: 'Brussels Terminal Expansion',
region: 'EMEA',
businessUnit: 'Logistics',
});

console.log(project.id);     // "hpr_abc123"
console.log(project.status); // "active"

Register a shipment

const shipment = await tenant.hseProjects.createShipment({
projectId: project.id,
code: 'SHP-BXL-001',
name: 'Antwerp → Brussels — Road Leg 1',
});

// List all shipments for the project
const shipments = await tenant.hseProjects.listShipments(project.id);

Reporting an event

Use hseEvents.upsert to create or idempotently re-submit an event. Pass a stable clientId (a UUID your app controls) to prevent duplicate reports from offline retries.

const event = await tenant.hseEvents.upsert({
clientId: 'a3f1c2e4-0001-4d2a-b9f3-112233445566',
kind: 'incident',
severity: 'high',
description: 'Forklift struck rack in aisle 7, two pallets fell.',
occurredAt: '2026-07-23T08:15:00Z',
latitude: 51.2194,
longitude: 4.4025,
projectId: project.id,
shipmentId: shipment.id,
transportMode: 'yard',
stopWork: true,
});

console.log(event.id);       // "hev_xyz789"
console.log(event.status);   // "open"
console.log(event.stopWork); // true

Event kinds and severity

kindUse when
incidentAn injury, property damage, or environmental release occurred
near_missA hazardous situation with no harm but significant potential
unsafe_actA person behaved unsafely but no incident followed
observationA general safety observation or positive recognition

severity is a free-text string; typical values are low, medium, high, and critical.

Listing and filtering events

// Filter by status and project
const page = await tenant.hseEvents.list({
status: 'open',
projectId: project.id,
limit: 50,
});

console.log(page.data);            // HseEvent[]
console.log(page.pagination.hasMore); // true | false

// Date-range filter
const weekEvents = await tenant.hseEvents.list({
dateFrom: '2026-07-01T00:00:00Z',
dateTo:   '2026-07-31T23:59:59Z',
kind:     'incident',
severity: 'high',
});

Paginating all events

hseEvents.listAll returns an async generator that follows the cursor automatically. See the Pagination guide for the full pattern.

// Stream every open event in the project — no manual cursor handling
for await (const e of tenant.hseEvents.listAll({
status: 'open',
projectId: project.id,
})) {
console.log(e.id, e.kind, e.severity);
}

Advancing event status

// Open → under investigation
await tenant.hseEvents.transition(event.id, {
to: 'under_investigation',
assigneeId: 'usr_safety_officer_01',
reason: 'Root-cause analysis started',
});

// Under investigation → closed
await tenant.hseEvents.transition(event.id, {
to: 'closed',
reason: 'All corrective actions verified, area cleared.',
});

Stop-work orders

Set or clear the stop-work flag independently of the event status.

// Issue a stop-work order
await tenant.hseEvents.stopWork(event.id, {
stopWork: true,
reason: 'Area unsafe — rack integrity unverified.',
});

// Clear once the area is safe
await tenant.hseEvents.stopWork(event.id, {
stopWork: false,
reason: 'Structural inspection passed, work resumed.',
});

Corrective actions

// Create an action
const action = await tenant.hseActions.create(event.id, {
assigneeId: 'usr_maintenance_02',
description: 'Inspect all racking in aisles 6–8 and replace damaged uprights.',
dueDate: '2026-07-30T17:00:00Z',
priority: 'high',
});

console.log(action.id);     // "hac_aaa111"
console.log(action.status); // "open"

// Update action — mark in progress
await tenant.hseActions.update(action.id, {
status: 'in_progress',
});

// Close the action when remediation is done
await tenant.hseActions.update(action.id, {
status: 'closed',
dueDate: '2026-07-28T14:00:00Z',
});

// List all actions for the event
const actionsPage = await tenant.hseActions.list(event.id, { limit: 20 });

Action statuses

StatusDescription
openTask created, not yet started
in_progressAssignee is working on the remediation
closedRemediation completed and verified
overdueDue date passed with no closure

Root-cause analysis

The RCA is a tree of why-nodes modelled after the 5-Whys method. Each node has a question, an answer, an optional factorType, and a parentId that builds the tree. Use hseRca.upsert to create or replace the analysis in one call.

await tenant.hseRca.upsert(event.id, {
method: '5-whys',
summary: 'Root cause: inadequate aisle width markings led to forklift path conflict.',
nodes: [
  {
    id: 'node_1',
    question: 'Why did the forklift strike the rack?',
    answer: 'The aisle width markings were worn and invisible.',
    factorType: 'environment',
    sortOrder: 1,
  },
  {
    id: 'node_2',
    parentId: 'node_1',
    question: 'Why were the markings worn?',
    answer: 'Floor repainting was overdue by 3 months.',
    factorType: 'management',
    sortOrder: 2,
  },
  {
    id: 'node_3',
    parentId: 'node_2',
    question: 'Why was repainting overdue?',
    answer: 'No scheduled maintenance reminder was in place.',
    factorType: 'process',
    sortOrder: 3,
  },
],
});

// Read back the RCA
const rca = await tenant.hseRca.get(event.id);
console.log(rca.method); // "5-whys"
console.log(rca.nodes.length); // 3

RCA factor types

factorTypeRepresents
peopleHuman behaviour, training, or fatigue
processProcedures, work instructions, scheduling
equipmentTools, machinery, PPE
environmentPhysical workspace, weather, signage
managementOversight, resource allocation, culture

Safety alerts and lessons learned

Once an incident is understood, publish a safety alert to inform the wider organisation.

// Create an alert from the event
const alert = await tenant.hseAlerts.createFromEvent(event.id, {
title: 'Forklift aisle collision — floor marking hazard',
body: 'An incident occurred in aisle 7 due to worn floor markings. All sites must audit aisle markings this week.',
audienceFilter: {
  regions: ['EMEA'],
  businessUnits: ['Logistics'],
},
});

// Publish to the filtered audience
await tenant.hseAlerts.publish(alert.id);

// Record a lesson learned
await tenant.hseAlerts.createLesson({
alertId: alert.id,
category: 'floor-safety',
summary: 'Worn aisle markings are a leading indicator of forklift collision risk. Add quarterly floor audit to site checklist.',
tags: ['forklift', 'floor-marking', 'aisle-safety'],
});

Attaching media

Field workers capture photo or video evidence on their device. The upload flow is: request a signed URL, PUT the file directly to cloud storage, then call completeUpload to commit the record.

// 1. Request a signed upload URL
const { uploadUrl, storageRef, expiresAt } = await tenant.hseMedia.getSignedUrl({
eventId: event.id,
mediaId: 'med_local_uuid_001',
mediaKind: 'photo',
contentType: 'image/jpeg',
fileName: 'aisle7-damage.jpg',
});

// 2. PUT the file directly to cloud storage
await fetch(uploadUrl, {
method: 'PUT',
body: imageBuffer,
headers: { 'Content-Type': 'image/jpeg' },
});

// 3. Commit the upload
const media = await tenant.hseMedia.completeUpload('med_local_uuid_001', {
storageRef,
fileSize: imageBuffer.byteLength,
});

console.log(media.uploadState); // "uploaded"

Offline sync

SafeScan is designed for field use where connectivity is unreliable. Devices queue operations locally and sync when back online.

Pull (server → device)

// First pull: since=0 returns everything
const pullResult = await tenant.hseSync.pull({
since: 0,
clientDeviceId: 'device_field_unit_07',
limit: 200,
});

console.log(pullResult.events);   // HseEventDetail[] — full event graphs
console.log(pullResult.cursor);   // e.g. 1048576 — new sequence number
console.log(pullResult.hasMore);  // true if more pages to pull

// Subsequent pull: pass the last cursor
const delta = await tenant.hseSync.pull({
since: pullResult.cursor,
clientDeviceId: 'device_field_unit_07',
});

Push (device → server)

const pushResult = await tenant.hseSync.push({
clientDeviceId: 'device_field_unit_07',
ops: [
  {
    opId: 'op_uuid_001',
    entity: 'event',
    id: 'a3f1c2e4-0001-4d2a-b9f3-112233445566',
    type: 'upsert',
    seq: 1,
    payload: {
      clientId: 'a3f1c2e4-0001-4d2a-b9f3-112233445566',
      kind: 'near_miss',
      severity: 'medium',
      description: 'Near-miss at loading bay 3.',
      occurredAt: '2026-07-23T10:00:00Z',
    },
  },
],
});

for (const result of pushResult.results) {
console.log(result.opId, result.status);
// e.g. "op_uuid_001" "applied"
}

Push result statuses

statusMeaning
appliedOperation accepted and persisted
duplicateopId already seen; server returned the existing record
conflictServer state conflicts with the operation payload
rejectedOperation was invalid (see error for details)

End-to-end example

Report an incident in the field, investigate, attach evidence, and close.

// 1. Report the incident (offline-safe via clientId)
const event = await tenant.hseEvents.upsert({
clientId: 'c0ffee00-beef-4a1b-8bad-000000000001',
kind: 'incident',
severity: 'high',
description: 'Chemical spill in bay 4 — drum punctured during unloading.',
occurredAt: new Date().toISOString(),
projectId: 'hpr_terminal_01',
transportMode: 'road',
stopWork: true,
});

// 2. Issue the stop-work order explicitly
await tenant.hseEvents.stopWork(event.id, {
stopWork: true,
reason: 'Hazmat spill — bay 4 cordoned off.',
});

// 3. Start investigation
await tenant.hseEvents.transition(event.id, {
to: 'under_investigation',
assigneeId: 'usr_ehs_lead',
reason: 'EHS team on site.',
});

// 4. Create corrective actions
await tenant.hseActions.create(event.id, {
assigneeId: 'usr_hazmat_team',
description: 'Contain and neutralise spill; dispose of contaminated materials.',
dueDate: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(),
priority: 'critical',
});

// 5. RCA
await tenant.hseRca.upsert(event.id, {
method: '5-whys',
summary: 'Drum was over-pressurised during road transport; valve failed on impact.',
nodes: [
  {
    id: 'n1',
    question: 'Why did the drum puncture?',
    answer: 'Internal pressure exceeded rated limit.',
    factorType: 'equipment',
    sortOrder: 1,
  },
  {
    id: 'n2',
    parentId: 'n1',
    question: 'Why was pressure excessive?',
    answer: 'Temperature relief valve was not inspected before shipment.',
    factorType: 'process',
    sortOrder: 2,
  },
],
});

// 6. Alert the organisation
const alert = await tenant.hseAlerts.createFromEvent(event.id, {
title: 'Chemical drum spill — Bay 4 — Antwerp terminal',
body: 'Drum over-pressurisation caused a spill. All sites must verify valve inspections before loading.',
audienceFilter: { businessUnits: ['Logistics', 'QHSE'] },
});
await tenant.hseAlerts.publish(alert.id);

// 7. Iterate all open high-severity events to check scope
for await (const e of tenant.hseEvents.listAll({ status: 'open', severity: 'high' })) {
console.log(e.id, e.kind, e.occurredAt);
}

// 8. Close after remediation
await tenant.hseEvents.transition(event.id, {
to: 'closed',
reason: 'Spill contained, bay cleared, corrective actions verified.',
});
await tenant.hseEvents.stopWork(event.id, { stopWork: false });

Next steps

  • Pagination — How listAll async generators work under the hood
  • Observations — Capture photo evidence linked to a location
  • Webhooks — React to HSE event lifecycle changes in real time
  • Missions & Routes — Schedule field inspection visits