Agent Playbook, build tutorial
A walkthrough of how to build an inventory agent that watches stock and demand per SKU, forecasts what will sell, works out when and how much to reorder, picks the right supplier, and raises the purchase order, while also flagging the overstock and dead stock that freezes your cash. Every code step is shown in three frameworks, Google ADK, the Claude SDK, and LangGraph, so you can build it on the stack your team already knows. The code here is the agent's core logic; standing it up in production also means adding the setup, integrations, and hardening around it.
Reordering looks like a simple "when low, buy more," and that is exactly why it leaks money at both ends. Order too late and you stock out: the worldwide Corsten and Gruen study put the average out-of-stock rate at about 8.3 percent and the resulting loss at around 4 percent of sales. Order too much and cash freezes on the shelf: IHL Group puts total inventory distortion, out-of-stocks plus overstocks, at roughly $1.7 trillion globally, about 6.5 percent of retail sales.
A flat min-max rule cannot see a festive spike coming or a supplier running late. An agent is different: it forecasts demand per SKU, sizes a safety stock for the variability, triggers the order at the right level, chooses the supplier on landed cost and reliability, and flags the slow movers before they become dead stock.
You do not build the agent loop from scratch. A framework gives you the moving parts: a step is a model plus an instruction plus a few typed tools, and the inventory maths are those tools. The code tabs on the relevant steps show the same read, forecast, check, and order flow three ways.
Google ADK gives the deterministic functions to an agent and runs it with a Runner, the Claude SDK drives a tool-use loop, and LangGraph builds an explicit per-SKU graph. The forecast, the reorder-point maths, and the EOQ are the same code in all three; only the orchestration and the supplier- quote reading differ.
Every SKU flows through one loop: read stock and sales, forecast demand, compute the reorder point, then either raise a purchase order when below it or watch and flag overstock when above. Big spend routes to a human, and guardrails for duplicate orders, MOQ, and audit wrap the whole thing.
Per-SKU on-hand, daily sales history, open purchase orders, supplier lead times, MOQs, and any shelf life or expiry.
Your POS and ERP (Tally, Zoho Inventory, Unicommerce, Vyapar), a model API key for reading quotes, your purchasing module, and your vendor list.
Target service level, auto-approve spend limit, overstock and dead-stock thresholds, and who signs off on large orders.
Install the packages and set your Gemini key. That is enough to run the runnable file below, which forecasts and sizes a reorder for a sample SKU locally. For production you connect your POS or ERP API (Tally, Zoho Inventory, Unicommerce) for live stock and sales, and your purchasing system to raise the order; those are provider-specific.
# Python 3.10 or newer
python -m venv venv && source venv/bin/activate
# The runnable path below uses Google ADK + Gemini:
pip install google-adk google-genai
# Your Gemini key from aistudio.google.com/apikey:
export GEMINI_API_KEY="your-key-here" | Layer | Pick | Why |
|---|---|---|
| Signals | Your POS and ERP API (Tally, Zoho Inventory, Unicommerce, Vyapar) | Pull current on-hand, open purchase orders, and daily sell-through per SKU. The reorder decision is only as good as these numbers. |
| Forecast | A baseline in statsmodels or Prophet, lifted by a seasonality index | Predict demand per SKU instead of guessing from a flat average, so the festive spike does not catch you empty. |
| Decision core | Plain Python for reorder point, safety stock, and EOQ | The maths is deterministic and testable. You never want a model freelancing on how much stock to buy. |
| Model | Gemini 2.5 Flash, Claude, or any model behind LangChain | Reads supplier quotes and price lists straight from the PDF or email into a schema. The code tabs show the same call in all three. |
| Agent framework | Google ADK, the Claude SDK, or LangGraph | Drives each SKU through read, forecast, check, and order, calling the deterministic tools. Pick the one your team runs. |
| Data | Postgres plus a dashboard | Store forecasts, reorder points, decisions, and an audit trail, and track fill rate and stockout days over time. |
The model and orchestration steps show the same logic in Google ADK, the Claude SDK, and LangGraph. Use the tabs to switch. The inventory maths are deterministic Python and identical everywhere. These are the building blocks, and they call your own systems (the POS, the ERP, the purchasing module) by name; the complete file you can run today is in "Put it together" below.
Every decision starts from clean numbers, so the agent pulls them per SKU from your POS and ERP: current on-hand, what is already on order in open purchase orders, and the recent daily sell-through. Counting on-order stock matters, because an item can be below its shelf level yet already covered by a PO arriving tomorrow. This is the same in every framework.
from pydantic import BaseModel
class SkuState(BaseModel):
sku: str
on_hand: int
on_order: int # already in an open PO
daily_sales: list[int] # recent units/day, newest last
lead_time_days: int
shelf_life_days: int | None = None
def load_state(sku: str) -> SkuState:
pos = erp.stock(sku) # current on-hand and open POs
hist = pos_sales.daily(sku, days=90) # last 90 days of sell-through
sup = vendors.primary(sku)
return SkuState(sku=sku, on_hand=pos.on_hand, on_order=pos.on_order,
daily_sales=hist, lead_time_days=sup.lead_time_days) A flat average is what gets shops caught empty before Diwali. The agent forecasts demand per SKU, starting from a recent trend and lifting it by a seasonality index for the month or festival. As history builds, you swap the baseline for a proper time-series model such as Prophet without changing anything downstream. Deterministic, the same in every framework.
import statistics
def forecast_daily(s: SkuState) -> float:
recent = s.daily_sales[-28:] # last 4 weeks
base = statistics.mean(recent) if recent else 0
season = seasonality_index(s.sku) # e.g. 1.3 in festive months
return base * season The reorder point is the level at which you must order so new stock lands before you run out: expected demand across the lead time, plus a safety stock that absorbs the days demand runs hot or the supplier runs late. Safety stock is sized from the variability of recent sales and your target service level, where a z of about 1.65 covers roughly 95 percent of cases. This is deterministic code.
import math
def reorder_point(s: SkuState, daily: float, z: float = 1.65) -> int:
sigma = statistics.pstdev(s.daily_sales[-28:] or [0])
safety = z * sigma * math.sqrt(s.lead_time_days)
return math.ceil(daily * s.lead_time_days + safety) Ordering too little means ordering again next week and paying freight twice; ordering too much freezes cash. The economic order quantity balances ordering cost against holding cost, then the agent caps it by the supplier's minimum order quantity and, for perishables, by what you can realistically sell before it expires.
def order_qty(s: SkuState, annual_demand: float, order_cost, hold_cost) -> int:
eoq = math.sqrt((2 * annual_demand * order_cost) / hold_cost)
qty = max(eoq, vendors.primary(s.sku).moq) # respect MOQ
if s.shelf_life_days:
qty = min(qty, perishable_cap(s, annual_demand)) # do not over-buy
return math.ceil(qty) The cheapest unit price is not always the right order, and quotes arrive as PDFs and emails, not tidy rows. The model reads each supplier's latest quote directly into a typed schema, then the agent ranks vendors on landed cost, lead time, and reliability. The tabs show the same quote read in Gemini, Claude, and LangChain; the schema is defined once and reused.
from google import genai
from google.genai import types
from pydantic import BaseModel
client = genai.Client()
class Quote(BaseModel):
vendor: str
unit_price: float
lead_time_days: int
moq: int
def read_quote(pdf: bytes) -> Quote:
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents=[types.Part.from_bytes(data=pdf, mime_type="application/pdf"),
"Extract the quote into the schema."],
config=types.GenerateContentConfig(
response_mime_type="application/json", response_schema=Quote))
return resp.parsed
def choose_vendor(sku: str, qty: int) -> Quote:
quotes = [read_quote(q) for q in vendors.latest_quotes(sku)]
return min(quotes, key=lambda v: (v.unit_price * qty, v.lead_time_days,
-vendors.on_time_rate(v.vendor))) import base64
from anthropic import Anthropic
client = Anthropic()
# Quote is the same Pydantic schema as the Gemini tab.
def read_quote(pdf: bytes) -> Quote:
msg = client.messages.parse(
model="claude-opus-4-8", max_tokens=512,
messages=[{"role": "user", "content": [
{"type": "document", "source": {"type": "base64",
"media_type": "application/pdf",
"data": base64.standard_b64encode(pdf).decode()}},
{"type": "text", "text": "Extract the quote into the schema."}]}],
output_config={"format": Quote})
return msg.parsed_output import base64
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
# Quote is the same Pydantic schema as the Gemini tab.
reader = ChatGoogleGenerativeAI(model="gemini-2.5-flash").with_structured_output(Quote)
def read_quote(pdf: bytes) -> Quote:
return reader.invoke([HumanMessage(content=[
{"type": "text", "text": "Extract the quote into the schema."},
{"type": "media", "mime_type": "application/pdf",
"data": base64.b64encode(pdf).decode()}])]) Now the loop comes together. If on-hand plus on-order is still above the reorder point, the agent does nothing, which is most of the time. When it dips below, the agent sizes the order, picks the supplier, and drafts the PO. Routine, low-value orders send automatically, while anything above your spend limit waits for a human.
def maybe_reorder(s: SkuState):
daily = forecast_daily(s)
rop = reorder_point(s, daily)
if s.on_hand + s.on_order > rop:
return # still covered, do nothing
qty = order_qty(s, annual_demand=daily * 365, order_cost=250, hold_cost=18)
vendor = choose_vendor(s.sku, qty)
po = purchasing.draft_po(s.sku, qty, vendor)
if po.value > policy.auto_approve_limit:
review_queue.add(po, reason="over_limit") # human approves big spend
else:
purchasing.send(po) # auto-send small, routine POs Stockouts are only half of inventory distortion; the other half is cash frozen in things that do not sell. On the same pass, the agent measures days of cover and flags anything with far too much as overstock to mark down or return, and items selling nothing for a long stretch as dead stock to liquidate.
def flag_dead_stock(s: SkuState, daily: float):
days_cover = s.on_hand / daily if daily else math.inf
if days_cover > policy.overstock_days:
flag(s.sku, "overstock", action="markdown_or_return")
if daily == 0 and s.on_hand > 0 and age(s.sku) > policy.dead_days:
flag(s.sku, "dead_stock", action="liquidate") Anything that spends money has to be safe to run on a schedule. Before drafting, the agent checks for an existing open PO so a retry never stacks duplicate orders. It avoids leaning on a single slow supplier when a faster alternative exists, respects MOQ and shelf life as hard limits, and writes every decision to an append-only audit log. A dashboard tracks fill rate, stockout days, inventory value, and dead-stock value.
def guard(s: SkuState, vendor, qty) -> "Vendor":
if purchasing.open_po_exists(s.sku):
raise SkipReorder("open_po") # never stack duplicate POs
if vendor.lead_time_days > policy.max_lead and alternatives(s.sku):
vendor = choose_vendor(s.sku, qty) # avoid a single slow supplier
return vendor
# audit.append(s.sku, decision); metrics.track(fill_rate, stockout_days,
# inventory_value, dead_stock_value); alert_if(fill_rate < target) Now wire the tools into one runnable agent that sweeps the catalogue. ADK gives the deterministic functions to an LlmAgent and runs it with a Runner, the Claude SDK drives a tool-use loop, and LangGraph builds an explicit per-SKU graph that branches to order only when stock is below the reorder point. The tools and the inventory maths are the same in all three.
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
reorder_agent = LlmAgent(
name="reorder_agent", model="gemini-2.5-flash",
instruction="For each SKU below its reorder point, size the order with order_qty, "
"pick the supplier with choose_vendor, and call maybe_reorder. "
"Flag overstock and dead stock.",
tools=[forecast_daily, reorder_point, order_qty, choose_vendor,
maybe_reorder, flag_dead_stock])
runner = Runner(agent=reorder_agent, app_name="inventory",
session_service=InMemorySessionService())
# Run daily over the catalogue via cron / Cloud Scheduler. from anthropic import Anthropic
client = Anthropic()
TOOLS = [forecast_tool, rop_tool, eoq_tool, vendor_tool, reorder_tool] # JSON schemas
def run_reorder(messages: list):
while True:
resp = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
tools=TOOLS, messages=messages)
if resp.stop_reason != "tool_use":
return resp
messages.append({"role": "assistant", "content": resp.content})
for b in resp.content:
if b.type == "tool_use":
out = dispatch(b.name, b.input)
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": b.id, "content": str(out)}]}) from langgraph.graph import StateGraph, START, END
def assess(s: dict) -> dict:
st = load_state(s["sku"]); daily = forecast_daily(st)
rop = reorder_point(st, daily)
return {"state": st, "daily": daily, "below": st.on_hand + st.on_order <= rop}
def order(s: dict) -> dict:
return {"po": maybe_reorder(s["state"])}
g = StateGraph(dict)
g.add_node("assess", assess); g.add_node("order", order)
g.add_edge(START, "assess")
g.add_conditional_edges("assess", lambda s: "order" if s["below"] else END)
g.add_edge("order", END)
reorder_agent = g.compile() # run per SKU on a daily sweep
Here is a runnable file. It forecasts demand for a sample SKU, computes its
reorder point, and sizes an order if stock is below it, then prints the
decision. Replace SKUS with your POS or ERP feed. Save it as main.py and
run python main.py.
# main.py -- run: python main.py
# A runnable reference: forecasts demand for one SKU, computes its reorder point,
# and sizes an order if it is below it. Replace SKUS with your POS/ERP feed.
import asyncio, math, statistics
from google import genai
from google.genai import types
from google.adk.agents import LlmAgent
from google.adk.runners import InMemoryRunner
client = genai.Client() # reads GEMINI_API_KEY
# ---- a sample catalogue (replace with your POS/ERP feed) ----
SKUS = {
"SKU-1180": {"on_hand": 42, "on_order": 0, "moq": 24, "lead_time_days": 6,
"daily_sales": [9, 11, 10, 12, 11, 13, 10, 12, 11, 10, 12, 11]},
}
# ------------------------------------------------------------
def reorder_check(sku_id: str) -> dict:
"""Forecast one SKU, compute its reorder point, and size an order if below it."""
s = SKUS[sku_id]
recent = s["daily_sales"][-28:]
daily = statistics.mean(recent) if recent else 0
sigma = statistics.pstdev(recent or [0])
rop = math.ceil(daily * s["lead_time_days"]
+ 1.65 * sigma * math.sqrt(s["lead_time_days"])) # 95% service level
if s["on_hand"] + s["on_order"] > rop:
return {"sku": sku_id, "action": "hold", "reorder_point": rop,
"on_hand": s["on_hand"]}
eoq = math.ceil(math.sqrt((2 * daily * 365 * 250) / 18)) # order cost 250, hold 18
return {"sku": sku_id, "action": "reorder", "reorder_point": rop,
"qty": max(eoq, s["moq"])}
agent = LlmAgent(
name="reorder_agent", model="gemini-2.5-flash",
instruction=("Call reorder_check on the SKU the user names, then report whether "
"to reorder, the reorder point, and the quantity if any."),
tools=[reorder_check])
async def main():
runner = InMemoryRunner(agent=agent, app_name="inventory")
session = await runner.session_service.create_session(app_name="inventory", user_id="demo")
msg = types.Content(role="user", parts=[types.Part(text="Check SKU-1180.")])
async for event in runner.run_async(user_id="demo", session_id=session.id, new_message=msg):
if event.is_final_response():
print(event.content.parts[0].text)
if __name__ == "__main__":
asyncio.run(main()) You should see the agent report the reorder point and, for this sample, a reorder with a quantity, since on-hand is below the point. Supplier selection from real quotes and the purchase order itself stay in the build steps above, which need your vendor and purchasing systems.
The buyer sees one screen: what is low, the numbers behind it, and the recommended order with a supplier already chosen.
| Approach | Best for | Effort | Cost | Control |
|---|---|---|---|---|
| Custom agent (build in-house) | You want your own forecast, multi-supplier rules, and tight ERP fit | High | Build cost + compute | Full |
| Inventory platform (Zoho Inventory, Unicommerce, Increff, NetSuite) | Standard reordering and stock control, fast start | Low | Subscription per seat or volume | Medium |
| Spreadsheet min-max | A small catalogue with steady, predictable demand | Low | Cheap, but blind to variability and lead-time risk | Low |
The mistakes that cause stockouts are almost always about variability, not averages. Demand is not flat: it spikes around festivals, paydays, and promotions, so a forecast without seasonality plans for a normal week and gets caught in a busy one. Lead times are not fixed either, so safety stock has to absorb both demand running hot and a supplier running late, which is why it scales with the square root of the lead time, not the average alone.
The other classic errors are structural. Ignoring stock already on order leads to double-buying; ignoring MOQ and shelf life leads to overstock and waste; leaning on one cheap but slow supplier turns a small saving into a lost sale. The agent encodes each of these as an explicit rule, so the decision is defensible rather than a guess that happened to work last time.
Benchmarks vary by source and should be treated as ranges, not promises. These are from neutral research, not vendor marketing.
| Metric | Manual | Agent |
|---|---|---|
| Average out-of-stock rate | ~8.3% of items globally (Corsten and Gruen worldwide study) | Lower, because reorder points fire before the shelf empties |
| Sales lost to stockouts | ~4% of sales on average (Corsten and Gruen) | Recovered as on-shelf availability rises |
| Cash frozen in overstock | Overstocks cost retailers about $554 billion globally (IHL Group) | Flagged early for markdown or return, not left to rot |
| Total inventory distortion | ~$1.7 trillion globally, about 6.5% of retail sales (IHL Group) | Shrinks as forecasts and reorder points tighten on both ends |
Sources: Corsten and Gruen, Retail Out-of-Stocks worldwide study; IHL Group inventory distortion research. Figures are industry ranges.
Everything you need to know about the service and how it works. Can’t find an answer? Mail us at info@galific.com
Galific designs, builds, and runs inventory agents like this on ADK, the Claude SDK, or LangGraph, wired into your POS, your ERP, and your suppliers. Or explore the ready-made versions, from Low Stock Alert to the Demand Planner and JIT Restock, in our agent suite.