know.2nth.ai Business biz erp erpnext
biz/erp · ERPNext · Skill Leaf

ERPNext on
REST + RPC.

The open-source ERP on the Frappe framework. Every doctype is a REST resource, and every server-side method is reachable via RPC. The manufacturing stack — Items, BOMs, Work Orders, Job Cards, Stock Entries — is the strongest part of the tree, backing real factories today.

Production REST + RPC Frappe framework Manufacturing v1.1.0

Open-source ERP built on Frappe, API-first.

ERPNext is the enterprise resource planning system most open-source-friendly factories, distributors, and professional services firms end up on. It runs on Frappe — a metadata-driven Python framework where every business entity is a "doctype" and every doctype gets an automatic REST endpoint for free.

You get two APIs for the price of one:

API Base path Use when
/api/resource/<DocType> REST CRUD on any document — Item, Sales Order, BOM, Stock Entry
/api/method/<dotted.path> RPC Server-side logic — stock balance, BOM explosion, document submission

2nth is middleware. Frappe is system of record.

The canonical pattern: read from ERPNext, run an AI-assisted workflow (production planning, reorder suggestions, quality escalations), and write approved decisions back as first-class Frappe documents. Never let the AI be the source of truth for stock or BOMs — the ERP's double-entry and submission semantics exist for reasons a model won't understand.

API Key + Secret — the only auth that belongs in production.

Frappe offers three ways to authenticate. The first is the right one for integrations. The others exist, but use them and you'll regret it.

# 1. API Key + Secret — recommended for agents and integrations
Authorization: token api_key:api_secret

# 2. Basic Auth — for one-off scripts
Authorization: Basic base64(user:password)

# 3. Session cookies — for browser-like flows only
$ curl -X POST https://site.example.com/api/method/login \
    -d 'usr=user@example.com&pwd=password'
# use returned cookies for subsequent requests

Generate the API Key and Secret from User > API Access in the ERPNext UI. The secret is only shown once — store it immediately in a secrets manager.

// Cloudflare Worker — ERPNext list call with token auth
const token = `${env.ERPNEXT_KEY}:${env.ERPNEXT_SECRET}`;
const res = await fetch(
  `${env.ERPNEXT_URL}/api/resource/Item?filters=${filters}&limit_page_length=50`,
  { headers: { 'Authorization': `token ${token}` } }
);
const { data } = await res.json();

Scope your keys.

Generate a read-only user for reporting agents and a separate read/write user for fulfilment and submission. Frappe enforces role-based permissions per doctype — don't give the AI the site administrator role just to make a demo work.

Resources, filters, and the manufacturing flow.

Three things to internalise: how to shape REST requests, how Frappe's JSON filter syntax works, and the document lifecycle that drives the manufacturing flow. Get those right and the rest is typing.

List, get, create, update. All four are a single REST verb on /api/resource/<DocType>. Pagination is off-by-default tiny — set it explicitly.

# List records with filters and specific fields
GET /api/resource/Item
  ?filters=[["item_group","=","Finished Goods"]]
  &fields=["name","item_name","item_group","stock_uom"]
  &limit_page_length=50
  &order_by=item_name asc

# Get a single record by name (the ID, not the display name)
GET /api/resource/Item/ITEM-CODE-001

# Create a new record
POST /api/resource/Item
Content-Type: application/json

{
  "item_code": "ITEM-001",
  "item_name": "Item Name",
  "item_group": "Finished Goods",
  "stock_uom": "Nos"
}

# Partial update
PUT /api/resource/Item/ITEM-001
{ "description": "Updated description" }

Filters are JSON arrays. Each filter is [field, operator, value]. Multiple filters become an array of arrays. Server-side filtering, not client-side post-processing.

Operator Meaning Example
=Equals["item_group","=","Raw Material"]
!=Not equals["status","!=","Cancelled"]
>/>=/</<=Comparison["qty",">",0]
likeWildcard["item_name","like","%Oak%"]
inIn list["status","in",["Open","In Process"]]
betweenRange["posting_date","between",["2026-01-01","2026-03-31"]]

Core doctypes. The ones that come up in every manufacturing integration — not the whole tree, just the load-bearing ones.

Item                      — Products, raw materials, sub-assemblies
BOM                       — Bill of Materials — recipe for a manufactured item
Work Order                — A production order
Job Card                  — Per-operation tracking on a work order
Workstation               — Machine or assembly station
Operation                 — Manufacturing step
Stock Entry               — Inventory movement (issue, receipt, manufacture)
Quality Inspection        — QC checks tied to receipts or work orders
Sales Order               — Customer orders (drives work order creation)
Purchase Order            — Supplier orders
Warehouse                 — Storage locations

The manufacturing flow. If you only know one thing about ERPNext, know this chain. Every real manufacturing integration ends up walking it top-to-bottom.

Sales Order
Work Order (from BOM)
Job Card (per operation)
Stock Entry (material transfer)
Stock Entry (manufacture → finished goods in)
Quality Inspection
Delivery Note
Sales Invoice

RPC methods. Some operations aren't exposed as REST resources — stock balances, BOM explosions, document transformations. These live behind Frappe's method RPC.

POST /api/method/erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry
Content-Type: application/json

{
  "work_order": "WO-00045",
  "purpose": "Material Transfer for Manufacture",
  "qty": 30
}
Method Purpose
frappe.client.get_countCount documents matching filters
frappe.client.get_listList with server-side aggregation
erpnext.stock.utils.get_stock_balanceReal-time stock balance for an item at a warehouse
erpnext.manufacturing.doctype.bom.bom.get_bom_itemsExplode a BOM into its raw material requirements
...work_order.make_stock_entryCreate a stock entry from a Work Order
...sales_order.make_work_orderCreate Work Orders from a Sales Order

Things that only bite in production.

Seven specific failure modes lifted from the canonical skill. Every one of these has cost someone an afternoon at least once.

Docstatus is your source of truth

0 is draft, 1 is submitted, 2 is cancelled. Most queries need ["docstatus","=",1]. Forgetting this means your "sales" list includes draft orders that don't actually exist.

Child tables need explicit access

BOM items, Sales Order items, Stock Entry items are child tables on the parent document. Fetch them via the parent GET or with dotted fields like fields=["items.item_code","items.qty"].

5 req/sec rate limit by default

Frappe's default rate limit for API keys is 5 requests per second. Scheduled syncs need batching or throttling — or you'll spend an hour wondering why every other call fails.

Pagination defaults to 20

Set limit_page_length explicitly. Use limit_page_length=0 to fetch everything, but only on small doctypes — on a 50k-item master this will hang the site.

name is the ID, not the display name

Every Frappe doctype has a name field that's the record's unique ID. Use name for lookups, not the display-friendly field like item_name.

BOMs are versioned — always filter by active

An item can have many BOMs. For production queries, filter with ["is_active","=",1] and ["is_default","=",1] — otherwise you might pick up an old recipe that still exists but isn't in use.

Stock Entry types are enum-specific

"Material Issue", "Material Receipt", "Material Transfer for Manufacture", "Manufacture" — exact strings. Typos silently fail or create the wrong kind of movement, which is worse than failing because it breaks the audit trail.

What erpnext pulls on, and what pulls on it.

The canonical skill declares its requires and improves in frontmatter. Here are the nodes in the tree erpnext is wired into.

Go deeper.

The canonical SKILL.md is the authoritative version and gets updates as the production skill evolves. Frappe and ERPNext docs are the upstream truth.