Your first pipeline¶
Now that Jane is installed and you’ve run the simplest possible example, it’s time to build your first real pipeline.
This chapter walks you through the core workflow you’ll use every day: taking raw input, interpreting it, validating it, and getting back a clean, structured result.
The goal here isn’t to teach every subsystem — you’ll get dedicated chapters for that. The goal is to give you a feel for how Jane flows.
Pipelines in Jane¶
A pipeline is a sequence of explicit steps:
scan(optional).normalize(automatic unless strict mode).parse(explicit).validate(explicit).decide(policy).
You build a pipeline by chaining fluent calls:
jane(value)
.parse(...)
.validate(...)
.run();
Every call returns a new pipeline builder, so the API stays clean and immutable.
A Simple Example¶
Let’s say you’re validating an age field that comes in as a string:
const result = await jane.value("42")
.parse("numeric")
.positive()
.run();
What happens here?¶
" 42 "is normalized (trimmed)."42"is parsed into the number42.42is validated as positive.- The pipeline returns a structured result.
Let’s inspect it.
Inspecting the Result¶
A typical result looks like this:
{
ok: true,
issues: undefined,
events: [
{
phase: 'scan',
kind: 'info',
code: 'scan.is.disabled',
path: {
segments: [],
inputName: undefined,
toString: [Function: toString]
},
message: 'Scan stage was skipped because policy.scan is false.',
userMessage: undefined,
metadata: {
scan: false,
mode: 'moderate',
runId: undefined,
createdAt: 1768628968671
}
},
{
phase: 'parse',
kind: 'info',
code: 'string.now.number',
message: 'String parsed into number 42.',
userMessage: 'Converted text into a number.',
path: {
segments: [],
inputName: undefined,
toString: [Function: toString]
},
metadata: { before: '42', after: 42 }
}
],
value: 42,
path: {
segments: [],
inputName: undefined,
toString: [Function: toString]
},
diff: undefined,
explain: undefined,
replay: undefined,
metadata: {
raw: '42',
rawType: 'string',
safe: '42',
safeType: 'string',
normalized: '42',
normalizedType: 'string',
final: 42,
finalType: 'number',
startedAt: '2026-01-17T05:49:28.671Z',
finishedAt: '2026-01-17T05:49:28.672Z',
durationMs: 1,
inputName: undefined
}
}
You’ll learn the full shape in a later chapter, but here’s the quick version:
ok: Whether the value is acceptable.value: The final interpreted value.issues: Only the errors (none here).events: Everything that happened.metadata: Timestamps and structural info
Even this simple pipeline gives you a complete, transparent record.
Adding More Rules¶
Pipelines can have as many rules as you want:
const result = await jane.value(" 42 ")
.parse("numeric")
.integer()
.min(18)
.max(120)
.run();
Each validator runs independently. Each one emits events. Policy decides what those events mean.
Handling Failures¶
If validation fails:
const result = await jane(" 42 d")
.parse("numericString")
.positive()
.run();
You’ll get:
{
ok: false,
issues: [
{
phase: 'validate',
kind: 'error',
code: 'type.not.valid',
path: '$',
message: "Expected 'number' but received 'string'.",
userMessage: 'Please enter a valid number.',
metadata: {...}
},
{
phase: 'parse',
kind: 'error',
code: 'fail.to.parse',
path: '$',
message: "Failed to parse string to a number.",
metadata: {...}
}
],
events: [
{
phase: 'normalize',
kind: 'info',
code: 'string.now.trimmed',
path: [Object],
message: 'Removed leading and trailing whitespace.',
userMessage: 'Extra spaces at the beginning or end were removed.',
metadata: [Object]
}
],
value: '42 d',
path: {
segments: [],
inputName: undefined,
toString: [Function: toString]
},
diff: undefined,
explain: undefined,
replay: undefined,
metadata: {
raw: ' 42 d',
rawType: 'string',
safe: ' 42 d',
safeType: 'string',
normalized: '42 d',
normalizedType: 'string',
final: '42 d',
finalType: 'string',
startedAt: '2026-01-15T02:09:12.984Z',
finishedAt: '2026-01-15T02:09:12.984Z',
durationMs: 0,
inputName: undefined
}
}
Jane never throws for validation failures. You always get a structured result.
Naming Pipelines¶
Names show up in telemetry, boundaries, and debugging:
const result = await jane.value("42")
.named("age")
.parse("numericString")
.validate("positive")
.run();
This becomes especially useful once you start validating objects.
User Messages¶
You can override the user-facing message for validation events:
const result = await jane.value("not-an-email")
.email()
.userMessage("Please enter a valid email address.")
.run();
This affects only validation events. Not scan, normalization, or parse.
Modes: strict, moderate, lax¶
You can control how much normalization and scanning happens:
jane.value(input).strict().run();
jane.value(input).moderate().run();
jane.value(input).lax().run();
You’ll get a full chapter on modes later, but here’s the short version:
strictNo normalization, scan enabled.moderateSafe defaults.lax: Developer-friendly, lossy normalization allowed.
What You’ve Learned¶
You now know how to:
- Build a pipeline.
- Parse values.
- Validate values.
- Inspect results.
- Name pipelines.
- Override user messages.
- Choose a mode.
This is the foundation of everything else in Jane.
In the next chapter, we’ll zoom out and look at how the pipeline stages work, so you understand what’s happening under the hood. Want to dive in?