- Expanso + Jev
- Example 09 of 10
A moderation pre-filter where people only see the edge cases.
Every piece of content gets a category and a needs-human judgment — safe content auto-allows, the gray zone queues by (1 − confidence).
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 /content- Receive
- Shape
- Ask Jev
- Gate
- Route
categoryneeds_human
allowreview
- Receivelines 11 to 16
An HTTP server input accepts content on
POST /content. - Shapelines 17 to 26
Parses the POST body if it arrived as a string, then stamps
received_at. It applies the same fixed rules every time, with no model involved; the timestamp is the one value that differs. - Ask Jevlines 27 to 73
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.categorychoiceneeds_humannoul
- Gatelines 74 to 88
One fixed rule over Jev’s answers allows content or queues it, and computes
review_priorityas one minus Jev’s confidence. - Routelines 89 to 106
A
switchoutput writes allowed content and the review queue to separate files.
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.
categorychoiceClassify this user-generated content.
safe · spam · hate · sexual · self_harm · other
needs_humannoulDoes this content need a human moderator to review it before publishing?
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.
- $cat == "safe" && $conf >= 0.9 && $needs_human < 0.5allow · Confidently safe, and Jev sees no need for a person.
- elsereview · Everything else, ordered by uncertainty.
If Jev is unreachable: review
With no answers, the category defaults to other and needs_human to 1, so everything queues for people at the highest priority. The example fails closed.
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-moderation
type: pipeline
description: Moderation pre-filter with Jev — every piece of user content gets a category judgment inline. Safe content auto-allows; everything else queues for humans, most-uncertain first.
namespace: production
labels:
category: integrations
pattern: ai-decision
model: jev
config:
input:
http_server:
address: "0.0.0.0:8080"
path: /content
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 }
- mapping: |
root = this
root.received_at = now()
# ── Ask Jev ──
- mutation: |
meta jev_start = timestamp_unix_milli()
- branch:
request_map: |
root = {
"state": this.string(),
"model": "jev-latest",
"questions": {
"category": {
"type": "choice",
"instructions": "Classify this user-generated content.",
"criteria": {
"safe": "Benign content, fine to publish",
"spam": "Unsolicited promotion, scams, repetitive junk",
"hate": "Attacks or demeans a protected group",
"sexual": "Sexually explicit content",
"self_harm": "Encourages or describes self-harm",
"other": "Concerning but does not fit the above"
}
},
"needs_human": {
"type": "noul",
"instructions": "Does this content need a human moderator to review it before publishing?"
}
}
}
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: humans only see the edge cases ──
- mapping: |
root = this
let cat = this.jev.answers.category.choice.or("other")
let conf = this.jev.answers.category.confidence.or(0)
let needs_human = this.jev.answers.needs_human.noul.or(1)
root.jev_decision = if $cat == "safe" && $conf >= 0.9 && $needs_human < 0.5 {
"allow"
} else {
"review"
}
# Review queue is worked most-uncertain-first downstream
root.review_priority = 1 - $conf
output:
broker:
pattern: fan_out
outputs:
- stdout:
codec: lines
- switch:
cases:
- check: this.jev_decision == "allow"
output:
file:
path: ./data/jev-moderation/allowed-${! now().ts_format("2006-01-02") }.jsonl
codec: lines
- check: "true"
output:
file:
path: ./data/jev-moderation/review-${! now().ts_format("2006-01-02") }.jsonl
codec: lines
106 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 /content on port 8080.
- Output. Local files under
./data/jev-moderation/. - 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.
{"content_id":"c-001","author":"user_4821","text":"Just finished the new onboarding flow, super smooth! Great work team."}{"content_id":"c-002","author":"user_9917","text":"CHECK OUT my crypto doubling service at shady-link.example, 100% guaranteed returns!!!"}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: category, needs_human. 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 "review". With no answers, the category defaults to other and needs_human to 1, so everything queues for people at the highest priority. The example fails closed.
Run the deterministic half on your own nodes.
Expanso Edge runs these pipelines where the data is created. The first five nodes are free.