Skip to content
andydataguy
Wiki Corpus

Data Stack: The Orchestrator Audit. LangGraph covered enough coordination work to put a separate orchestrator under review.

I started with a search feature for this site and ended up auditing the tools I used to coordinate data pipelines. The question was whether the work still needed a separate orchestration platform. LangGraph already supplied typed state, conditional routing and runtime fan-out, with checkpointing for recovery. Those mechanisms covered enough of the job to make the extra platform a decision I had to justify.

AI & TECHNICAL · Reading guide~44 min read · 10,295 words09 AUG 2026
[ BUILD LOG / PROCESS-FLOW ]
#data-engineering #langgraph

I started with a search feature for this site and ended up auditing the tools I used to coordinate data pipelines. The question was whether the work still needed a separate orchestration platform. LangGraph already supplied typed state, conditional routing and runtime fan-out, with checkpointing for recovery. Those mechanisms covered enough of the job to make the extra platform a decision I had to justify.

That conclusion has a boundary. A library can coordinate work inside an application. A team still has to run the application, store its checkpoints, handle failed writes and decide who gets called when it breaks. The useful comparison includes those obligations. Counting boxes on a diagram won't settle it.

A gouache-dark scene: a compact machine of brass and glass running on a desk in a deep navy room, one amber lamp on it, while a stack of unopened boxed appliances towers in the shadow behind it, unplugged.
A working machine is a better resume than a deck about machines. The site is the machine.

Start with the jobs the stack has to do

The architecture in my original search-build note divided the work between Next.js, Convex and Python. Next.js handled the site. Convex handled the served data and backend functions. Python handled the pipelines that prepared the material. The diagram names responsibilities before product categories.

It isn't a complete inventory of every dependency. A durable checkpointer needs storage. External model calls still go to external providers. The code runs somewhere, and someone owns that runtime. The diagram describes the application flow; the operating inventory has to account for everything underneath it.

Three labeled blocks in a row: NEXT.JS renders, CONVEX holds, PYTHON produces. One arrow from Python into Convex, one arrow from Convex into Next.js. Nothing else on the field.
The request path crosses three systems. That's the exhibit.

A document arrives, a pipeline processes it, and a product reads the result. Somewhere along that path we need to track progress, reject bad work, combine partial results and recover from interruptions. Those are real requirements. Installing an orchestrator is one way to meet them. Before adding one, I want to know which requirements the stack already meets and which would otherwise become code I have to maintain.

That's what I mean by min-maxxing. A character build gives you a limited allocation of points, and a useful build spends them on the abilities the run uses. A stack has a limited allocation too, except the points are engineering attention, operating cost and the number of systems a person can understand well enough to repair.

The analogy holds at the allocation decision. It breaks if removing a dependency also removes a capability the workload needs. A character with every point in damage and none in survival has a short run. An application with a tiny diagram and no recovery story has the same problem.

A single output curve rising then flattening toward a plateau. A moving dot climbs the curve toward the flat. A marker labeled ADD THE NEXT TOOL sits at the plateau, far right. Text: squeeze the curve first.
The dot climbs toward the flat of the curve, and the marker for the next tool waits at the plateau.

Audit the capabilities before adding a service

The question I keep returning to is what the parts we already have can do that we're not asking them to do. A database may already supply a search index. A workflow library may already persist execution state. A platform feature may be a wrapper around behavior our application can express without another deployment.

That doesn't make every wrapper waste. A product can package difficult operating work, provide an interface that the team depends on, or make a failure understandable to someone who won't read Python. The audit has to name that value too. Otherwise we're comparing an existing service with an imaginary replacement that gets its support and recovery for free.

An audit card: one tool block in the center labeled WHAT ELSE CAN YOU DO. Around it four dim capability tags lighting from dim to lit: SEARCH, FILES, REALTIME, FUNCTIONS. A tally at the base: jobs held 5, vendors replaced 4.
The min-max audit is one question per tool: what else can you already do?

My data-engineering work grew out of moving and interpreting data across advertising accounts. I later helped design an algorithmic trading system, where different indicators, market conditions and prediction pipelines made the data work more involved. Those experiences explain why I care about what enters a system, how it changes and whether anyone can reconstruct the result.

They don't prove that this site's architecture is right. The architecture has to earn that conclusion through its own mechanisms and operating evidence.

A gouache-dark trading desk at night: a brass rebalancing scale sits on the desk weighing small glowing position tiles, some being lifted off the heavy side, while dim monitors in the background show flat curves nobody is watching. One amber lamp over the scale.
The scale lifts positions off the heavy side: stacks are portfolios, and they rebalance on schedule or bleed on schedule.

A stack audit also needs permission to revisit an earlier good decision. A service can have earned its place when a team adopted it and become redundant after another part acquired the same capability. The original decision can remain reasonable while the current allocation changes. Keeping an old tool solely because the team once did the work of installing it makes the installation cost permanent.

A patch-notes scroll unrolled beside a character build panel. The scroll lists meta changes; on the panel, stat points visibly drain out of two faded stats and pour into a newly lit stat labeled STATE MACHINERY. A stamp reads RESPEC, dated this era.
Points drain out of two faded stats and into state machinery, which is the respec this audit ran.

The site gives the audit a concrete workload

This site is meant to demonstrate the work I can do. That purpose is more demanding than a list of tools on a services page. Search needs usable documents. Usable documents need ingestion and processing. A failed parser needs an identifiable outcome. A change to the corpus needs to become available to the reader through a defined path.

An evaluation card split in two columns: CLAIMS with dim unchecked lines, and RUNNING with lit checks beside live-system rows: pipelines, search, realtime, this page. An inspector's magnifier hovers over the RUNNING column only.
The magnifier hovers only over the running column, so the claim has to be something that runs.

The original note described some of that search work as still being built. A written architecture can't certify which parts are live today, so I won't use the page you're reading as proof of every backend claim. The workload gives us something concrete to reason about. Runtime evidence determines which parts of the design are operating.

A gouache-dark museum hall on deep navy: plinths in a row, each holding a dusty piece of data machinery under glass, placard cards beneath, one violet velvet rope line in front. No people.
Each plinth holds a piece of machinery somebody added once and nobody re-audited.

The audit starts at the database because that's where several ordinary jobs can share one implementation. Then it moves into the pipeline, where state and recovery determine whether removing an orchestrator reduces the work or merely hides it.

One deployment can handle several jobs

The Convex decision in the original build was to use capabilities that belonged together before buying separate services for them. The list had five jobs: database, realtime delivery, full-text search, files and backend functions. The count describes the capabilities in this design. It doesn't establish five vendors removed or a particular amount saved.

The database holds documents under a declared schema, with indexes for the reads the application needs. Mutations give the application a transaction boundary for related database changes. Search indexes let the same backend expose a different way to find those documents. Reactive queries connect changes in the data to subscribed clients. File storage and server-side functions cover other ordinary application needs.

Those jobs share context. Search operates on records the database already holds. A result page needs permission to read those records. The application that updates a record also knows when a reader should see the new value. Keeping those operations together can remove integration work that would otherwise need a second implementation.

Search removes a synchronization job only within its scope

A separate search engine can be the right choice. It also creates a question the team must answer every time the source data changes: how does the index catch up, and how will we notice when it doesn't?

A search index declared alongside the source table reduces the application code needed to maintain that relationship. This TypeScript schema illustrates the idea. It's a partial schema example, with its imports and application-specific permissions omitted, rather than a complete search implementation.

export default defineSchema({
  entries: defineTable({
    title: v.string(),
    body: v.string(),
    service: v.string(),
  }).searchIndex("search_body", {
    searchField: "body",
    filterFields: ["service"],
  }),
});

The declaration says which text to search and which field can filter the search. The surrounding application still needs a query, access checks and a way to present results. Search relevance also remains a product question. A valid index can return documents in an order the reader finds unhelpful.

A refused-parts list drawn as a rack of empty slots: SEARCH CLUSTER, CACHE LAYER, QUEUE SERVICE, SYNC PIPELINE, each slot stamped NOT HIRED. Beneath, one line: the parts you don't buy don't page you.
A refused-parts list is the real min-max artifact: every slot here is a part that was never hired.

The min-max decision is narrower than declaring the whole search problem solved. We can avoid operating a second indexing system for the requirements the built-in index meets. A requirement it doesn't meet remains a reason to evaluate another system. That requirement might concern retrieval behavior, operating controls or the shape of the data. It needs a name before it becomes a purchase.

Reactive queries change the update path

A client using a reactive query subscribes to a result. Changes to the data that result depends on can produce an updated result for the client. The application doesn't need to recreate that dependency tracking with its own polling loop for the same use case.

Two small timelines. Top, POLLING: a client fires requests on a fixed clock at a database, most marked wasted. Bottom, REACTIVE: the database pushes once, exactly when the data changes, and the page updates. One change, one push.
With reactive queries, the page updates because the data moved, not because a poller asked.

Picture a document-processing status page. The page reads the document's current processing state. A worker records that extraction failed. A subscribed client can receive the changed result through the data layer, without a second mechanism that periodically asks whether the status changed.

That's an illustrative flow, not an observed event from this site's runtime. It helps separate the capability from the user experience. The interface still needs to explain pending work, display a failure and handle a lost connection. Reactivity supplies an update mechanism. It doesn't decide what a confusing status means or make every view fresh under every condition.

An inventory card of frontend plumbing, each item struck through: polling loop, refresh button, cache invalidation, staleness guard, retry-on-focus. Beneath, one line stands unstruck: subscribe once, and a single push arrow from a data core to a page.
One subscription replaces the whole inventory of plumbing, so none of it gets written.

The same care applies to caching and background work. A platform's query behavior may remove a reason to add an application cache. Scheduled functions and actions may cover a workload that would otherwise require a separate job service. Neither conclusion grants the replacement every queue feature or every cache policy. Rate control, work ownership, retries and failure visibility still belong in the requirement comparison.

A transaction has a boundary

The integration cost is often larger than a connector suggests. Two services may use different identifiers, disagree about delivery, or recover on different schedules. A sync job has to translate between those assumptions and record enough state to repair a partial result.

Five solid component blocks connected by four thin glowing joints. The blocks are healthy; each joint carries a hairline crack and a small unwatched-zone marker between the two dashboards' coverage arcs, which fail to overlap exactly at the joints.
The two dashboards' coverage arcs miss each other exactly at the joints, which is where the cracks are.

Consolidation can eliminate a particular seam. A transaction that changes related database records together removes a class of half-written state inside that transaction. The claim should stop at the boundary. An external API call, a Python process and a checkpoint store don't automatically participate in that same database commit.

Suppose the database accepts a processed article, then the worker loses its connection before recording completion elsewhere. The article exists. The worker's record of completion may disagree. The team needs a way to reconcile those facts regardless of how many jobs the database performs. Combining services doesn't make an uncertain acknowledgment impossible.

A pipeline result drawn as a sealed crate landing on a dock in exactly one piece, stamped ATOMIC, beside an alarm bell labeled OR ANNOUNCED FAILURE. Below, a ghosted forbidden scene: a half-crate on one store and its orphan half on another, crossed out as a state that cannot occur.
The crate lands whole or rings the bell, and the half-crate split across two stores is crossed out because it can't happen.

That's why I want a refused-parts list with reasons, rather than a celebratory vendor count. The list should say which job an existing capability covers, what remains outside its boundary and what would make the decision change. It becomes possible to revisit the decision without reconstructing the argument from whoever happened to be in the room.

The next seam remains even after that consolidation: Python produces data the application serves. Keeping that boundary explicit is what lets the pipeline change without turning the frontend into a workflow engine.

Pipelines produce records the product can serve

Python owns the processing side of the design: parsing, chunking, enrichment, embeddings, scoring and workflow decisions. Convex holds the results the product needs to read. Next.js renders the experience around those results. Each part can change without acquiring the other parts' responsibilities.

A reader-facing page shouldn't need to understand how to repair an extraction before it can display a document's status. A parser shouldn't need to render a page to announce that it failed. The shared record between them needs enough information for both jobs: identity, processing status, useful output when available and an explicit failure when it isn't.

One-way seam diagram: a Python pipeline block on the left writes through a single arrow into a Convex core, and surface blocks on the right read outward through fanned arrows. A back-arrow from surfaces to pipelines is drawn and crossed out.
The seam is one direction: pipelines write, application interfaces read.

The original note described a one-directional seam: pipelines write, pages read. That's a useful account of result delivery. It leaves room for a separate command path when a person requests work. An interface can ask the backend to start a job and then read its status; it doesn't have to keep a long processing request open or acquire the pipeline's internal state.

The distinction is between asking for work and owning its execution. A processing record can survive a browser tab closing. A durable result can be read by another page or another authorized client. The producer and consumer agree on the record, so neither needs to depend on the other's private implementation.

Configuration needs an identity too

The original architecture put tunable pipeline behavior in the database: model choices, prompt templates, rubric definitions and gate thresholds. The pipeline could read those values without changing its implementation. That's a useful separation between a change to the system's capabilities and a change to an existing policy.

A parser replacement changes what the program can do. Selecting an already-supported parser changes which capability a run uses. The first calls for a code change. The second can be configuration, provided the application validates it and controls who can change it.

A control panel of labeled dials, model, prompt, rubric, threshold, wired directly into a database core, with a live-in-seconds pulse traveling to a pipeline. Off to the side, a slow crate labeled DEPLOY TRAIN is reserved for a card reading NEW CAPABILITY ONLY.
The dials wire straight into the database, and the deploy train is reserved for new capability.

The timing of that read matters. A run that loads configuration once at startup keeps a consistent policy for that run. An edge that reads the current threshold each time can react to a change while work is in flight. Those are different designs, and the source note used both descriptions. A team has to choose deliberately where it wants consistency and where it wants immediate policy changes.

I'd record the configuration identity with the run and the policy used with the gate decision. Otherwise a reviewer sees a failed score under today's threshold and can't explain why the same score advanced yesterday. The result needs to identify the rule that acted on it.

An admin control also needs bounds. A mistyped value can stop useful work, while an overly permissive value can admit bad work. Permissions, input validation and a record of changes belong beside the dial. Moving a constant into a database changes how it's managed; it doesn't make changing it consequence-free.

Count the full operating cost

The cost discipline in the original note was to avoid paying for idle infrastructure that wasn't producing value. I still want that question asked. A usage-based service can fit bursty work better than capacity kept running for an occasional batch. The workload and the current billing terms decide whether that's true for a particular deployment.

A gouache-dark room, empty, with a single antique electricity meter on the wall glowing amber, its dial spinning, cables running to nothing. Deep navy shadow everywhere else.
Idle infrastructure is a meter running in an empty room.

A design statement that everything scales to zero can't stand in for a bill. Stored data remains. Checkpoints remain. A service can charge for resources other than active execution. External calls may be charged even when the overall workflow fails. Someone also has to maintain credentials, understand failures and keep the retained data usable.

The comparison should include the cost of the rejected service and the cost of replacing its work. A library import may be cheap while the checkpoint database and operating work are substantial. A managed platform may have a visible fee while bundling work the team would otherwise absorb. Hiding either side makes the result look leaner than it is.

The same reasoning explains the language split. My Python practice centers on data models, processing and instrumentation. TypeScript fits the web application and the backend contract in this design. Forcing one language into every job would need an advantage large enough to justify giving up the libraries and conventions already doing useful work on the other side.

A gouache character sheet on dark navy: a lone adventurer figure rendered in brass and cream, with stat bars where three stats are pushed to full and glowing, and a dozen other stats near zero, deliberately dark. An unused ornate weapon rack stands behind.
A lean stack is a character build: all points in the stats the run actually uses.

Min-maxxing allocates the work to parts that can do it well, including the work needed to operate them. A new part earns its place when a named requirement survives the audit and the existing parts can't satisfy it at an acceptable cost.

The database and the producer/consumer contract take us only part of the way. Inside Python, we still need to coordinate steps, keep intermediate results and recover from failures. That's where a separate orchestrator used to feel like the default, and where decomposing the job changes the decision.

Decompose orchestration before choosing an orchestrator

A pipeline has operations, a rule for which operation runs next and some record of what the earlier operations produced. Nodes, edges and state describe that anatomy. The labels on the nodes can say parse, enrich, embed or publish; the coordination questions remain recognizable.

The anatomy of every pipeline: labeled nodes connected by directed edges, and beneath the whole graph a single wide band labeled THE THING THAT REMEMBERS, with thin taps rising from the band into each node.
The band under the graph is the thing that remembers, with a tap rising into every node.

The original audit broke the orchestrator's job into eight responsibilities. It's a working inventory for this workload, not a claim that every platform contains nothing else. The point is to discover which responsibilities the application team would inherit if the platform disappeared.

ResponsibilityWhat the pipeline needs
Dependency orderingA step starts after the inputs it requires are available.
SchedulingA time or event can initiate a run.
RetriesEligible failures get bounded additional attempts.
State passingLater steps can read earlier results under a known contract.
BranchingThe next operation depends on a result or condition.
Fan-outWork expands into per-item operations at runtime.
ObservabilityAn operator can inspect execution and diagnose failures.
RecoveryAn interrupted run can continue under a defined replay policy.

A program can express each behavior. That observation alone doesn't tell us whether writing or assembling the program is worth doing. The bundle may still be valuable. We need to inspect what a library supplies, what the runtime supplies and what remains application work.

A gouache-dark hardware store aisle at night, shelves towering with boxed machinery into the shadows, while at the counter a small tray holds three simple brass fittings under one amber lamp.
The aisle sells the whole hardware store. The job needed three fittings.

Scheduling and retries need execution policies

Time-based scheduling can trigger a function. An arriving document can trigger work through an event or request path. Those mechanisms initiate execution. A scheduler doesn't, by itself, define what happens if a previous run is still active or a trigger arrives twice.

The original note treated scheduling as largely solved plumbing. For a small application, the initiating mechanism may already exist. The audit should still ask about overlapping runs, missed triggers, duplicate delivery and the ownership of work that hasn't completed. A cron expression describes a time. The application needs a policy for what a run means.

A timeline from 1975 to 2026. At the far left, a small stone marker labeled CRON. Along the rest of the line, progressively larger and fancier monuments labeled with generic scheduler product shapes, each with a small plaque reading STILL A CRON STRING INSIDE.
Scheduling was solved in 1975. The monuments came later.

Retries need a similar separation. A library can handle a delay, backoff and another attempt. The application has to decide which errors deserve that attempt. A temporary connection failure may be eligible. A document rejected by a deterministic validation rule needs correction or an explicit terminal result.

Retrying every exception can repeat a bad request until it consumes the allowance and still produces nothing useful. Retrying no exceptions can discard recoverable work. Neither a platform nor a graph can infer the domain's policy from the presence of an error.

An error triage board: incoming failures sort into two bins, TRANSIENT with a backoff spiral and a retry budget meter, and DETERMINISTIC with a straight route to quarantine stamped ZERO RETRIES. A footnote reads: the classification was always your job.
Failures sort into transient and deterministic bins, and that sorting was always the real work; the retry around that work is a solved library.

The scope of a retry belongs in that policy too. Repeating a node may repeat several operations inside it. A checkpoint can narrow the work the engine needs to revisit, but the code inside the replayed unit determines which external actions might happen again. The unit of recovery should match a unit the team can reason about.

Traces connect execution to documents and decisions

My Python stack uses Logfire for structured observability. The useful property is the ability to connect an operation with the document, decision or error it concerns. A status row says that work failed. A structured record can help identify which input failed, which route it took and what policy it used.

Side by side: a run-list screen of colored status rows with a scrollbar, versus a query console where one typed question about repair-path documents since Tuesday returns a small exact result table drawn from a span store. The console's answer is lit; the run list just continues.
One typed question returns an exact table from the span store, while the run list just keeps scrolling.

For the document pipeline, I want to ask which inputs entered repair, what failure class sent them there and whether a change coincided with a different failure pattern. Those are proposed operational questions, not measurements from this article. They tell us which fields execution evidence must retain.

A platform UI can also provide valuable inspection, and some operators need that interface. Calling every run list useless would confuse the presentation with the underlying records. The audit should compare the questions each approach can answer and the people who have to answer them.

A trace also differs from a checkpoint. A trace records execution evidence. A checkpoint supplies state the workflow engine can use to resume or inspect a run. An operator may need both to understand a failure: what the run knew and what an external operation reported.

A directed graph of five nodes where a bright pulse of state flows along the edges from the entry node to the exit node, the moving dot carrying a small document glyph that grows as it passes each node.
A run is just state flowing along edges, gaining weight at every node.

Backfills add historical selection and safe repetition

A backfill runs work over an earlier range of documents, dates or identifiers. The graph may be the same graph used for newly arriving data. What changes is the selection of inputs and, often, the version of processing we want applied to them.

The difficult questions are whether the old input is still available, what the revised output should replace and whether repeating a write is safe. An upsert keyed to a stable identity can help, but it doesn't decide whether a historical result should overwrite a newer one. The application needs that conflict policy.

A nightly pipeline loop drawn once, with two direction arrows: forward over tonight's batch, and backward over a shaded historical range labeled REPLAY THE RANGE. At the sink, one stamp reads UPSERT, same stamp both directions. A dim platform button labeled BACKFILL sits unplugged.
One loop runs in both directions and stamps the same upsert at the sink, while the platform's backfill button sits unplugged.

A platform's backfill interface may make selection, visibility and scheduling easier. A library-based design needs enough of those capabilities for its operators. The comparison is complete only when the historical range, progress, failed subset and sink behavior are all accounted for.

Dependency ordering can be simple function composition for a linear path. Branching, dynamic width and resumable state are where an application benefits from a more explicit model. Handing every function the same mutable dictionary leaves too many rules implicit: which keys exist, who can write them and what happens when two workers return updates.

Two towers compared. Left: a DAG file locked in a registry crate, with a control-plane scaffold grown around it: UI, API, mapper, run database, workers. Right: the same graph drawn as a small data structure inside a program card, surrounded by nothing but the program's own tests and deploy arrow.
Register your control flow with a server and the server needs a control plane. Keep it in the program and it needs nothing.

Keeping graph definitions in the application can reduce the machinery between the transformation logic and its execution. It also leaves deployment, triggering and operations with the application team. The graph is a data structure; the running system still needs an owner.

Two timelines stacked: CONSTRAINTS, where a wall labeled coordination-needs-a-fleet visibly falls partway along; and DEFAULTS, where the install-a-platform habit runs on unbroken past the fallen wall, shaded as running on memory.
The constraint fell. The default kept walking. The aisle lives in the gap between those lines.

The capability audit therefore leaves a useful cluster of requirements to inspect together: state and its merge policy, routing, runtime fan-out and recovery. LangGraph puts those mechanisms in the Python program. The state contract is where their relationship starts.

Eight primitive tiles in a row. Four fade to dim with the label SOLVED IN YOUR LANGUAGE: ordering, scheduling, retries, observability. Four glow violet with the label THE HARD FOUR: state, branching, fan-out, recovery.
Cross off the solved four. The hard four are the real product.

State names the data and the rules for combining it

LangGraph lets a program declare state, define nodes that read it and return updates, and connect those nodes into a workflow. A node can call a model, parse a file or perform ordinary application work. The graph mechanism doesn't require a language-model decision at every step.

That distinction is what made the library relevant to this audit. An application that processes documents needs intermediate results even when the processing is deterministic. The parser produces text, the chunker produces chunks and another operation produces scores. A later operation needs to know which of those results exists and what shape to expect.

A gouache-dark split scene: on the left a night market of crumpled unlabeled paper bags spilling into shadow; on the right a brass shipping manifest under an amber lamp, every line itemized, stamped, and legible.
Untyped state is a pile of paper bags. Typed state is a manifest.

A dictionary named context can carry all of that. It can also hide every agreement the functions are making. One function expects doc_id, another writes document_id, and a third assumes chunks already contains validated records. A declared state schema gives the team one place to express those expectations.

Reducers make merge policy explicit

A small Python example shows the difference between a scalar channel and an accumulating one. This is a schema illustration, not a complete executable pipeline.

from operator import add
from typing import Annotated
from typing_extensions import TypedDict

class PipelineState(TypedDict):
    doc_id: str
    chunks: Annotated[list[str], add]

The chunks annotation associates the channel with an addition reducer. The intended operation combines lists rather than treating each new list as the complete replacement value. A node can return an update to a named part of the state without knowing how every other part is maintained.

Two parallel streams of small value tokens flow from the left and meet at a single funnel node labeled REDUCER, emerging as one ordered ribbon on the right. The funnel shows the contract: new = reducer(left, right).
A reducer folds parallel writes into one value, deterministically, by a rule you wrote.

That declaration is small, but the policy it expresses can matter throughout the graph. Appending chunks, combining results by identity or keeping a selected score are different operations. The team should choose a reducer because its behavior fits the meaning of the data, rather than because it makes concurrent writes stop raising errors.

The list example is also a useful warning. Appending two result lists retains both lists' contents. It doesn't sort them into source order or discover that two items describe the same chunk. A reducer only supplies the rule we wrote down. If a consumer requires a particular order, make the order part of the representation and resolve it explicitly before the consumer uses the result.

Side by side: a loose cloud of arrows all mutating one amorphous blob labeled context, versus a column of named typed channels, doc_id, chunks, scores, each with its own small reducer gate and exactly one write path per node.
Typed channels with a declared merge policy sit on one side, and a pile everyone mutates sits on the other.

A scalar channel without a merge policy also needs a clear writer. The original example called that behavior last-write-wins. That's too casual a description for parallel work: two competing updates need a defined rule, not an assumption about which worker finishes last. Use one owner for a value or define how multiple updates combine.

Deduplication starts with identity

A reducer can express a deduplication policy. Two feeds can deliver the same article, or two extraction passes can identify the same entity. A channel keyed by a canonical identifier gives a merge rule somewhere to express what should happen when those results meet.

The identifier does much of the work. A rule can't combine two records as the same entity unless the system can establish that identity. Similar titles, overlapping text and an identical external identifier are different kinds of evidence. Picking a key is a domain decision the workflow library can't make for us.

Two feed arrows deliver the same article card twice into a channel gate stamped KEYED: LATEST WINS. One folded card exits. Below, a crossed-out pipeline stage labeled DEDUPE SERVICE sits unbuilt, its outline dotted.
Dedupe as a channel policy: the collision folds at the gate, and the stage never gets built.

The conflict policy needs equal attention. Keeping the latest record requires a definition of latest: arrival order, source revision or an effective timestamp. Keeping the highest-confidence extraction requires a rule for what that confidence measures and whether scores from different extractors are comparable. A reducer can encode a decision after we make it. It can't supply the decision.

The behavior under repetition matters too. Appending an identical update twice gives a different result from appending it once. A keyed replacement rule may tolerate repetition differently. Work that can be replayed needs a merge policy that accounts for that possibility, particularly where duplicates would change a count or downstream action.

This is why I like putting merge semantics beside the state declaration. A reviewer can find the policy without following every writer through the program. It also becomes possible to ask whether the policy matches the data before the first wide fan-out makes conflicting assumptions expensive to untangle.

A type declaration and runtime validation do different jobs

I use Pydantic as a central place to define structured data across integrations and workflows. The benefit is having an explicit model of what passes between operations, rather than allowing each integration to invent its own version.

A relay of systems, parser, graph nodes, database, surface, passing a single sealed contract card hand to hand. At each handoff a small validation stamp strikes the card. Below the relay, a rejected malformed card bounces off the first stamp and never enters the chain.
Pydantic-as-IR: one shape re-signed at every handoff, and malformed data never boards.

The compiler comparison helps explain the intent. Each processing stage consumes a known representation and produces a representation the next stage understands. An extracted document, a chunk and a scored result have different jobs, so their contracts should make those distinctions visible.

A Python type annotation alone doesn't prove that runtime values satisfy the contract. Choosing a Pydantic state model also doesn't establish that every partial update, external response and serialized record has been validated at the point the application needs. Validation belongs at explicit boundaries, with a defined failure outcome.

A parser response should become a validated record before downstream code treats it as one. A database write should carry the shape and identity the consumer expects. If a representation changes between Python and TypeScript, the team needs to maintain that agreement rather than assume a shared name makes it true.

The result is a state model whose meaning a person can inspect: what a field contains, who writes it, how updates combine and where malformed data stops. Persistence takes the next step. It makes selected execution state survive the process that produced it.

A checkpoint preserves state for inspection and recovery

A durable checkpointer lets a workflow retain execution state beyond the life of one Python process. The run has an identity, and the persistence layer associates its saved state with that identity. The application can use the retained history to inspect or continue work according to the engine's execution rules.

The Postgres example attaches a saver when the graph is compiled. An in-memory saver is useful for an example or local development, but its presence doesn't mean the history survives losing the process.

The following excerpt preserves the source's API shape. It omits graph construction, node definitions, credentials, deployment and error handling, and it hasn't been executed as part of this article.

from langgraph.checkpoint.postgres import PostgresSaver

with PostgresSaver.from_conn_string(DB) as checkpointer:
    checkpointer.setup()
    graph = builder.compile(checkpointer=checkpointer)
    config = {"configurable": {"thread_id": "ingest-example"}}
    result = graph.invoke({"doc_id": "example-document"}, config)

A production implementation has to establish the storage, lifecycle and connection behavior appropriate to its installed library version. A compile argument connects the engine to persistence. It doesn't operate the database on the team's behalf.

A five-node graph with a filmstrip beneath it: one frame per step, each frame a small card capturing the full state at that moment, connected by a thread line labeled with one thread id. The last frame glows as the current state.
A checkpoint is the whole graph state, snapshotted per step, filed under a thread.

Recovery depends on where work was saved

Consider an illustrative document flow with extraction, chunking, scoring and a final write. The extraction result is saved before later work begins. A later operation fails. Retained execution state can let the workflow continue without starting from the original input and repeating every completed stage.

That's a substantial capability. Describe the checkpoint and the replay unit without guaranteeing that no paid operation ever runs twice. The engine can preserve work at its defined boundaries. Code between those boundaries may run again.

An external write makes the distinction concrete. A node sends a result to the database. The database accepts it, but the worker fails before it records the acknowledgment in its own durable state. Recovery now has to deal with an operation that may already have happened.

The workflow needs a stable identity for the intended write and a sink policy that handles repetition or lets the operator reconcile uncertainty. A checkpoint doesn't join the external service's transaction merely because the node called that service. This is the same seam the database audit exposed, now seen from the worker's side.

A 2 a.m. incident card split in two. Left: an engineer silhouette facing a tower of platform layers, each a possible liar: UI, scheduler, runner, run DB, then the code. Right: the same silhouette facing one lit screen of plain code and one checkpoint query. A clock in the corner reads 02:00 on both sides.
The 2 a.m. difference: five layers that might be lying, against one screen and one query.

I'd make that failure part of a pilot. Interrupt work around the expensive or consequential operation, resume it, and inspect both the execution history and the sink. A successful return from the graph proves less than a reconciled output. The question is whether the system did the intended work under the failure the team needs it to survive.

A paused run needs a way back in

Durable state also supports human decisions in the middle of a workflow. A run can retain the work it has completed and wait for approval, clarification or review before proceeding. The waiting state needs to survive, and the application needs a defined way for an authorized person to resume that same run.

A gouache-dark night ledger room: a heavy open ledger on a stand writes itself by lamplight, each line a small state seal, while through a doorway an operator sleeps in a distant lit cottage. A bell rope hangs beside the ledger, unpulled, waiting for morning's decision.
The ledger remembers so the operator can sleep. The run resumes when morning answers.

The graph can supply the pause-and-resume mechanism. The surrounding product still owes the person an explanation of what's waiting, the material needed to decide, permission to act and confirmation of what happened after the decision. A paused checkpoint with no discoverable review record is unfinished operator work.

No worker should have to remain occupied solely to remember that a person hasn't answered. That's the architectural benefit. The deployment still determines whether idle compute is released and what storage or service charges remain. Human waiting and zero total cost are different claims.

Replay is useful only when its inputs remain intelligible

Time travel lets a developer inspect historical state and, where supported, fork execution. That's more concrete than trying to infer every intermediate value from final output.

A branching history: a main line of checkpoint frames runs left to right; from frame three, a violet fork drops down and runs forward as an alternate timeline labeled RERUN WITH FIX. A magnifier hovers over frame three, labeled inspect the past as data.
Time travel is a query over your own history, then a fork.

A retained state doesn't freeze the entire outside world. An external page may change, a model may change and a referenced artifact may disappear. A replay that fetches those resources again can produce different results. A team that needs reproducibility should decide which input bytes, configuration identities and external responses it must retain.

The checkpoint history and the trace answer related questions. The state explains what the workflow retained. The trace can explain what an external call reported. Reconstructing a consequential decision may require both, together with the version of the program that interpreted them.

Keep histories useful and keep their cost visible

Saving large intermediate artifacts repeatedly can increase storage and write overhead. Small states can refer to bulk artifacts held elsewhere. That makes sense when the reference remains valid for as long as the history needs to be inspected or replayed.

A retention shelf: recent checkpoint frames stored dense and bright on the near shelf, thinning to final-state-only frames on the far shelf, and a trapdoor labeled EXPIRED beneath. To the side, a state card passes a heavy artifact to an external store and keeps only a reference tag, staying slim.
Slim states, real retention windows and a checkpointer you can decline make durability a dial.

Retention therefore has to cover the checkpoint and its dependencies. Deleting a referenced document while keeping its history may leave a record that can explain a route but can't reproduce processing. Different uses can need different retention: incident diagnosis, replay, audit and permanent final results aren't interchangeable obligations.

Thread identifiers separate histories at the workflow level. They still need a naming policy so unrelated runs don't accidentally reuse an identity. A thread identifier also isn't an authorization policy, and distinct threads can still call the same external system or modify the same business record.

Four horizontal lanes, each a thread with its own id and its own chain of checkpoint frames, pulses moving at different offsets. Walls between lanes are solid; nothing crosses. Label: a hundred runs, a hundred histories, zero shared anything.
Threads give concurrent runs isolated histories, by construction.

Persistence makes the state durable enough to support recovery and human decisions. A routing rule gives those decisions somewhere to act. That rule needs to say what failed work is allowed to do next.

Conditional edges make failed work take a visible route

A conditional edge selects what happens next from the state the workflow has reached. In this example, a routing function examines an extraction result and returns a destination. Good-enough work advances, recoverable work goes to repair, and work the system can't safely use goes to quarantine.

That's a place to put a quality policy the whole execution path can respect. The check runs before the dependent operation. A low-quality extraction can be held before the embedding step spends resources turning that extraction into a searchable representation.

One node labeled EXTRACT with three outgoing edges fanning to EMBED, REPAIR, and QUARANTINE. On the edge junction sits a small function card reading state in, route out. The EMBED edge is lit; the others wait.
A conditional edge reads the state and picks the next node, which makes it a guardrail with a return value.

The following Python shape is illustrative. The application supplies the state schema, score, threshold and registered destinations. The order of the conditions is part of the policy, so a real implementation has to decide which failure takes precedence when more than one applies.

def route_on_quality(state: PipelineState) -> str:
    if state["extraction_score"] < threshold:
        return "repair"
    if not state["entities"]:
        return "quarantine"
    return "embed"

builder.add_conditional_edges("extract", route_on_quality)

The example's empty-entities condition belongs to its workload. Some valid documents may contain no entities worth extracting. A gate needs the domain's definition of an unacceptable empty result, rather than treating every empty collection as a failure.

An empty success can hide the actual failure

The failure form I want to avoid is a parser exception converted into an empty list while the run reports success. Downstream operations can accept that empty list and complete their own jobs correctly. The workflow then records successful execution while producing none of the content the product expected.

A pipeline where one early node swallows a red failure and emits a small green check. The green check multiplies as it moves right, stacking into a wall of checkmarks, while beneath the line a shadow region labeled EMPTY RESULTS spreads wider at every step.
One swallowed failure turns into a wall of green checks while the empty results spread underneath.

The mistake is in the outcome contract. An empty result, a rejected document and a failed extraction mean different things. They need distinct representations or enough explicit status for a later operation to distinguish them. Otherwise the next operation has to guess whether no content was a legitimate conclusion.

A visible repair route gives degraded work an identity and a destination. Quarantine needs the original input or a durable reference, the reason it stopped, the relevant policy and a way to inspect what happened. It also needs an owner and a condition for reconsidering the work.

A gate can block work that violates its check. It can't guarantee that everything it admits is correct. A weak score, a missing condition or a misconfigured rule can still pass bad work.

A gouache-dark pair: on the left, a wall of monitoring screens glowing over an empty chair, one chart quietly falling; on the right, a single heavy brass gate across a conveyor, holding a flawed part mid-line under an amber inspection lamp.
The chart falls over an empty chair while the gate holds the flawed part mid-line.

That limit is a reason to inspect admitted work as well as rejected work. Counting quarantine gives one view of the policy. Reading representative results gives another. A rule that rejects everything can have a perfect record of preventing bad output while making the product useless.

Evaluations can inform routing without becoming authority by default

The original content-pipeline design extended the pattern to rubric scores, voice checks and banned-pattern detectors. Those checks produce findings the routing function can read. The function can advance the work, request a bounded repair with feedback or require a human decision.

An evaluation card wired directly into an edge: a rubric with lit score bars for voice, structure, and grounding feeds a routing junction with three exits labeled ADVANCE, RETRY WITH FEEDBACK, and HUMAN. The wire from rubric to junction is the point: the eval is part of the edge.
An eval rubric wired to an edge makes QA run on every unit, not in a sprint ritual.

The useful separation is between evaluation and routing. A checker produces evidence under a named rule. A policy decides what that evidence permits. Combining those jobs invisibly makes it difficult to tell whether work failed because the content was wrong, the checker was wrong or the configured threshold changed.

A pattern match can identify a sentence worth reading. It doesn't settle the sentence's quality in context. A rubric score has the same limitation at a different level. The system needs a policy for a checker error or unavailable evaluation, as well as policies for an unfavorable and a favorable result. An unavailable check should retain that status.

An auto-eval can be configured as a switch on an edge. That's a useful configuration goal: reuse the evaluation mechanism and name the rubric and policy rather than rebuilding the wiring. It doesn't make evaluating free. Each added check can add latency, operating cost and another way to reject useful work.

A switchboard of small toggle switches labeled voice, stop-words, grounding, structure, each wired to an edge on a graph beneath. Two switches are up and their edges glow; a hand-shaped cursor flips a third, and its edge ignites. A tag reads: rubric definitions come from the database.
Auto-evals work as toggles: name the rubric, flip the switch, and the edge gates from then on.

The audit question therefore applies to checks too. What failure does this check address? What evidence does it produce? What's allowed to happen when it fails or disagrees with another check? More checks earn their place when they improve a consequential decision.

Record which policy made the decision

A gate should leave enough evidence to explain why a particular unit advanced or stopped. I want the score or finding, its meaning, the selected route and the policy identity attached to the document's execution record. The state and traces can then connect a decision with its evidence.

A gate node casting a paper trail: each passing unit drops a small score slip into a ledger that accumulates into a drifting distribution curve over weeks. A margin note on the curve reads: the series audits the gate. The gate itself holds a single declared policy card.
The gate acts once per unit and writes down why. The series it leaves audits the gate itself.

That record also lets an operator examine changes in the policy's results. A higher rejection rate might indicate worse source data, a changed parser, a stricter rule or a broken evaluator. A rejection count alone can't distinguish those explanations. The retained context tells the operator where to investigate.

The original note proposed tightening a threshold during an incident. That can be useful when the threshold measures the damage we need to stop. It can also quarantine good work or miss corruption the score doesn't detect. The incident procedure needs an observed failure, an explicit policy change and a way to inspect the effect.

An incident timeline from Friday 4 p.m. to Monday: corruption begins, a threshold dial turns up, a quarantine counter climbs visibly while the main line stays clean, the source recovers, the dial turns back, and the quarantined batch re-runs through the gate. No deploy marker appears anywhere on the line.
The whole incident fits on one line: a dial up, a counter watched, a dial back, and no deploys.

Whether the change affects running work depends on when that work reads configuration. A startup snapshot won't change halfway through a run. An edge that deliberately reads current policy may change its behavior. Record the choice so recovery doesn't reinterpret an old decision under a rule that didn't exist when it was made.

A subgraph can keep a complicated extraction-and-validation stage understandable by giving it its own internal steps and exposing a smaller result to its parent. That's an application contract to design, not a promise that every child state or side effect is isolated automatically.

A parent graph of three large rooms connected left to right, where the middle room opens in cutaway to reveal its own small interior graph with private state channels. Only one labeled conclusion pipe exits the room into the parent flow.
Subgraphs scope state like rooms scope noise.

Once the gate can distinguish outcomes and preserve its decisions, the workflow can widen without treating every item as the same case. Runtime fan-out lets the data determine that width.

Send lets the data determine the work's width

A pipeline definition can name a processing operation before it knows how many inputs will arrive. The batch might contain a few documents or a larger collection. Each document may produce a different number of chunks. Writing a separate node for every possible item would confuse the operation with the number of times it needs to run.

Dynamic fan-out keeps those ideas separate. The program defines the worker. At runtime, a routing function inspects the available work and produces one dispatch per selected item. The input determines the width within the resources and policy the application allows.

One node facing a batch of incoming documents. Edges fan out from it, one per document, and the fan visibly matches the count of the batch: the data decided N, at run time. A dim ghost fan behind shows the author-time guess, wrong in both directions.
The fan matches the data, decided at run time. The author never had to guess N.

LangGraph's Send API expresses that pattern with a destination and the state passed to that invocation. The source used a list comprehension, which is a compact way to show that the count comes from the data. This is a partial wiring illustration. It doesn't supply the worker, its schemas or an operating configuration.

from langgraph.types import Send

def fan_out(state: PipelineState):
    return [
        Send("process_doc", {"doc": document})
        for document in state["docs"]
    ]

builder.add_conditional_edges("ingest", fan_out)

The useful reduction is in the coordination code the team writes. It doesn't mean the list comprehension supplies unlimited compute. The engine still runs on a particular runtime with finite memory, connections and capacity. A request to an external service also remains subject to that service's limits.

Fan-in needs a result policy

Parallel workers produce partial results. The parent workflow needs to combine them in a form its next operation can consume. A reducer on the relevant state channel can supply that combining rule, so the fan-in uses the same policy the state model already declares.

Left: a fan-out ending in a loose heap of result fragments, labeled a pile. Right: the same fan-out closing through a reducer funnel into one ordered ribbon, labeled an answer. The funnel carries the append rule from the state declaration.
Fan-out without a reducer is a pile. With one, it's an answer.

That's one mechanism serving two related jobs: ordinary state updates and the return from wider work. The benefit depends on making the policy fit both. A list append may suit independent results that will later be sorted. A keyed merge may suit results with stable identities. A score channel may need one owner or a reduction rule rather than several competing replacements.

The fan-in also needs to know what constitutes a complete result. A batch with failures may produce a useful partial answer, or the downstream operation may require every item. The application should choose between those cases and represent the difference. Missing workers shouldn't disappear into a result collection that looks complete.

Three machine parts meshed as one mechanism: a fan plate labeled SEND for runtime width, a folding gear labeled REDUCERS for deterministic collapse, and a reel labeled CHECKPOINTS for durable spread. Arrows show each part gripping the other two; a small plate beneath reads: any one is a feature, together they are the product.
The interlock is the product: width from Send, collapse from reducers, durability from checkpoints.

State, routing, Send and checkpointing interlock here. The state supplies a result contract, routing chooses the work, Send supplies runtime dispatch and persistence can retain progress at the engine's defined boundaries. The application supplies the meaning of success, the resources and the policy for unfinished work.

Per-invocation state doesn't isolate every side effect

Passing each worker the data intended for that invocation reduces the temptation to have every worker mutate a shared context object. The code becomes easier to inspect: this worker receives this document and returns an update of this kind.

A mechanical cutaway: Send tokens branch into isolated worker cells, each with a private state card. All cells drain into a single channel rail whose gate is stamped with the reducer rule from the schema declaration. One rule plate serves both the branching and the merging, highlighted as the same part.
Private state out, declared reducers back in. The merge policy is the one you already wrote.

That's a useful programming boundary, but it doesn't protect everything a node can reach. Workers can still share a database record, write the same file, exhaust a connection pool or call the same rate-limited provider. A helper that closes over mutable global state can create a race even when the graph's state channels are well designed.

Isolation has to be evaluated across the whole worker, including its external effects. Likewise, a checkpointed fan-out doesn't prove the sink has received exactly one output per input. Identity and replay policy remain part of the worker contract.

A bounded design can make those questions tractable. Choose a unit of work small enough to identify and retry, decide how much may be in flight, and retain a visible account of unfinished items. The appropriate bound comes from the work and the runtime, not from a universal document count.

A ruler laid across fan-out scales: a violet-lit span from tens to thousands of items labeled A ROUTING FUNCTION, and beyond a marked boundary, a dim span labeled millions sustained, DIFFERENT AISLE. The boundary tick is drawn heavy and unambiguous.
The claim, with its boundary drawn: product-scale fan-out is a routing function's job.

A judge can select candidates after the fan-in

The original note proposed another use of the same shape: generate several candidates, collect them and evaluate which should advance. Headline candidates were its example. Competing parsers or extraction methods can follow the same general pattern when alternatives have a useful role.

Candidate generation incurs cost even when a candidate is rejected. What selection can avoid is sending every candidate through later expensive work. The evaluation therefore needs to happen at a point where it can improve the result enough to justify the extra candidates and the judging step.

A judge also needs a rule for the case where none of the candidates meets the requirement. Selecting the highest score from a bad set still selects a bad candidate. Advance, repair and human review remain distinct outcomes. The candidate set and evaluation evidence should remain inspectable for whatever period the application needs to explain that choice.

This is a design pattern, not evidence that every difficult step should fan out to several models. A deterministic operation with one correct output may benefit more from a validation rule. A noisy extraction may benefit from an alternative method. The workload has to earn the additional branch.

Per-item processing keeps the graph tied to the product

Document pipelines offer a concrete use for runtime width. Each article can be parsed and chunked. Each chunk can be processed under a narrower contract. Results return to the article-level record, where the workflow can decide whether the document is ready for the next stage.

Three stacked lanes labeled PER DOCUMENT, PER CHUNK, PER RECORD, each showing the same fan and fold silhouette at a different scale, nested like matryoshka outlines. One caption line: the same shape, all the way down.
Per-document, per-chunk, per-record: the everyday pipeline is fan and fold, all the way down.

Nested fan-out can also multiply work, so the team needs to understand where width expands. A document-level limit alone may leave a large chunk-level expansion uncontrolled. The graph should remain explainable at both levels, with enough state to connect a result to its document and chunk.

The pieces now describe an application workflow rather than a set of unrelated features. Following one document through them makes it possible to see which responsibilities the library covers and which remain outside it.

Follow one document through the whole workflow

The source note assembled the argument around an article-processing pipeline: input enters a graph, work fans out by article and chunk, results combine, a quality gate chooses a route, and accepted output reaches Convex. A checkpointer retains execution history while traces record operations. The frontend reads the served result.

That's the design to inspect. Its execution still needs to be demonstrated. A complete implementation needs the actual producers, schemas, workers, permissions, sink writes and runtime configuration behind each of those operations.

The site's article pipeline as one complete annotated graph: ingest node, per-article fan, nested per-chunk fan folding through reducer gates, a quality gate with three routes including a loud quarantine, a transactional write into a Convex core, reactive pushes to page icons, and a checkpoint reel filing frames beneath the whole run.
The whole machine on one card: fans, folds, gates, one transaction, and a checkpoint reel underneath.

I'd follow one document through the flow by asking what each boundary adds to its record. The input begins with an identity and a source. Processing adds derived material. Evaluation adds findings. A routing decision adds a permitted next action. A write produces a result the application can read. Each transition needs enough evidence to explain how it relates to the one before.

Different inputs need different preparation

I built on Archon and changed its ingestion approach for material that included Markdown, textbooks in PDF and YouTube transcripts and comments. That work used document-type-specific chunking and retrieval connected to local agent workflows.

The transferable lesson is that those sources don't arrive with the same structure. A clean article has headings and paragraphs. A transcript reflects speech and may need different segmentation. A PDF's extracted text can have its own layout problems. Treating every source as an interchangeable string can lose the relationships retrieval needs.

That case supports the need for different processing paths. It doesn't establish that this site's current ingestion is the same system, or that a particular graph produced the page now on screen. The connection is the engineering problem: preserve enough of each input's meaning that later retrieval can return a useful passage.

A routing decision can select preparation appropriate to the source. A subgraph can keep the details of that preparation out of the parent flow. The parent still needs a clear result contract, so it can distinguish a usable document from an extraction that stopped or needs review.

Combine results before deciding what may advance

Inside a document branch, chunk-level work can produce partial results. Those results need stable references back to the document and to the chunk they describe. Reducers can combine them under the declared merge policy, and a document-level operation can evaluate the assembled result.

This is where several earlier choices meet. An append reducer preserves results but doesn't prove coverage. A score needs a meaning and an evaluator identity. A missing chunk needs an outcome. The document-level gate should have enough information to decide whether the result meets the requirement, needs repair or must stop.

Repair also needs a stopping condition. Feeding the same failed output back through the same operation without a changed input or method can consume attempts without learning anything. A repair record should identify what failed, what the next attempt changes and what happens if that attempt still fails.

The graph makes that route visible. The policy makes it useful. A visible loop with no budget or exit can be just as troublesome as an invisible retry.

Make the final write and its acknowledgment inspectable

Accepted output enters the data store through a defined write operation. Related database updates can share a transaction where the backend supports it. The write should associate the served result with its source and processing identity so a later reader can distinguish a current result from an old one.

The worker also needs to handle the uncertain acknowledgment discussed earlier. A durable checkpoint before the write preserves its intended input. A durable result after the write preserves what the database accepted. The recovery procedure has to reconcile them when the process fails between those observations.

A reactive page can then display the stored state. It can show that processing is complete, that review is waiting or that an input failed. The frontend doesn't have to infer the workflow from whether a body field happens to be empty.

The operator's inspection follows the same record in the other direction. Start with the unexpected served result, find its document and run, inspect the gate finding, then examine the relevant checkpoint and external-call trace. The point of retaining those links is to let the investigation start from what the person can see.

None of that requires a language model to choose the next operation. A model may perform extraction or evaluation within a node. A deterministic routing function can still decide what its output permits. That's why a library associated with agents can be relevant to ordinary document engineering.

The assembled workflow has covered substantial coordination work in the application. To decide whether an orchestrator is redundant, we now have to compare that implementation with the jobs the platform does beyond this path.

A library can replace coordination inside an application

My position is that a separate orchestrator has to earn its place in a single team's product pipelines. Typed state, declared merges, conditional routing, runtime fan-out and durable persistence give the application substantial coordination capability. Installing another platform merely because the work is called a data pipeline is a default worth challenging.

The original note called the middle of the orchestration market obsolete. The mechanism supports a more useful decision rule: a platform becomes redundant for a workload when its required jobs are already covered and the team can operate the replacement. Its category name doesn't settle that question.

A map with a heavy boundary line. Inside the line, dimming shapes labeled single-team DAG orchestration, pipeline platforms, glue services. Outside the line, solid bright structures labeled warehouses, streaming backbones, cross-org platforms. The border is the argument.
The obsolete class is drawn with its border: what's inside collapses into primitives, and what's outside stands.

The border follows responsibilities. A single team may have requirements that exceed a library-based design. A larger organization may still have a small application workflow that doesn't need a shared platform. Team count, source count and document count are clues about coordination, not universal thresholds.

Warehouses and streaming systems do different work

A workflow library can decide when to invoke processing. It doesn't replace the query execution capabilities of a warehouse. Removing the tool that coordinates a query doesn't remove the need for the system that executes it.

The same distinction applies to a streaming backbone. Event retention, delivery and the operating requirements of a sustained event flow aren't supplied by expressing downstream work as graph nodes. A graph may be part of a system that consumes events without being the event infrastructure itself.

Three standing structures rendered solid against a dark sky, each with a one-line deed of survival: a columnar warehouse labeled REAL ENGINE UNDERNEATH, a streaming backbone labeled THE FIREHOSE IS REAL, a cross-org substrate labeled COORDINATION IS THE POINT. In the foreground, the collapsed mid-band lies as low rubble.
The survivors, with their reasons on the deed. Each stands on something a library can't be.

Cross-team coordination can also be a product in its own right. Shared quotas, permissions, lineage, scheduling rules and operator interfaces may be exactly what the platform team exists to provide. A library inside one application's repository doesn't establish a common agreement among all the other applications.

That was the original argument's strongest limit. The tooling under review was the extra coordination layer for product pipelines whose work already lived in Python. Keeping that scope prevents a useful simplification from becoming a claim about every kind of data infrastructure.

The capability mapping helps test a particular platform against that scope. Each substitution has a condition the team must satisfy.

Application mechanismCoordination work it may coverObligation that remains
Typed state and reducersPassing intermediate results and combining updatesIdentity, validation and correct merge semantics
Conditional edgesBranching and gatingSound checks, policy ownership and visible failures
SendStarting jobs and per-item workCapacity, rate limits and side-effect safety
Durable checkpointerRetaining state for inspection and recoveryStorage operations, replay policy and sink reconciliation

The table doesn't imply that a particular platform lacks these mechanisms. It compares the jobs with an application-level implementation. The platform may supply an operating experience or a shared control the table doesn't cover; that value belongs in the inventory.

Operating work survives the import

The source's cost argument was that a platform adds another server or service, another deployment story and another set of failure modes. That can be true of a proposed platform. The replacement also needs a runtime, persistence and a way to operate the work. Describing that replacement as an import statement understates the comparison.

An itemized standing bill on a dark ledger card: line items for a server to patch, a dialect to maintain, a second failure domain, an upgrade treadmill, and a which-layer-is-lying tax on every incident. The total line reads: paid monthly, in attention. A thin side column marks the library alternative: an import statement.
The standing bill, itemized. It's paid monthly, in attention, whether or not anything runs.

I want to compare the parts someone must understand and maintain. Which services require upgrades? Where are credentials stored and changed? Who diagnoses a stuck run? Who can restart it safely? Which record explains an uncertain write? A library-based system wins when simpler operation or a better fit to the workload justifies the change.

A mature incumbent also carries knowledge the team has already paid to acquire. Removing it can discard useful procedures along with avoidable machinery. The audit should identify what those procedures do before replacing them with an application that has no equivalent.

A gouache-dark building at night, finished and glowing warmly from inside, still wrapped in a full exoskeleton of rusting scaffolding with an empty operator hut and a cold work lamp. The scaffolding touches nothing that needs it.
The building is finished and lit, and its scaffolding still stands, touching nothing that needs it.

Library lock-in and platform lock-in both deserve inspection

The original note argued that plain Python functions and typed models make a library easier to leave than a platform-specific pipeline definition. Keeping transformation logic separate from graph wiring can help. It gives the team a smaller region of code to adapt when coordination changes.

Two exit doors compared by luggage. Door one, leaving a platform: a huge cart stacked with translated DAG crates, DSL manuals, scheduler assumptions. Door two, leaving the library: one small case labeled WIRING, while a porter's rack of plain functions and typed models stays put, labeled ALREADY YOURS.
Compare exit costs: the platform was the shape of your code. The library was only the wiring.

The separation has to exist in the implementation. A node that embeds routing decisions, checkpoint assumptions and platform-specific state access throughout its transformation code is less portable than its file extension suggests. The exit cost includes persisted state and operating procedures as well as function definitions.

A practical test is to identify which code could run without the workflow engine and which code depends on it. That test doesn't require an immediate migration. It reveals whether the claimed boundary is real enough to preserve options.

Maturity deserves the same concrete treatment. A familiar storage engine underneath a saver doesn't prove the entire workflow integration behaves correctly under failure. The team needs evidence for the combinations it uses: library version, saver, runtime and consequential node behavior. Plain code can be easier to inspect while still containing bugs.

The same pipeline run drawn twice. Above: a rack of platform services, scheduler, run-state DB, task mapper, retry service, each a box with its own on-call shadow. Below: the identical run as one program with three glowing glyphs on it, state, edge, send, plus a checkpoint reel. The racks dissolve into the glyphs along dotted fall lines.
The same run, before and after: a rack of services dissolving into three primitives and a checkpoint reel.

Data-pipeline vocabulary helps explain graph mechanisms

LangGraph's association with agents can obscure its use as workflow machinery. A data engineer looking for backfills, run recovery and result merging may use different terms from a developer looking for conversation state and human approval. Translating those concepts helps the engineer evaluate a tool already available in the language.

A library scene as a diagram: a shelf labeled AI AGENTS holds a heavy book spine reading WORKFLOW ENGINE, mis-shelved and glowing at the seams. Across the aisle, a reader silhouette labeled DATA ENGINEERS browses a shelf labeled PIPELINES, back turned. A dotted sight line between them never connects.
The docs gap is a filing error: workflow primitives shelved under agents, and the right readers in the wrong aisle.

The original claim that nobody had written the relevant documentation went beyond the evidence. I don't need to establish an empty literature to explain how the mechanisms map. The valuable work is to show the mapping, retain the limits and make it possible to test with an actual pipeline.

A dark gouache rooftop at night under a violet sky. A bearded man in a long coat and bowler hat holds a brass telescope to his eye, aimed into an empty stretch of sky on the right with nothing in it. High on the left, behind and above his opposite shoulder, a bright comet burns across the clouds with its trail sweeping away, and he never turns to look at it.
The best tools get discovered by the wrong aisle first.

I also wouldn't turn this design into a dated prediction about which platforms teams will adopt. The useful wager is local: for a defined pipeline, can the application meet its coordination and operating requirements without the additional platform? That can be answered by a comparison, and the comparison can be wrong in a way that teaches us something.

A wager board with four stakes of visibly different sizes: a heavy stack on defaults following primitives, a medium stack on the vocabulary merge, a small early stack on checkpoint-native on-call culture, and one declined chip pushed off the table beside a card reading: the platforms will rebrand as agents.
The bets are sized by stake, and one chip is declined on principle.

A platform that survives that comparison has earned its place. A platform whose only remaining job duplicates the application's capabilities is a candidate for removal. The next step is one bounded experiment that makes the difference inspectable.

Compare one pipeline before replacing the platform

Choose one awkward pipeline, express its state and routes explicitly, add persistence where recovery matters and run it beside the incumbent. The consequential part is what the comparison proves.

The pipeline should be bounded enough to understand and representative enough to expose the requirement that motivated the audit. A demonstration that avoids the expensive step, the difficult input or the failure the team worries about can produce clean results while leaving the platform decision unanswered.

A small before-and-after work card. Before: a tangled mini-pipeline with a shared blob and a hidden except-pass. After: five typed nodes, one violet gate edge, one checkpoint reel underneath. A calendar corner reads one afternoon; a footnote reads runs next to everything you already have.
Monday's move is one ugly pipeline rewritten as a graph, with one gate and one checkpointer, and nothing announced.

I start with the problem independent of a proposed solution. Applied here, the problem might be that failed processing repeats costly work, that a status hides missing results or that nobody can reconstruct a rejected document. Choosing LangGraph comes after naming the problem and the observable future that would count as an improvement.

The future should be inspectable. An operator can find the failed unit, understand why it stopped, resume it under a known policy and reopen the resulting record. That's a more useful target than a claim that the new architecture is modern or lean.

Keep the comparison from corrupting the product

Two pipelines writing to the same live destination can interfere with each other. The pilot needs a controlled comparison boundary until duplicate and conflicting writes have been addressed. Use separately identifiable outputs or another controlled path the team authorizes, and keep consequential external actions out of a comparison that isn't ready to reconcile them.

Both implementations should process identified input versions under recorded configurations. The comparison can then distinguish a behavior difference from a changed document or policy. Similar-looking output alone doesn't establish that the workflows performed equivalent work.

The input set should cover the problem the pilot exists to investigate. A legitimate empty document, malformed data, a repeat delivery and an interrupted external operation exercise different decisions. The particular cases belong to the workload. The point is to make success and non-success visible rather than choose only inputs that follow the easiest route.

A useful sequence keeps the decisions in order:

  1. Record the required outcome and which failure the pilot must handle.
  2. Identify the input versions, state schema, policy and output boundary.
  3. Run both paths without allowing the comparison to overwrite trusted results.
  4. Interrupt the new path at the consequential boundary and inspect recovery against the sink.
  5. Compare usable output, failed work, repeated effects and the operator's ability to explain the run.
  6. Decide which operating obligations remain and whether removing the incumbent is justified.

That sequence is a proposed method, not a report of an experiment completed for this article. A team can use it to reach either decision without pretending the architecture chose the result in advance.

The operating manual has several distinct jobs

The source sketched a missing manual for graph-based data pipelines. Its topics remain worth retaining even without the unsupported claim that no one has written them. Each topic answers a different operating question.

Historical reprocessing needs input selection, versioning and a policy for replacing previous results. A backfill that identifies its range but silently overwrites newer work is incomplete. The operator also needs to see how much selected work finished and which subset remains unresolved.

Reducer design needs a definition of identity and a conflict policy for duplicate or late data. The schema should explain whether repeated inputs append, replace, combine or stop. A rule chosen for one kind of data may be wrong for another channel in the same graph.

Gate design needs to distinguish mechanical checks, statistical findings and evaluated judgments. Each needs an outcome policy and a way to represent a failed checker. Quarantine must remain discoverable, with enough context and authority for a person to act on it.

A bookshelf holding seven slim unwritten volumes, spines titled with the missing chapters: backfills, reducers, gates, retention, observability, migration, testing. Each spine is a dim outline except a glowing eighth volume pulled halfway out, labeled CHAPTER ZERO, which is this note.
The missing manual sits on a shelf as seven spines waiting for authors, with chapter zero pulled out.

Checkpoint retention needs to describe what history remains useful for and which referenced artifacts must survive with it. Retaining every intermediate forever has a cost. Deleting history indiscriminately can remove the evidence needed to explain a consequential decision.

Observability needs to connect a visible output or failure to its run, route, policy and external operations. A collection of traces with no stable connection to the business record may contain an answer to the operator's question and still be unusable to the operator who has to find it.

Migration needs a reversible order. Move a bounded workload, preserve the trusted output path and retire incumbent capabilities only after identifying what still depends on them. A successful graph run doesn't prove that another team's alert, report or scheduling process has stopped using the old platform.

Testing needs to address the contracts the graph actually uses. Reducers have merge properties to examine. Gates have routing boundaries. Recovery has replay and side-effect behavior. Retained checkpoints may provide useful diagnostic material, subject to data handling and version compatibility. None of those topics reduces to checking that one normal invocation returns a value.

Let the result decide the next allocation

A pilot may show that the graph makes state and failure behavior easier to understand while the platform still earns its keep for shared operations. It may also show that the platform adds no remaining capability the workload needs. Either outcome improves the audit because it connects the decision to observed work.

A field of dim pipeline nodes across a dark map. One lights violet, then its neighbor, then two more, each ignition triggered by a small checkmark event beside the previous one. A dim megaproject banner labeled BIG MIGRATION lies folded and unused at the map's edge.
Adoption spreads pipeline by pipeline, on evidence, while the migration banner stays folded.

That's the respec part of min-maxxing. Change the allocation when the workload and evidence justify it. Keep the ability that earns its points. The search feature that started this audit gives the whole exercise a product outcome beyond the architecture diagram: a reader should be able to find useful material.

Search makes the pipeline's usefulness visible

The search build brought these decisions together. A corpus needs preparation, accepted output needs a serving path, and the reader needs a query that returns something useful. The original plan paired Python processing with an index and a reactive application. That's a coherent workload to evaluate, even while its exact production state remains a separate question.

A build ledger for one search feature: rows for pipeline, gate, index, surface, each pointing back at a chapter numeral of this note, and a vendor column that reads zero new vendors, four times. The deployment already knew how.
The search build adds zero new vendors. Every row points back at a chapter.

Search can reveal defects the pipeline's status never shows. A run can finish and still produce chunks that separate an explanation from its context. An index can contain documents while returning poor answers to the queries visitors make. Infrastructure completion and retrieval usefulness need evidence of their own.

The min-max sequence is to use the retrieval capability already available, examine where it succeeds and fails, and add another mode when it addresses a demonstrated gap. The original note put lexical search first and semantic retrieval later. That ordering is a proposal to test, rather than a claim that lexical search answers most visitors' questions.

Different retrieval methods return different kinds of evidence

Lexical search uses words in the indexed material. It can be useful when the reader names the thing the document names. The design then has to handle the cases where the reader's wording differs, where a result is too broad or where the relevant explanation spans material the index presents separately.

Three sonar layers over one dark corpus reef: a lexical ping drawn as tight rings labeled EXACT WORDS, a semantic ping as wide soft rings labeled MEANING, and a dim third ring labeled NEXT held in reserve. Each layer lights different parts of the same reef.
Layers of retrieval are kinds of ping. Same corpus, different echoes.

Semantic retrieval uses a representation intended to capture aspects of meaning. My knowledge-engine work used embeddings and source-specific preparation to improve access to a mixed corpus. That experience is a reason to consider a semantic layer, not a guarantee that adding vectors improves every search result.

The comparison should start with identified queries and inspectable results. Which passage returned? Does it answer the question? Did the preparation preserve the qualification the reader needs? A retrieval system that finds a confident sentence and loses its limit can make the corpus less useful even while the result looks relevant.

A new retrieval layer may reuse ingestion, gates and result storage. It may also introduce another index, a changed representation and additional operating work. That's another min-max decision with the same test: what named job does the extra part perform, and what evidence shows it performs that job well enough?

Echolocation depends on interpreting the return

Send a specific question, inspect what comes back, then adjust the next question in light of the return. The sequence reveals what the system can retrieve and where it has difficulty.

A dark cave interior. A single ping expands from a small emitter as concentric rings; where the rings strike unseen structure, arcs of the cave wall light up briefly in cyan, sketching shelves and hollows of a corpus out of blackness. Two return echoes travel back toward the emitter.
A query is a ping. The corpus answers with its shape, one echo at a time.

A silent return has several possible causes. The material may be absent, ingestion may have failed, the representation may be poor or the query may not reach it. Search alone doesn't tell us which explanation is right. The source record, processing history and index state help distinguish them.

That's where the pipeline mechanisms become useful to the reader's problem. A missing search result can lead to an identifiable input, a processing decision and a stored outcome. The investigation can establish whether the system lacks the knowledge or failed to make existing knowledge available.

A gouache-dark observatory interior on deep navy: brass instruments arranged around a single amber lamp, dials faintly lit, cables neat, a wide black window showing nothing at all. The instruments are the only way the room sees.
The window shows nothing, so the instruments are the room's only way to see.

An operator can explain what reached retrieval and what failed

A reliable system lets the operator stop compensating for its failures. Here, that means accepted documents reach retrieval, failed documents remain visible and the person responsible can explain the difference.

A quiet morning report card: overnight rows reading pipelines ran, gates held, one item in quarantine with state intact, pages sent zero. A steaming cup sits printed in the corner. The card's only bright accent is the zero.
The deliverable is a boring Tuesday, and the only bright mark on its morning report is the zero next to pages sent.

That future is a target. A typed schema, a gate and a checkpointer don't prove it has arrived. The evidence is the actual document flow, the recovered failure, the retained decision and the query that returns useful material. The site can demonstrate those capabilities where they are operating, and work that remains planned should retain that status.

A probability distribution drawn as a soft wide curve over two outcome labels, SHIPS and DECORATES. As observation ticks accumulate along a scroll track beneath, the curve visibly narrows and its mass slides toward one side. The final tick is unlabeled: the reader's.
Every section scrolled is an observation, and the distribution narrows. The last tick is yours.

The same standard applies to a comparison with an incumbent pipeline. Separate the outputs, preserve their identities and let the evidence show what changed. A smaller tool list counts as an improvement when the resulting system still does the job and the team can operate it.

Two pipelines running side by side into one shared sink: the incumbent platform lane and a slim graph lane. Above the sink, an evidence board tallies matched outputs, resume events, and incidents per lane. A note reads: the evidence does the persuading; nothing was migrated.
The demonstration runs next to the incumbent, into the same sink. Evidence does the persuading.

I'd keep an orchestrator that earns its place through shared controls, operating needs or capabilities the application doesn't cover. I'd challenge one whose remaining job is already handled by state, routing, fan-out and persistence in the program. That's the decision this audit makes available; a platform name can't make it for us.

A two-door card. Left door, dimmed: fifteen source icons, three team crests, a wax compliance seal, labeled KEEP YOUR ORCHESTRATOR. Right door, lit violet: one team, one product, a short stack of pipelines, labeled HAND ME THE UGLY ONE. A plain sign between them: both answers are fine.
Here's the disqualifier, drawn: one door is yours, and I only work the other one.

The search query returns us to the reason for the stack. Someone asks for a piece of knowledge, and the system has to produce useful evidence from the material it holds. Every part under that query has a job to justify, including the parts I like using.

A gouache-dark closing scene: the observatory from earlier at a distance on a ridge, one amber window still lit, instruments running themselves under a violet night. A small figure walks home downhill with hands in pockets.
The light stays on in the observatory. The operator went home.
#data-engineering#langgraph#convex#min-max#build-log

Keep reading

On this page

Visuals 79