zlow: What We Actually Built
The previous post described the architecture as we understood it then. We’ve kept building since. Some things we called wrong. Some things turned out to be more interesting than we expected. This is the honest update.
What zlow Is
zlow is a pipeline runner where the schema is the program.
Not a framework. Not an agent loop. A runtime that executes a sequence of schema fills, connected by declared triggers, with algorithms handling everything except the reasoning itself.
A step is exactly two fields:
const scriptwriterStep: Step = {
name: 'scriptwriterStep',
schema: `Z.object({
_config: Z.object({
subscribesTo: Z.literal("generate_video"),
onComplete: Z.literal("script:ready"),
systemPrompt: Z.literal("You are HYPE BEAST..."),
temperature: Z.literal(0.85),
}),
game: Z.string().rag$getGameMetadata(),
script: Z.string(),
hookLine: Z.string(),
tagline: Z.string(),
title: Z.string(),
})`,
}
No system prompt on the Step object. No configuration outside the schema. The schema is the full control plane — routing, model, temperature, enrichment, everything.
The Seam
There are two worlds.
The algorithmic world: routing, extension dispatch, context assembly, circuit breakers, side effects. Deterministic. Testable. Free.
The latent world: the LLM fills the schema. Reasoning, synthesis, judgment. Non-deterministic. Necessary. Contained.
The schema is the seam between them. Every step is a flip into latent space. The structured output flips back. The algorithmic world never leaves.
This isn’t an architectural preference. It’s a testability guarantee. Because the seam is explicit, you can mock either side of it and test the other. A ten-step pipeline with real routing, real extension dispatch, real schema validation — zero API calls, runs in milliseconds.
const result = await pipeline([analysisStep, ragStep, publishStep]).run(
{ topic: 'test input' },
{
mocks: {
analysisStep: () => ({ topic: 'test', confidence: 0.9, needsMoreInfo: null }),
ragStep: () => ({ enrichedContext: 'found docs' }),
publishStep: () => ({ published: true, url: 'http://...' }),
},
}
)
assert.deepEqual(result.path, ['analysisStep', 'publishStep']) // ragStep skipped
That test runs the real routing logic. The real schema validation. The real extension registry. The real context threading. The mocks replace the LLM calls, nothing else.
Input Extensions: Before the Model Runs
This is the piece that makes the inversion concrete.
When a field is annotated with rag$getGameMetadata(), the extension fires before the model call. The result goes into $context. The model sees it in the prompt.
input extensions → assemble $context → model call → validate → output extensions
We verified this is actually happening by running a real Gemini call with a mocked enrichment handler:
“Manor Lords just smashed one million sales in only forty-eight hours! Forget the hype — with a 93% positive score and over 42,000 players already obsessed…”
“93% positive score.” “Over 42,000 players.” Those numbers came through $context.rag$getGameMetadata. The schema declared the enrichment. The algorithm fetched it. The model used it.
That’s the inversion working end to end.
Fan-Out and the Ordering Constraint
Multiple steps can subscribe to the same trigger. When step A emits "content:ready", steps B and C can both subscribe — each with a different schema, a different model, a different task. That’s fan-out. The branches are heterogeneous. This is different from map/reduce, which applies the same function to all nodes. Here each branch is a genuinely different computation.
The collecting step — the one that reasons across all branch outputs — uses join: true:
_config: Z.object({
subscribesTo: Z.enum(["branchA:done", "branchB:done"]),
join: Z.literal(true),
})
Without join: true, the collecting step would run after the first branch completed. With it, the step only runs when all its triggers are present in the accumulated trigger log — meaning every branch has actually finished.
We want to be precise about what this is. Branches run sequentially, not concurrently. This is not a concurrent join barrier in the distributed systems sense. It is a sequential ordering constraint: the collecting step is guaranteed to run after all branches have completed, in a fully deterministic, testable execution.
We made this choice deliberately. Concurrent join barriers are hard to implement correctly and harder to test — non-deterministic execution means flaky tests and subtle race conditions. Sequential ordering gives you the same correctness guarantee while keeping the pipeline fully testable with mocks. For the use cases we’re building toward, that trade-off is the right one.
Accumulated Context
After each step runs, its output is merged into an accumulated context object. Every subsequent step receives everything: the original input, plus all previous step outputs merged together.
This is what makes the collecting step useful. It doesn’t receive only the last branch’s output — it receives the merged output of all branches, plus the original input. The LLM sees the full picture.
What We Don’t Have Yet
Branch context isolation is an open gap. In the current implementation, the second branch receives the accumulated context including the first branch’s output. Ideally each branch should receive a snapshot of the context at the fork point — independent of what its siblings produced. This hasn’t been fixed yet.
Algorithmic-only steps — steps that skip the model call entirely — aren’t implemented. The capability ladder (routing to different models by annotation) is partially done: model in _config works, but automatic routing by schema annotation doesn’t.
Why This Matters
The field is mostly working on how much the LLM can do. More tools. Better reasoning. Longer context. zlow is working on a different question: how much of the system can live in algorithmic space, where it’s testable, deterministic, and free?
The answer is: most of it. Routing is algorithmic. Context assembly is algorithmic. Extension dispatch is algorithmic. Side effects are algorithmic. The only part that isn’t is the reasoning — structured input becoming structured output through something we can’t fully predict.
zlow puts the seam at the schema. Everything on one side is deterministic. Everything on the other side visits latent space and comes back with an answer.
That seam is narrow. But it’s in exactly the right place.
zlow is an internal library in active development. Zontax is the schema language that makes the annotations possible.