cd /news/ai-agents/your-ai-agent-graph-compiles-that-do… · home › topics › ai-agents › article
[ARTICLE · art-159054] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Your AI Agent Graph Compiles. That Doesn't Mean It's Correct.

A developer demonstrated that TypeScript's type system cannot catch structural bugs in multi-agent workflow graphs, using a LangGraph contract-processing example where a fast-track route reaches a publish node without ever producing the required compliance approval. Because the approval field is typed as optional and the publish node uses a non-null assertion, the code compiles cleanly but throws at runtime. The developer argues that guaranteeing required data exists on every execution path is a dataflow analysis problem that static graph analysis, not type checking, must solve.

by read7 min views2 publishedOct 27, 2026

Why TypeScript can't catch certain bugs in multi-agent workflows — and what static graph analysis can do about them.

TypeScript catches a remarkable number of mistakes before an application ever runs. Incompatible types, incorrect arguments, missing properties — the compiler handles these pretty well.

But when building complex AI workflows, there's a problem that TypeScript alone can't solve:

Every component in a graph can be correctly typed while the graph itself is structurally incorrect.

I'm not talking about LLM hallucinations, unreliable prompts, or model nondeterminism.

I'm talking about ordinary engineering bugs caused by how execution paths connect.

Let's look at a concrete example.

Imagine a contract-processing system. Before publication, each contract must pass a compliance review.

The system also supports a fast-track route that skips part of the normal processing.

The workflow looks like this:

                       Start
                         |
                       Triage
                         |
                 +-------+-------+
                 |               |
             Normal route    Fast track
                 |               |
          Compliance Review      |
                 |               |
                 +-------+-------+
                         |
                  PublishContract

At first glance, this seems reasonable.

Except there's a problem.

PublishContract requires a compliance approval, but the fast-track route skips the component that produces it.

In other words, there is a reachable execution path where required input data is never created.

What does TypeScript think about this?

Let's use a conventional LangGraph StateGraph.

First, define the shared state:

import {
  Annotation,
  StateGraph,
  START,
  END,
} from "@langchain/langgraph";

interface ContractDraft {
  contractId: string;
  summary: string;
  fastTrackRequested: boolean;
}

interface ComplianceApproval {
  reviewer: string;
  certificateId: string;
}

const State = Annotation.Root({
  draft: Annotation<ContractDraft>(),
  approval: Annotation<ComplianceApproval | undefined>(),
  publishedRecord: Annotation<string | undefined>(),
});

The approval field is declared as ComplianceApproval | undefined.

That makes sense. At the beginning of the workflow, the compliance review hasn't happened yet.

Now let's add the publication node:

.addNode("publishContract", (state) => {
  const cert =
    state.approval!.certificateId.toUpperCase();

  return {
    publishedRecord:
      `PUBLISHED:${state.draft.contractId}:${cert}`,
  };
})

The non-null assertion operator (!) tells TypeScript that we are certain the value exists.

But that certainty has nothing to do with the actual structure of the graph.

Now consider the conditional transition:

.addConditionalEdges("triage", (state) =>
  state.draft.fastTrackRequested
    ? "publishContract"
    : "complianceReview"
)

If fastTrackRequested is true, execution reaches publishContract without producing approval.

TypeScript accepts the code.

At runtime, accessing certificateId on undefined throws.

Of course, we could remove the non-null assertion and handle the missing value explicitly. That would prevent this particular exception, but it wouldn't fix the architectural problem: the workflow still allows publication without the required compliance review.

TypeScript understands types and local control flow within functions.

But it doesn't prove that an arbitrary sequence of nodes in an external execution graph will produce every value required by downstream components.

To establish that guarantee, we need to know more than the type of a value.

We need to know whether that value is available on every execution path that reaches its consumer.

That's a dataflow analysis problem.

In a conventional application, we might write:

function publish(
  draft: ContractDraft,
  approval: ComplianceApproval
): PublishedContract

TypeScript verifies that both arguments are present at a call site.

But in a graph-based system, the producer of ComplianceApproval and its consumer may be separated by conditional branches, loops, and parallel execution paths.

Typing individual components is no longer enough.

We need to answer a more difficult question:

For every reachable execution path, are all required inputs guaranteed to exist when a component runs?

This is one of the problems I set out to address with AGBoss, an open-source TypeScript framework built on top of LangGraph.

AGBoss components declare their input and output DTOs explicitly.

Here's an example from the project's executable test suite. An action renders an invoice using an order request and a coupon discount:

@Action({
  name: "render-invoice-action",
  description: "Renders final invoice",
  input: {
    request: CheckoutRequestDto,
    coupon: CouponDiscountDto,
  },
  output: InvoiceSummaryDto,
})
export class RenderInvoiceAction
  implements ActionHandler<RenderInvoiceAction> {

  async execute(
    input: InputOf<RenderInvoiceAction>
  ): Promise<OutputOf<RenderInvoiceAction>> {
    // Generate the invoice
  }
}

The component explicitly requires two inputs: an order request and a calculated coupon.

Now imagine a workflow with two routes. VIP customers receive a promotional discount, while standard customers proceed directly to invoice generation.

start(MissingInputBrokenStart)
  .returns(CheckoutRequestDto)
  .toRouter(TierBrokenRouter)
  .chooseOne(
    route(ApplyVipCouponAction)
      .returns(CouponDiscountDto)
      .toAction(RenderInvoiceAction),

    route(RenderInvoiceAction),
  )

Look at the second route.

It invokes RenderInvoiceAction without producing the required CouponDiscountDto.

The DTOs are valid. The component contracts are valid. But the graph contains a structural error.

AGBoss includes a static graph analyzer, invoked with:

npx agboss check

It analyzes reachable execution paths and tracks which data is available to each component.

For our example, it reports:

[input.missing-on-path]

The diagnostic identifies the required coupon input of render-invoice-action.

This is a different level of verification.

Instead of waiting for a particular combination of inputs to trigger a failure in production, we can detect the structural problem before starting the application.

And because this is static analysis, it doesn't require calling an LLM, spending tokens, or relying on nondeterministic model responses.

We need to ensure that every route reaches a component whose declared inputs are actually available.

One solution is to use separate invoice actions for VIP and standard orders:

start(MissingInputFixedStart)
  .returns(CheckoutRequestDto)
  .toRouter(TierRouter)
  .chooseOne(
    route(ApplyVipCouponAction)
      .returns(CouponDiscountDto)
      .toAction(RenderInvoiceAction)
      .returns(InvoiceSummaryDto)
      .toFinish(MissingInputFixedFinish),

    route(RenderStandardInvoiceAction)
      .returns(InvoiceSummaryDto)
      .toFinish(MissingInputFixedFinish),
  )

Now the VIP branch produces a coupon before rendering the invoice, while the standard branch uses an action that doesn't require one.

Both branches produce InvoiceSummaryDto.

We've fixed the dataflow structure, rather than merely adding another runtime check.

While working on this problem, I realized that checking whether a DTO exists is only the first step.

Complex workflows have several other interesting classes of structural bugs.

Imagine an agent drafts a document, another agent approves it, and a third requests changes.

The document is revised, but the previous approval remains in the workflow state.

The approval exists. Its type is correct. But it belongs to an earlier version of the document.

AGBoss detects this class of problem with:

[input.outdated-on-path]

Suppose two branches run concurrently.

One modifies a financial calculation, while the other audits the previous version.

When the branches join, the audit result may no longer correspond to the current data.

The analyzer can report:

[launch.branch-outdates-sibling]

Consider an order workflow that reserves inventory, charges a customer, and confirms the purchase.

If the payment fails and an error route reaches the workflow finish without releasing the inventory, the reservation may remain active.

AGBoss supports declared compensating actions and checks compensation coverage on error paths.

The corresponding diagnostic is:

[flow.uncompensated-action]

These are different problems, but they share a common principle:

The correctness of a graph cannot be inferred from the correctness of its individual nodes.

I wanted these claims to be reproducible rather than just theoretical.

So I created a separate repository of executable examples:

AGBoss Examples — 02-path-scope-proof

It covers all four scenarios.

Each includes:

After installing the example's dependencies, you can run:

npm run check
npm test
npm run demo

The scenarios are designed to run deterministically and offline, without external LLM API calls.

The goal isn't simply to display a convincing error message.

It's to reproduce a concrete failure class and verify that the analyzer detects it.

Static analysis doesn't magically prove that an entire application is correct.

It won't tell you whether a discount was calculated correctly, whether a legal document complies with regulations, or whether an LLM produced a useful answer.

It also relies on developers declaring component contracts and data dependencies accurately.

But it can verify structural properties that would otherwise require extensive testing.

And the more complex a graph becomes, the more valuable that distinction is.

With three nodes and two transitions, manual inspection is relatively easy.

With dozens of nodes, conditional routes, loops, parallel branches, and nested workflows, the number of possible execution paths can grow rapidly.

Testing every combination independently becomes increasingly impractical.

Static analysis doesn't replace testing.

It helps eliminate entire classes of errors before testing even begins.

In conventional software engineering, we've long accepted that certain bugs are better caught at build time than at runtime.

I think AI-agent systems deserve the same approach.

LLMs introduce nondeterminism. That's unavoidable.

But the presence of nondeterministic components doesn't mean the rest of the system has to be unpredictable.

Graph structure, component contracts, data availability, and execution paths are engineering problems.

And many of them can be verified before the first model call.

That's the idea behind AGBoss, the open-source TypeScript framework I'm building on top of LangGraph, with dependency injection, declarative components, and static graph verification.

I'm curious how other developers approach this.

Have you encountered bugs where every node was correctly typed, but the workflow failed only on a particular execution path? How did you catch them?

GitHub: https://github.com/agbossjs/agboss

Executable examples: https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof

Documentation: https://agboss.dev

Tags: #typescript #ai #architecture #opensource

── more in #ai-agents 4 stories · sorted by recency
── more on @typescript 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-ai-agent-graph-…] indexed:0 read:7min 2026-10-27 · —