Toward Autonomous Companies: Building Multi-Agent Architecture with TypeScript and LangGraph
A quotation workflow with sales, database, and manager agents: Zod contracts, LangGraph StateGraph, scoped tools, human approval, checkpoints, idempotency, and outbox. Compiled code and eight behavioral tests.
· TankDev Engineering
A customer requests a quotation for ten pairs of work boots. A sales agent reads the request, a data agent retrieves price and availability, and a manager agent accepts a five-percent discount. All three agree. The system can still be wrong: the price might belong to another account, inventory might be stale, or approval might refer to an earlier proposal. Agreement between models does not resolve those failures.
That is where we start examining autonomous companies. Connecting models that exchange messages has become easier; assigning commercial responsibility has not. We will build a quotation workflow with three specialized roles using TypeScript, Zod, and LangGraph. Agents interpret and propose; services retain control over data access, discount authority, and durable business operations.
TypeScript and Zod do not make a model output perfectly correct or secure. The compiler catches mismatches in application code, while runtime schemas detect violations of the contracts we defined. Whether a price belongs to this customer, a discount is authorized, or a request was understood correctly still needs separate evidence. The rest of this article turns that distinction into executable boundaries.
This is a reference workflow, not a TankDev customer production result. Its code core was validated with Node.js 22.23.2, TypeScript 5.9.3, @langchain/langgraph 1.6.0, and Zod 4.6.7. Model, pricing, approval, and transaction adapters are explicit interfaces backed by deterministic fakes in tests. These checks are not a model-quality or performance benchmark.
1. From RAG to multi-agent architecture: which boundary changes?
RAG retrieves relevant information and connects it to generation. Multi-agent design concerns coordination between distinct responsibilities and contexts. They can coexist: a data agent may use RAG to search documents while a sales agent interprets the result against a customer request. Renaming each retrieval step an agent does not create a different architecture.
Our reason for separation is the difference in tools and authority. Sales interprets customer language without needing a pricing table. Data retrieves permitted commercial facts without promising discounts. Management evaluates findings without turning its own judgment into authorization. If one agent with narrow tools achieves the same quality for less, three model calls are not mandatory. This is consistent with the context and coordination considerations in LangChain’s multi-agent guidance.
| Role | Input / output | Authority withheld |
|---|---|---|
| Sales agent | Request → product/quantity/requested discount | Choosing tenant, inventing price or approval |
| Database agent | Allowlisted lookup plan → versioned commercial facts | Arbitrary SQL, DDL/DML, cross-account scans |
| Manager agent | Proposal evidence → review/reject recommendation | Roles, limits, approval records, payment authority |
| Policy / domain service | Identity + current facts + approval → durable decision | Bypassing rules on model instructions |
Communication is not an unrestricted group chat. Supervisor orchestration gives specialists specific inputs and controls updates to shared state. Subagent architectures can isolate context. Here, we use an explicit StateGraph rather than a prebuilt supervisor package so that allowed calls and transitions remain visible in code.
2. TypeScript and Zod: where type safety starts and stops
Treat HTTP responses, tool results, checkpoints, and model outputs as unknown when they cross a trust boundary. Casting JSON.parse output as Quote performs no runtime validation. TypeScript discriminated unions and narrowing improve application control flow without establishing external facts. Read that distinction alongside the TypeScript handbook’s narrowing model.
Zod parse/safeParse applies runtime contracts; its basic usage documentation explains those APIs. We use strictObject to reject unexpected fields, including a model adding tenantId or approved:true. Bound quantities, array lengths, text sizes, and numeric ranges. Apply byte limits, timeouts, and generation-token budgets before JSON parsing too; schema validation alone is not resource protection.
import * as z from "zod";
import { createHash } from "node:crypto";
export const Line = z.strictObject({
sku: z.string().regex(/^[A-Z0-9-]{1,40}$/),
quantity: z.number().int().min(1).max(1000),
});
export const Intent = z.strictObject({
kind: z.literal("quote_request"),
lines: z.array(Line).min(1).max(20),
requestedDiscountBps: z.number().int().min(0).max(2000),
});
export const LookupPlan = z.strictObject({
operation: z.literal("lookup_quote_facts"),
skus: z.array(Line.shape.sku).min(1).max(20),
});
export const Fact = z.strictObject({
sku: Line.shape.sku,
unitPriceMinor: z.number().int().min(0).max(1_000_000_000),
available: z.number().int().nonnegative(),
sourceVersion: z.string().min(1).max(80),
});
export const Facts = z.strictObject({
currency: z.literal("TRY"),
rows: z.array(Fact).min(1).max(20),
});
export const Decision = z.strictObject({
route: z.enum(["review", "reject"]),
reason: z.enum(["POLICY_REVIEW", "UNSUPPORTED_REQUEST"]),
});
export const Proposal = z.strictObject({
runId: z.string().uuid(),
policyVersion: z.string().min(1),
currency: z.literal("TRY"),
discountBps: z.number().int().min(0).max(2000),
lines: z.array(Line.extend({
unitPriceMinor: Fact.shape.unitPriceMinor,
sourceVersion: Fact.shape.sourceVersion,
})).min(1).max(20),
});
export type TrustedContext = Readonly<{
tenantId: string; accountId: string; runId: string; policyVersion: string;
}>;
export type ProposalData = z.infer<typeof Proposal>;
export function validatePlan(intent: z.infer<typeof Intent>, raw: unknown) {
const plan = LookupPlan.parse(raw);
const expected = [...new Set(intent.lines.map(x => x.sku))].sort();
if (JSON.stringify([...plan.skus].sort()) !== JSON.stringify(expected)) {
throw new Error("LOOKUP_SCOPE_MISMATCH");
}
return plan;
}
export function buildProposal(
ctx: TrustedContext, intent: z.infer<typeof Intent>, facts: z.infer<typeof Facts>
): ProposalData {
if (new Set(intent.lines.map(x=>x.sku)).size !== intent.lines.length) {
throw new Error("DUPLICATE_SKU");
}
const lines = intent.lines.map(line => {
const matches = facts.rows.filter(row => row.sku === line.sku);
if (matches.length !== 1 || matches[0]!.available < line.quantity) {
throw new Error("FACTS_MISSING_OR_STOCK_INSUFFICIENT");
}
const fact = matches[0]!;
return { ...line, unitPriceMinor: fact.unitPriceMinor, sourceVersion: fact.sourceVersion };
}).sort((a,b) => a.sku.localeCompare(b.sku));
return Proposal.parse({
runId: ctx.runId, policyVersion: ctx.policyVersion, currency: facts.currency,
discountBps: intent.requestedDiscountBps, lines,
});
}
export function proposalHash(proposal: ProposalData): string {
// Explicit versioned canonicalization; never hash arbitrary object key order.
const canonical = ["quote-proposal/v1", proposal.runId, proposal.policyVersion,
proposal.currency, proposal.discountBps,
[...proposal.lines].sort((a,b)=>a.sku.localeCompare(b.sku))
.map(x=>[x.sku, x.quantity, x.unitPriceMinor, x.sourceVersion])];
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
}The file contains three different checks. Schemas establish valid integer quantities. validatePlan prevents the data planner from widening the SKU set. buildProposal assembles price from service facts and checks availability. None of those reserves inventory. A perfectly valid value in the right currency can still belong to the wrong account; the lookup adapter must enforce identity scope.
Money uses minor-unit integers: 120000 kuruş represents TRY 1200. Discounts use basis points: 500 bps means five percent. Rounding, tax, and currency scale remain commercial-service rules; do not assume two decimal places for every currency. requestedDiscountBps expresses a request, not permission to apply it.
proposalHash binds a particular proposal version. It is not a signature, encryption, or permission. This canonicalization has an explicit field order; independently implemented services need a shared versioned format and conformance tests. Tenant, account, approver, and expiry must also exist in the approval record.
3. The database agent: expose a business tool, not database credentials
The data agent does not receive an open-ended SQL task. It selects lookup_quote_facts and the permitted SKUs. The server validates the plan before running prepared queries or a pricing API. tenantId and accountId come from verified identity, not the prompt or model JSON. Parameterization mitigates SQL injection; it does not repair missing tenant filters or business authorization.
-- Read-only business tool; values originate from trusted request context
-- and the validated lookup plan. Apply row/time limits at the service boundary.
BEGIN READ ONLY;
SET LOCAL statement_timeout = '750ms';
SELECT sku, unit_price_minor, available, source_version
FROM account_quote_facts
WHERE tenant_id = $1
AND account_id = $2
AND sku = ANY($3::text[])
ORDER BY sku
LIMIT 20;
COMMIT;
-- Use a restricted DB role and tenant-aware row policies where applicable.
-- A view with stale commercial data is still stale: validate at commit time.RLS can provide another defensive layer, but only with correct roles, view behavior, pooled-connection context, and tested policies. Read-only access is not harmless: it can leak data or exhaust resources through costly queries. Allowed columns, row counts, query deadlines, and tenant scope remain separate controls.
Product descriptions, CRM notes, and user text are untrusted data. A note claiming management approval cannot expand tool authority. A prompt instruction to ignore malicious instructions is insufficient protection. A data role without a discount-approval tool cannot directly execute that action through such a tool, though misleading content and output risks still require evaluation.
4. LangGraph StateGraph: explicit state and transitions instead of a chat transcript
Store the contracts the next step needs rather than an entire conversation: intent, versioned facts, proposal, and an approval reference. Bind identity and policy version through trusted context. Sales does not receive all commercial history, the data planner does not need the entire conversation, and the manager model receives no database connection. Narrower context makes decision provenance easier to inspect.
The following workflow.ts uses the preceding contracts.ts. Model adapters in Ports may use a provider’s structured-output feature, but still return unknown and are parsed at application boundaries. Do not assume provider schema constraints cover every Zod refinement. Generating a recommendation and applying a business decision are separate nodes.
import * as z from "zod";
import {
StateSchema, StateGraph, START, END, interrupt, MemorySaver,
} from "@langchain/langgraph";
import {
Intent, Facts, Decision, Proposal, validatePlan, buildProposal, proposalHash,
type TrustedContext, type ProposalData,
} from "./contracts.js";
const State = new StateSchema({
runId: z.string().uuid(),
requestText: z.string().min(1).max(12000),
intent: Intent.optional(), facts: Facts.optional(),
proposal: Proposal.optional(), approvalId: z.string().uuid().optional(),
phase: z.enum(["NEW","REVIEW","REJECTED","COMMIT","DONE"]).default("NEW"),
quoteId: z.string().optional(),
});
type S = typeof State.State;
export interface Ports {
salesModel(text: string): Promise<unknown>;
dataPlanner(intent: z.infer<typeof Intent>): Promise<unknown>;
lookup(ctx: TrustedContext, skus: string[]): Promise<unknown>;
managerModel(proposal: ProposalData): Promise<unknown>;
reviewPolicy(ctx: TrustedContext, proposal: ProposalData): Promise<boolean>;
readApproval(ctx: TrustedContext, id: string, hash: string): Promise<"approve"|"reject">;
commitQuote(args: {
ctx: TrustedContext; proposal: ProposalData; approvalId: string;
proposalHash: string; idempotencyKey: string;
}): Promise<string>;
}
export function createWorkflow(
ctx: TrustedContext, ports: Ports, checkpointer: MemorySaver
) {
const guard = (s: S) => {
if (s.runId !== ctx.runId) throw new Error("RUN_SCOPE_MISMATCH");
};
const sales = async (s: S) => {
guard(s);
return { intent: Intent.parse(await ports.salesModel(s.requestText)) };
};
const database = async (s: S) => {
guard(s); const intent = Intent.parse(s.intent);
const plan = validatePlan(intent, await ports.dataPlanner(intent));
const facts = Facts.parse(await ports.lookup(ctx, plan.skus));
return { facts };
};
const manager = async (s: S) => {
guard(s);
const proposal = buildProposal(ctx, Intent.parse(s.intent), Facts.parse(s.facts));
const decision = Decision.parse(await ports.managerModel(proposal));
const policyAllows = await ports.reviewPolicy(ctx, proposal);
return { proposal, phase: decision.route === "review" && policyAllows
? "REVIEW" as const : "REJECTED" as const };
};
const review = async (s: S) => {
guard(s); const proposal = Proposal.parse(s.proposal);
const hash = proposalHash(proposal);
// This node re-enters on resume. No irreversible side effect before interrupt.
const resume = z.strictObject({ approvalId: z.string().uuid() }).parse(
interrupt({ runId: ctx.runId, proposalHash: hash, proposal })
);
const decision = await ports.readApproval(ctx, resume.approvalId, hash);
return { approvalId: resume.approvalId,
phase: decision === "approve" ? "COMMIT" as const : "REJECTED" as const };
};
const commit = async (s: S) => {
guard(s); const proposal = Proposal.parse(s.proposal);
const approvalId = z.string().uuid().parse(s.approvalId);
// Trusted domain service must atomically recheck authorization, approval,
// expiry, policy and source versions; persist quote + outbox; deduplicate.
const quoteId = await ports.commitQuote({ ctx, proposal, approvalId,
proposalHash: proposalHash(proposal), idempotencyKey: `${ctx.runId}:quote:v1` });
return { quoteId, phase: "DONE" as const };
};
return new StateGraph(State)
.addNode("sales", sales).addNode("database", database)
.addNode("manager", manager).addNode("review", review).addNode("commit", commit)
.addEdge(START, "sales").addEdge("sales", "database").addEdge("database", "manager")
.addConditionalEdges("manager", s=>s.phase==="REVIEW"?"review":END)
.addConditionalEdges("review", s=>s.phase==="COMMIT"?"commit":END)
.addEdge("commit", END).compile({ checkpointer });
}The LangGraph Graph API documents StateSchema, StateGraph, conditional edges, and compilation. Declaring state schemas does not establish authorization or correctness for tool results; nodes therefore parse inputs and enforce scope explicitly. Model-produced routes stay within allowed choices. The graph has no unrestricted run-any-agent operation or user-selected node name.
Each model evaluation here is one bounded invocation; a specialist could later contain a subgraph or tool loop. If so, separately bound tool calls, turns, and the total deadline. Three roles do not imply parallel execution: extraction precedes pricing, and evidence precedes management review, making this critical path sequential.
5. Human approval: a version-bound authorization record, not approved:true
The manager’s review route recommends opening a review step. reviewPolicy checks roles, accounts, discount limits, and product conditions before allowing that step. All acceptable proposals in this example require human review. Future automation of a low-risk operation needs a distinct permission and limit, not a prompt telling the manager to become autonomous.
The review interface displays the proposal hash, items, quantities, pricing versions, and policy version. The backend verifies the approver’s tenant/account authority and persists the decision, proposalHash, expiry, and actor. Resume carries the resulting approvalId. An arbitrary UUID or approved:true from a client does not create approval.
import { Command, MemorySaver } from "@langchain/langgraph";
import { createWorkflow } from "./workflow.js";
// Harness fragment: ctx is resolved by a trusted gateway; ports implement
// the interfaces above. Reuse this saver/app within this local demo only.
const app = createWorkflow(ctx, ports, new MemorySaver());
const config = { configurable: { thread_id: ctx.runId }, recursionLimit: 12 };
await app.invoke({ runId: ctx.runId, requestText }, config);
// Pauses in review. A separate authenticated endpoint creates approvalId.
await app.invoke(new Command({ resume: { approvalId } }), config);
// If an execution failed, resume from its saved state; do not submit
// the original input again as an unrelated business operation.
await app.invoke(null, config);The interrupt documentation explains interrupt/Command behavior. A resumed node can run again from its beginning, so avoid email or ERP writes before interrupt. Do not swallow interrupt control flow in a broad catch and treat it as an ordinary retryable failure. Validate resumed data again, and never treat thread_id as authorization.
The trusted gateway checks run ownership for every start, status read, and resume. Do not copy ctx wholesale from a request body. Serialize concurrent resumes for one run or reject them through version checks. Equality between graph runId and ctx.runId is an additional consistency check, not a replacement for that gateway.
6. Checkpoints, business transactions, and outbox: three different records
MemorySaver is deliberate: this is a same-process validation harness, not restart recovery. Production requires an appropriate persistent checkpointer, access/retention policy, and backups. Persistence distinguishes thread state from longer-lived store data, while the checkpointer documentation describes durability modes. None of those modes creates a shared ERP transaction.
Do not leave durable run acceptance to the checkpointer’s first write. Record identity, normalized request hash, and idempotency key in the application database. A checkpoint saying the commit node was running cannot establish whether the business transaction committed. A crash just after quote/outbox commit can cause the node to run again. commitQuote therefore needs idempotent domain semantics that return the same result for the same key.
-- Contract sketch for commitQuote; implement conflict branches explicitly.
BEGIN;
-- Scope by trusted tenant/account and lock the durable run row.
SELECT id FROM agent_runs
WHERE id = :run_id AND tenant_id = :tenant AND account_id = :account
FOR UPDATE;
-- No row => reject. Existing idempotent result + identical proposal hash
-- => return it before re-consuming an approval. Different hash => conflict.
SELECT * FROM approvals
WHERE id = :approval_id AND tenant_id = :tenant AND account_id = :account
FOR UPDATE;
-- Require matching proposal_hash/run_id/policy_version, approve decision,
-- unexpired approval, valid current authority and not consumed elsewhere.
-- Revalidate current pricing/stock authority and source versions.
-- Changed facts => no silent rewrite of the approved proposal; review again.
INSERT INTO quotes(id, run_id, proposal_hash, idempotency_key, state)
VALUES (:quote_id, :run_id, :hash, :key, 'PENDING_DELIVERY');
-- UNIQUE on the properly scoped idempotency key.
UPDATE approvals SET consumed_by = :quote_id WHERE id = :approval_id;
INSERT INTO outbox(event_id, aggregate_id, event_type, payload)
VALUES (:event_id, :quote_id, 'quote.accepted.v1', :payload);
COMMIT;
-- ERP/CRM delivery is asynchronous; duplicate deliveries require receiver dedup.The SQL is a domain-contract sketch, omitting table definitions and complete error branches. Do not hold a transaction open across long remote pricing/ERP calls. Use versioned expiring quote/reservation tokens or an explicit pending workflow when remote authority is involved; local locks do not make remote inventory atomic. Quotation acceptance here is not payment or stock reservation.
A price change after approval requires a new review rather than transferring approval to a different offer. An ERP timeout should not send the proposal back through the models. Look up external operation IDs, retry delivery within budget, and reconcile. Our API integration guide expands idempotency and outbox, while event-driven LLM architecture covers long-running worker execution.
7. Parallelism and reducers: more agents do not remove concurrency problems
Credit eligibility and shipping options may be researched in parallel if independent. Letting both overwrite one decision field is not a valid merge strategy. Use separate state channels or findings keyed by agent/attempt, then join only after checking completion, conflicts, and expiry. A reducer merges updates; it does not establish business agreement.
An append-only reducer can duplicate findings during replay. Deduplicate stable identities and define whether conflicting content under one identity is an overwrite or an error. Order-dependent reducers can produce different results under concurrency. Prefer deterministic, order-independent merges where possible; a security or credit veto cannot disappear through majority voting.
Agent count is not throughput. Three model calls with retries and broad context can amplify requests and tokens substantially. Track attempts, tool calls, token reservations, spend, and deadline across the entire run, not just per node. recursionLimit 12 is an additional graph-step bound here; it is neither a spending budget nor a total deadline. Queue admission, global quotas, and tenant fairness still need their own controls.
Illustrative run budget, not a benchmark:
sales attempt: 1.5 s / 2,000 input / 300 output tokens
data planner: 1.0 s / 1,000 input / 150 output tokens
manager attempt: 2.0 s / 2,500 input / 250 output tokens
Sequential model latency alone: 4.5 s (assumed means)
Token usage before retries: 5,500 input + 700 output
Add lookup/policy/checkpoint latency; human wait is a separate duration.
Do not add p95 values and call the sum the end-to-end p95.
Cost per completed run includes failed attempts and review/operations cost.A timed-out provider request may have completed or incurred charges. Avoid multiplicative retries across application, SDK, and graph layers. Transient network failures can receive bounded retries; schema failures need bounded repair or review; authorization denial cannot be bypassed through a more persuasive prompt. Cancellation and deadlines do not automatically undo downstream actions.
8. Testing: prevent the wrong transaction before celebrating a good answer
Validation ran the real LangGraph and Zod packages. Model ports returned fixed JSON, while approval and commit ports used in-test records. Eight tests covered contract violations and graph control flow. In the lost-commit-response case, the commit adapter was invoked twice while the fake domain ledger retained one result. This demonstrates the replay boundary; it does not validate real PostgreSQL/ERP idempotency, which needs integration and crash testing.
| Test | Expected behavior |
|---|---|
| Extra tenant field / negative quantity | Runtime contract rejects |
| Expanded SKU scope | Lookup-scope check stops execution |
| Well-shaped but insufficient stock | Domain validation rejects |
| Interrupt and approved resume | No commit before approval; then DONE |
| Human rejection | No quotation created |
| Forged approvalId | Resume cannot create a business effect |
| Changed source version | Commit-time validation rejects |
| Lost response after commit | Repeated call; one effect in the fake ledger |
# Versions used for this article's core validation.
npm install --save-exact @langchain/langgraph@1.6.0 zod@4.6.7
npm install --save-dev --save-exact typescript@5.9.3 tsx@4.23.15 @types/node@22
# Commit the resolved lockfile; @types/node@22 resolves a patch version.
npx tsc --noEmit
npx tsx --test workflow.test.tsThe reference source bundle includes contracts.ts, workflow.ts, eight tests, tsconfig, and locked dependency files. It needs no API key and reproduces the checks with fake adapters. Real service connections and production authorization/persistence layers are outside the bundle’s scope.
Evaluate real models separately against a fixed set covering Turkish/English requests, missing codes, conflicting quantities, unsolicited discounts, injected instructions, and cross-account data requests. Measure schema pass rate, extraction accuracy, unsupported claims, false acceptance, unnecessary reviews, and end-to-end completion separately. Three agents repeating the same mistake are not independent verification, especially when they share a model and evidence.
Trace run_id, node, attempt, model/prompt/schema/policy versions, tool-result references, and decision reasons. Minimize sensitive data rather than logging every raw prompt; checkpoints also need access and retention policies. Keep run IDs out of unbounded metric labels. Report model/tool latency separately from human waiting, and do not confuse fast rejection with commercial success.
9. Production rollout: from shadow mode to bounded autonomy
Do not launch the first version with unrestricted authority to act for customers. In shadow mode, compare proposals with the existing process without external effects. Then introduce quotation preparation requiring approval. Expand a narrow task only after observing error cost and operational burden on labeled cases. Limited authority is a rollout decision, not a missing demo feature.
A kill switch should stop new admissions or commit authority while keeping accepted runs visible. Define what happens to pending approvals when graph, schema, or policy versions change. Old checkpoints may refer to old node names; do not blindly load them into incompatible code. Finish active threads on compatible workflow versions or apply tested migrations. Rollback does not mean deleting approval and outbox records.
At TankDev, we design applied AI, custom software, and system integration around these boundaries. Turning an agent demo into an operational process means delivering tool contracts, tenant authorization, durable transactions, evaluation sets, and operating practices together. The engineering question includes what the model is permitted to do under specific conditions.
Ownership cost includes checkpoint storage, pricing/ERP calls, retries, human review, evaluation, and maintenance alongside model tokens. Read this with our PoC-to-production guide. Our AI- and ERP-integrated web application article connects the architecture to customer-facing screens and sales workflows.
Progress toward autonomous companies does not require handing every decision to a model. Clear boundaries around request acceptance, evidence, authority, and repeated effects make further automation possible. Three agreeing agents are a good demonstration. Explaining why a lost response did not create a second commercial transaction is a more demanding architectural test.
To evaluate an agent workflow connected to your ERP/CRM, discuss the technical scope with TankDev. Start with the decision to automate, data ownership, and the cost of a wrong action before choosing a model name.
Frequently Asked Questions
01Does multi-agent architecture replace RAG?
No. RAG connects generation with retrieved evidence; multi-agent design coordinates roles. A data agent can use RAG internally. Additional agents are optional if one agent solves the task adequately.
02Do TypeScript and Zod make LLM outputs completely reliable?
No. They strengthen code types and runtime contracts. Source correctness, authorization, freshness, business rules, and external consistency still need separate checks. Type safety is not a business-correctness guarantee.
03Why treat model output as unknown?
External data has not been checked by the compiler. A type assertion does not validate it. Parse unknown before using the inferred type, and rely on it only within the defined contract.
04Must the database agent generate arbitrary SQL?
Not here. Use narrow lookup plans, allowed fields, and prepared queries with backend-derived tenant/account scope. Open-ended SQL use cases require additional isolation, restricted roles, query controls, and data authorization.
05Can the manager agent authorize discounts?
The model recommends. Policy and authorization decide which discount is permitted. This example requires human approval and rechecks approval, versions, and current authority at commit.
06Does thread_id isolate users or tenants?
No. It identifies checkpoint continuity. Start, read, and resume endpoints must authorize run ownership separately. Supplying another thread ID must not grant access.
07Can a node run again after a LangGraph interrupt?
Yes. A resumed node can execute again from its beginning. Avoid irreversible effects before interrupt and rely on separately idempotent domain-service contracts.
08Is MemorySaver sufficient for production?
Here it only supports a same-process test harness. Restart recovery needs a persistent checkpointer and operating policy. Durable run acceptance, business transactions, and outbox remain separate contracts.
09Why use idempotency and outbox if checkpoints exist?
Graph progress and business database/ERP effects are not one transaction. A lost commit response can replay a node. Idempotency preserves one result, while outbox durably records delivery intent.
10Do three agreeing agents validate a decision?
No. Shared models or evidence can produce correlated errors. Use authoritative facts, deterministic checks, and labeled evaluation. Majority agreement is not authorization or proof.
11Do the article tests measure real LLM quality?
No. Eight contract/control-flow behaviors were tested with real LangGraph/Zod and fake model/domain adapters. Model quality, real database/ERP behavior, and restart recovery require separate testing.
12Can TankDev connect this architecture to an existing ERP or CRM?
Where supported APIs, data access, and transaction contracts permit it, we can design and develop the scope together. Roles, authorization, data models, approval, idempotency, evaluation, and operations belong to the integration, beyond prompts alone.