- Expanso + Jev
- Example 04 of 10
Sensor triage: Expanso attaches the baseline, Jev reads the deviation.
Every reading gets an anomaly score and a dispatch judgment — real anomalies become work orders, the rest roll up to files.
Where the record goes
Every stage on the left is Expanso, and it is deterministic. The record crosses to Jev once, for the one question a rule cannot answer, and comes straight back. Line numbers link to the YAML below.
POST /readings- Receive
- Shape
- Ask Jev
- Gate
- Route
anomalydispatch
dispatchflagrollup
- Receivelines 11 to 16
An HTTP server input accepts readings on
POST /readings. - Shapelines 17 to 33
Parses the body, then attaches the asset’s normal operating baseline (max temperature, max vibration) and stamps
received_at. The baseline is a fixed value in this example; a comment marks a cache lookup for production. - Ask Jevlines 34 to 79
Expanso packs the whole record and the typed questions into one request. Jev answers. If the call fails, a
catchsubstitutes empty answers markedjev-unavailable, and the pipeline keeps going.anomalyscoredispatchnoul
- Gatelines 80 to 93
Fixed thresholds over Jev’s answers choose dispatch, flag, or rollup.
- Routelines 94 to 117
A
switchoutput writes to one file per decision.
What Jev is asked
Jev, judgmentThe pipeline sends the record with 2 typed questions. Jev answers each one with a value the pipeline can compare against a number.
anomalyscoreHow anomalous is this sensor reading relative to its baseline?
dispatchnoulShould a maintenance technician be dispatched based on this reading?
What Expanso does with the answers
Expanso, deterministicFixed thresholds, checked in order. The first rule that matches sets the route. These are the expressions in the pipeline, not a summary of them.
- $dispatch >= 0.8dispatch · Jev is confident a technician is needed.
- $anomaly >= 2flag · A concerning deviation or worse.
- elserollup · Routine telemetry.
If Jev is unreachable: rollup
With no answers, both scores default to 0, so every reading rolls up and nothing is dispatched or flagged while Jev is unreachable.
The pipeline
This is the example's own pipeline file, unmodified. Violet marks the lines Expanso runs on its own. Orange marks the handoff, and the darker orange band is the HTTP call to Jev itself.
name: jev-sensor-triage
type: pipeline
description: Sensor anomaly triage with Jev — every reading is judged against its baseline in context. Real anomalies dispatch maintenance with a context packet; routine telemetry rolls into aggregates.
namespace: production
labels:
category: data-routing
pattern: ai-decision
model: jev
config:
input:
http_server:
address: "0.0.0.0:8080"
path: /readings
allowed_verbs: ["POST"]
pipeline:
processors:
- mapping: |
# http_server already parses JSON bodies; only parse raw strings
root = if this.type() == "string" { this.parse_json() } else { this }
# Attach each sensor's normal operating baseline so Jev judges
# deviation in context, not raw values (production: use a cache lookup)
- mapping: |
root = this
root.baseline = {
"temp_c_max": 85,
"vibration_mm_s_max": 4.5,
"note": "Values above these sustained for 5+ minutes are abnormal for this asset class"
}
root.received_at = now()
# ── Ask Jev ──
- mutation: |
meta jev_start = timestamp_unix_milli()
- branch:
request_map: |
root = {
"state": this.string(),
"model": "jev-latest",
"questions": {
"anomaly": {
"type": "score",
"instructions": "How anomalous is this sensor reading relative to its baseline?",
"criteria": [
"Normal operation",
"Slightly unusual, watch",
"Concerning deviation",
"Severe anomaly",
"Critical failure signature"
]
},
"dispatch": {
"type": "noul",
"instructions": "Should a maintenance technician be dispatched based on this reading?"
}
}
}
processors:
- http:
url: "${JEV_API_URL:https://api.typesafe.ai/v1/systemone}"
verb: POST
headers:
Content-Type: application/json
Authorization: "Bearer ${TYPESAFE_API_KEY}"
timeout: 2s
retries: 1
- catch:
- mapping: |
root = {"answers": {}, "model": "jev-unavailable"}
result_map: |
root.jev = {
"answers": this.answers,
"model": this.model.or("jev-latest"),
"ms": timestamp_unix_milli() - metadata("jev_start")
}
# ── Confidence-gated cascade ──
- mapping: |
root = this
let anomaly = this.jev.answers.anomaly.score.or(0)
let dispatch = this.jev.answers.dispatch.noul.or(0)
root.jev_decision = if $dispatch >= 0.8 {
"dispatch"
} else if $anomaly >= 2 {
"flag"
} else {
"rollup"
}
output:
broker:
pattern: fan_out
outputs:
- stdout:
codec: lines
- switch:
cases:
# Production: swap for http_client -> CMMS work-order API
- check: this.jev_decision == "dispatch"
output:
file:
path: ./data/jev-sensor-triage/dispatch-${! now().ts_format("2006-01-02") }.jsonl
codec: lines
- check: this.jev_decision == "flag"
output:
file:
path: ./data/jev-sensor-triage/flagged-${! now().ts_format("2006-01-02") }.jsonl
codec: lines
- check: "true"
output:
file:
path: ./data/jev-sensor-triage/rollup-${! now().ts_format("2006-01-02") }.jsonl
codec: lines
117 lines. Copy and Download both give you the file byte for byte.
What you need
- Expanso Edge installed, to validate and run the pipeline.
- A Jev endpoint. The pipeline posts to
JEV_API_URL, and falls back tohttps://api.typesafe.ai/v1/systemonewhen that variable is unset. - A key for that endpoint in
TYPESAFE_API_KEY. The pipeline sends it as a bearer token and has no default for it.
What it proves
- Input. POST /readings on port 8080.
- Output. Local files under
./data/jev-sensor-triage/. A comment in the YAML marks where a work-order API replaces the dispatch file in production. - Scope. This example ships as a pipeline file and sample records. Its README marks the live runtime (event generator and dashboard) as coming next, so what is published here is the pipeline itself.
- Revision. The file shown is the example as of commit
517c38fof its repository, which is still being developed.
Sample records
The first two of the records that ship with this example. Download all of them.
{"sensor_id":"pump-07","ts":"2026-09-18T14:00:00Z","temp_c":72.4,"vibration_mm_s":2.1,"rpm":1750}{"sensor_id":"pump-07","ts":"2026-09-18T14:01:00Z","temp_c":73.1,"vibration_mm_s":2.3,"rpm":1750}Questions about this example.
receive, shape, gate, route. Each of those stages applies the same fixed rules every time, with no model involved. Expanso also sets the thresholds that turn Jev's answers into a route.
Jev answers 2 typed questions about each record: anomaly, dispatch. It does not choose the route. The pipeline's gate does that from Jev's answers.
The pipeline catches the failed call, marks the record jev-unavailable, and the gate resolves to "rollup". With no answers, both scores default to 0, so every reading rolls up and nothing is dispatched or flagged while Jev is unreachable.
Run the deterministic half on your own nodes.
Expanso Edge runs these pipelines where the data is created. The first five nodes are free.