{"slug": "your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct", "title": "Your AI Agent Graph Compiles. That Doesn't Mean It's Correct.", "summary": "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.", "body_md": "*Why TypeScript can't catch certain bugs in multi-agent workflows — and what static graph analysis can do about them.*\n\nTypeScript catches a remarkable number of mistakes before an application ever runs. Incompatible types, incorrect arguments, missing properties — the compiler handles these pretty well.\n\nBut when building complex AI workflows, there's a problem that TypeScript alone can't solve:\n\n**Every component in a graph can be correctly typed while the graph itself is structurally incorrect.**\n\nI'm not talking about LLM hallucinations, unreliable prompts, or model nondeterminism.\n\nI'm talking about ordinary engineering bugs caused by how execution paths connect.\n\nLet's look at a concrete example.\n\nImagine a contract-processing system. Before publication, each contract must pass a compliance review.\n\nThe system also supports a fast-track route that skips part of the normal processing.\n\nThe workflow looks like this:\n\n```\n                       Start\n                         |\n                       Triage\n                         |\n                 +-------+-------+\n                 |               |\n             Normal route    Fast track\n                 |               |\n          Compliance Review      |\n                 |               |\n                 +-------+-------+\n                         |\n                  PublishContract\n```\n\nAt first glance, this seems reasonable.\n\nExcept there's a problem.\n\n`PublishContract` requires a compliance approval, but the fast-track route skips the component that produces it.\n\nIn other words, **there is a reachable execution path where required input data is never created.**\n\nWhat does TypeScript think about this?\n\nLet's use a conventional LangGraph `StateGraph`.\n\nFirst, define the shared state:\n\n``` js\nimport {\n  Annotation,\n  StateGraph,\n  START,\n  END,\n} from \"@langchain/langgraph\";\n\ninterface ContractDraft {\n  contractId: string;\n  summary: string;\n  fastTrackRequested: boolean;\n}\n\ninterface ComplianceApproval {\n  reviewer: string;\n  certificateId: string;\n}\n\nconst State = Annotation.Root({\n  draft: Annotation<ContractDraft>(),\n  approval: Annotation<ComplianceApproval | undefined>(),\n  publishedRecord: Annotation<string | undefined>(),\n});\n```\n\nThe `approval` field is declared as `ComplianceApproval | undefined`.\n\nThat makes sense. At the beginning of the workflow, the compliance review hasn't happened yet.\n\nNow let's add the publication node:\n\n``` js\n.addNode(\"publishContract\", (state) => {\n  const cert =\n    state.approval!.certificateId.toUpperCase();\n\n  return {\n    publishedRecord:\n      `PUBLISHED:${state.draft.contractId}:${cert}`,\n  };\n})\n```\n\nThe non-null assertion operator (`!`) tells TypeScript that we are certain the value exists.\n\nBut that certainty has nothing to do with the actual structure of the graph.\n\nNow consider the conditional transition:\n\n``` js\n.addConditionalEdges(\"triage\", (state) =>\n  state.draft.fastTrackRequested\n    ? \"publishContract\"\n    : \"complianceReview\"\n)\n```\n\nIf `fastTrackRequested` is true, execution reaches `publishContract` without producing `approval`.\n\nTypeScript accepts the code.\n\nAt runtime, accessing `certificateId` on `undefined` throws.\n\nOf 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.\n\nTypeScript understands types and local control flow within functions.\n\nBut it doesn't prove that an arbitrary sequence of nodes in an external execution graph will produce every value required by downstream components.\n\nTo establish that guarantee, we need to know more than the type of a value.\n\nWe need to know **whether that value is available on every execution path that reaches its consumer.**\n\nThat's a dataflow analysis problem.\n\nIn a conventional application, we might write:\n\n```\nfunction publish(\n  draft: ContractDraft,\n  approval: ComplianceApproval\n): PublishedContract\n```\n\nTypeScript verifies that both arguments are present at a call site.\n\nBut in a graph-based system, the producer of `ComplianceApproval` and its consumer may be separated by conditional branches, loops, and parallel execution paths.\n\nTyping individual components is no longer enough.\n\nWe need to answer a more difficult question:\n\nFor every reachable execution path, are all required inputs guaranteed to exist when a component runs?\n\nThis is one of the problems I set out to address with **AGBoss**, an open-source TypeScript framework built on top of LangGraph.\n\nAGBoss components declare their input and output DTOs explicitly.\n\nHere's an example from the project's executable test suite. An action renders an invoice using an order request and a coupon discount:\n\n```\n@Action({\n  name: \"render-invoice-action\",\n  description: \"Renders final invoice\",\n  input: {\n    request: CheckoutRequestDto,\n    coupon: CouponDiscountDto,\n  },\n  output: InvoiceSummaryDto,\n})\nexport class RenderInvoiceAction\n  implements ActionHandler<RenderInvoiceAction> {\n\n  async execute(\n    input: InputOf<RenderInvoiceAction>\n  ): Promise<OutputOf<RenderInvoiceAction>> {\n    // Generate the invoice\n  }\n}\n```\n\nThe component explicitly requires two inputs: an order request and a calculated coupon.\n\nNow imagine a workflow with two routes. VIP customers receive a promotional discount, while standard customers proceed directly to invoice generation.\n\n```\nstart(MissingInputBrokenStart)\n  .returns(CheckoutRequestDto)\n  .toRouter(TierBrokenRouter)\n  .chooseOne(\n    route(ApplyVipCouponAction)\n      .returns(CouponDiscountDto)\n      .toAction(RenderInvoiceAction),\n\n    route(RenderInvoiceAction),\n  )\n```\n\nLook at the second route.\n\nIt invokes `RenderInvoiceAction` without producing the required `CouponDiscountDto`.\n\nThe DTOs are valid. The component contracts are valid. But the graph contains a structural error.\n\nAGBoss includes a static graph analyzer, invoked with:\n\n```\nnpx agboss check\n```\n\nIt analyzes reachable execution paths and tracks which data is available to each component.\n\nFor our example, it reports:\n\n```\n[input.missing-on-path]\n```\n\nThe diagnostic identifies the required `coupon` input of `render-invoice-action`.\n\nThis is a different level of verification.\n\nInstead of waiting for a particular combination of inputs to trigger a failure in production, we can detect the structural problem before starting the application.\n\nAnd because this is static analysis, it doesn't require calling an LLM, spending tokens, or relying on nondeterministic model responses.\n\nWe need to ensure that every route reaches a component whose declared inputs are actually available.\n\nOne solution is to use separate invoice actions for VIP and standard orders:\n\n```\nstart(MissingInputFixedStart)\n  .returns(CheckoutRequestDto)\n  .toRouter(TierRouter)\n  .chooseOne(\n    route(ApplyVipCouponAction)\n      .returns(CouponDiscountDto)\n      .toAction(RenderInvoiceAction)\n      .returns(InvoiceSummaryDto)\n      .toFinish(MissingInputFixedFinish),\n\n    route(RenderStandardInvoiceAction)\n      .returns(InvoiceSummaryDto)\n      .toFinish(MissingInputFixedFinish),\n  )\n```\n\nNow the VIP branch produces a coupon before rendering the invoice, while the standard branch uses an action that doesn't require one.\n\nBoth branches produce `InvoiceSummaryDto`.\n\nWe've fixed the dataflow structure, rather than merely adding another runtime check.\n\nWhile working on this problem, I realized that checking whether a DTO exists is only the first step.\n\nComplex workflows have several other interesting classes of structural bugs.\n\nImagine an agent drafts a document, another agent approves it, and a third requests changes.\n\nThe document is revised, but the previous approval remains in the workflow state.\n\nThe approval exists. Its type is correct. But it belongs to an earlier version of the document.\n\nAGBoss detects this class of problem with:\n\n`[input.outdated-on-path]`\n\nSuppose two branches run concurrently.\n\nOne modifies a financial calculation, while the other audits the previous version.\n\nWhen the branches join, the audit result may no longer correspond to the current data.\n\nThe analyzer can report:\n\n`[launch.branch-outdates-sibling]`\n\nConsider an order workflow that reserves inventory, charges a customer, and confirms the purchase.\n\nIf the payment fails and an error route reaches the workflow finish without releasing the inventory, the reservation may remain active.\n\nAGBoss supports declared compensating actions and checks compensation coverage on error paths.\n\nThe corresponding diagnostic is:\n\n`[flow.uncompensated-action]`\n\nThese are different problems, but they share a common principle:\n\n**The correctness of a graph cannot be inferred from the correctness of its individual nodes.**\n\nI wanted these claims to be reproducible rather than just theoretical.\n\nSo I created a separate repository of executable examples:\n\n[AGBoss Examples — 02-path-scope-proof](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)\n\nIt covers all four scenarios.\n\nEach includes:\n\nAfter installing the example's dependencies, you can run:\n\n```\nnpm run check\nnpm test\nnpm run demo\n```\n\nThe scenarios are designed to run deterministically and offline, without external LLM API calls.\n\nThe goal isn't simply to display a convincing error message.\n\nIt's to reproduce a concrete failure class and verify that the analyzer detects it.\n\nStatic analysis doesn't magically prove that an entire application is correct.\n\nIt 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.\n\nIt also relies on developers declaring component contracts and data dependencies accurately.\n\nBut it can verify structural properties that would otherwise require extensive testing.\n\nAnd the more complex a graph becomes, the more valuable that distinction is.\n\nWith three nodes and two transitions, manual inspection is relatively easy.\n\nWith dozens of nodes, conditional routes, loops, parallel branches, and nested workflows, the number of possible execution paths can grow rapidly.\n\nTesting every combination independently becomes increasingly impractical.\n\nStatic analysis doesn't replace testing.\n\nIt helps eliminate entire classes of errors before testing even begins.\n\nIn conventional software engineering, we've long accepted that certain bugs are better caught at build time than at runtime.\n\nI think AI-agent systems deserve the same approach.\n\nLLMs introduce nondeterminism. That's unavoidable.\n\nBut **the presence of nondeterministic components doesn't mean the rest of the system has to be unpredictable.**\n\nGraph structure, component contracts, data availability, and execution paths are engineering problems.\n\nAnd many of them can be verified before the first model call.\n\nThat's the idea behind [AGBoss](https://github.com/agbossjs/agboss), the open-source TypeScript framework I'm building on top of LangGraph, with dependency injection, declarative components, and static graph verification.\n\nI'm curious how other developers approach this.\n\n**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?**\n\n**GitHub:** [https://github.com/agbossjs/agboss](https://github.com/agbossjs/agboss)\n\n**Executable examples:** [https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)\n\n**Documentation:** [https://agboss.dev](https://agboss.dev)\n\n**Tags:** #typescript #ai #architecture #opensource", "url": "https://wpnews.pro/news/your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct", "canonical_source": "https://dev.to/pravvich/your-ai-agent-graph-compiles-that-doesnt-mean-its-correct-515h", "published_at": "2026-10-27 20:38:01+00:00", "updated_at": "2026-10-27 20:42:16.529564+00:00", "lang": "en", "topics": ["ai-agents", "artificial-intelligence", "ai-tools", "developer-tools"], "entities": ["TypeScript", "LangGraph", "LangChain"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct", "markdown": "https://wpnews.pro/news/your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct.md", "text": "https://wpnews.pro/news/your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct.txt", "jsonld": "https://wpnews.pro/news/your-ai-agent-graph-compiles-that-doesn-t-mean-it-s-correct.jsonld"}}