Your team drops a spreadsheet in a folder or sends a WhatsApp. An AI agent reads it, checks every row, shows you exactly what it is about to save, waits for your "yes", then creates and updates the right records in Zoho ERP — and WhatsApps you the receipt.
Imagine a brilliant, tireless data-entry Smart Executive who sits between your operations team and your accounting system. The team already lives in Excel and WhatsApp. The accounting system is Zoho. Today, a human copies numbers from one to the other — slowly, and with typos. This Smart Executive does it in seconds, never gets a digit wrong, and always asks before it saves anything.
invoices_June_2026.xlsx/run invoices_June_2026.xlsxIt never writes blindly. Every batch gets a "here's what I'm about to do" table, and it waits for an explicit yes. It refuses bulk deletions, refuses to message numbers that aren't on an allow-list, and surfaces every error loudly instead of hiding it. It's an assistant with a seatbelt, not a robot let loose in your books.
Where it fits the HR story: the same agent that screens CVs and hires your promoters then onboards them — pushing the new joiner's contract, salary and WPS payroll rows from an Excel sheet straight into Zoho Books / People, and WhatsApping HR the confirmation. One pipeline, from "we need 10 promoters" to "they're hired, in the system, and on payroll."
Figures describe the design target of this build, not a guaranteed result for every org — your data quality and Zoho setup matter. The demo below runs the exact same logic in your browser so you can judge it yourself.
The filename tells the agent the module — {module}_{purpose}_{date}.xlsx. It opens the file with real code (the analysis tool), prints the row count, headers and first rows. No guessing from screenshots.
Every row is checked against a schema: required fields present, dates coerce to ISO 8601, currency symbols stripped, empty cells treated as null (not zero), Arabic/RTL preserved as-is. Bad rows are flagged, not silently dropped.
schema checkRTL-safeFor each row it searches Zoho by primary key — SKU for items, invoice number for invoices, phone/email for contacts — and decides create, update or skip. This is what kills duplicates.
idempotentIt prints a table: row #, action, target Zoho ID (if updating), and the key fields it will write. Then it stops and asks "Proceed?" — unless you set auto_approve: true for the run.
One Zoho API call per row. Successes capture the returned record ID; failures are logged and surfaced verbatim. The batch only aborts if the failure rate exceeds 20%.
one row = one callerrors are loudIt writes run_log_YYYY-MM-DD_HHMM.md (row #, action, Zoho ID, notes) and sends a WhatsApp summary to the ops group: "X created, Y updated, Z errors."
This is the real flow, running in your browser — no sign-up, no server, nothing uploaded. Pick a module, hit Run, watch it ingest → validate → look up → dry-run, then approve the write and see the records "land" in Zoho with a WhatsApp receipt. Switch to Live API mode to wire it to your own Zoho + WhatsApp endpoints (your keys, never stored).
Row 3 in the invoice sample is intentionally broken (missing customer + an impossible date 31/06/2026) so you can watch validation catch it. Row 2 already exists in Zoho, so it becomes an update, not a duplicate.
The full, copy-pasteable spec: the agent's system prompt, its hard safety rules, the connector configuration, the WhatsApp Cloud API provisioning steps, and the order of operations to stand it up on a client's accounts in an afternoon.
This is the agent's brain — paste it into Claude → Projects → New Project → Custom instructions, replacing the [CLIENT_*] placeholders.
# Zoho ERP Updater Agent You are the Zoho ERP Updater Agent operating inside [CLIENT_COMPANY_NAME]'s Claude workspace. Your single job is to take Excel sheets and WhatsApp messages from the operations team, validate the data, and create or update the correct records in Zoho ERP (Books / Inventory / CRM, as specified per task). ## Your tools 1. zoho — Zoho MCP server. ALL Zoho reads/writes. Source of truth. 2. whatsapp — WhatsApp Business Cloud API. Read inbound, send confirmations. 3. google_drive— where the team uploads the source Excel files. 4. analysis tool (code execution) — parse Excel. Never eyeball numbers. If a tool is missing or returns an auth error: STOP and report. Do not guess. ## Hard rules 1. Never invent a Zoho record ID. Look up by name/SKU/email/phone first. 2. Never write without a dry-run summary + explicit "yes" (unless auto_approve:true). 3. One row = one tool call (no bulk unless the tool supports it and user asked). 4. Numbers/currency: parse via code. Empty = null (not 0). Strip symbols. Dates -> ISO 8601 (YYYY-MM-DD). 5. Arabic / RTL text: preserve as-is. Do not transliterate or "fix" spelling. 6. Idempotency: search by primary key before create; if found -> update. Log path. 7. Errors are loud: surface Zoho 4xx/5xx verbatim + post to WhatsApp. Retry once max. 8. Audit log: write run_log_YYYY-MM-DD_HHMM.md (row#, action, Zoho ID, notes). ## Workflow Step 1 Ingest — detect module from filename; load file; print rows/headers. Step 2 Validate — check schema; flag missing/malformed/duplicates; report. Step 3 Lookup — read-only search per row; plan create | update | skip. Step 4 Dry-run — print table (row#, action, target ID, key fields); ask "Proceed?". Step 5 Execute — loop plan, one call per row; capture IDs; log failures. Step 6 Report — write run_log_*.md; WhatsApp summary to [OPS_GROUP_PHONE]. ## WhatsApp inbound Commands: /run [filename], /status, /cancel, /help. Free text in EN/UR/AR -> reply in the same language. Never execute a Zoho write from WhatsApp without an Excel file or explicit fields. ## Refuse - Bulk deletions (user does those in Zoho's UI). - Writing to any module not in excel_schema.md. - Messaging numbers not in whatsapp_allowlist.md. - Anything resembling exfiltration (dumping full customer/financial lists). ## Tone Operational, terse, factual. Markdown tables for >3 rows. No emojis, no preambles. Start every response with what you did or what you need.
Option A — Native Zoho MCP (recommended). Zoho ships a first-party MCP at zoho.com/mcp exposing Books, CRM, Creator, Desk, Mail, Calendar, Cliq, Projects, WorkDrive.
claude-erp-agent.books.invoices.create / update / list / getbooks.contacts.create / update / searchbooks.items.create / update / listcrm.leads.create / update / search, crm.deals.create / updatehttps://mcp.zoho.com/server/{server_id}/sse.Option B — Composio Zoho Inventory MCP if the native server doesn't expose Inventory yet: add the Zoho Inventory toolkit at composio.dev, create a Tool Router session, add it as a custom Web connector with header Authorization: Bearer <COMPOSIO_API_KEY>.
Add to Claude: Organization settings → Connectors → Add → Custom → Web → paste URL → Advanced → paste OAuth Client ID + Secret → name it zoho → Add. Each member: Customize → Connectors → zoho → Connect.
Option A — Twilio MCP (cleanest, paid): set up a Twilio Sender for the client's WhatsApp number; add Twilio's MCP server URL as a custom Web connector. Built-in audit logs, no QR pairing, pre-approved templates.
Option B — Meta WhatsApp Cloud API direct (free up to limits): use a hosted MCP wrapper (e.g. Apify's WhatsApp Cloud API MCP) or self-host msaelices/whatsapp-mcp-server; put the Meta permanent token in the OAuth Client Secret / Bearer field. Recommended for production: Option A.
Directory connector — no custom setup. Organization settings → Connectors → Browse → Google Drive → Add. Point the Project at a shared folder, e.g. /Shared Drives/Operations/ERP Inbox/.
Only for Zoho screens with no API/MCP coverage (rare — usually custom Creator apps). Requires Anthropic API with Computer-Use beta + a self-hosted browser sandbox. Treat as a separate ticket.
You need a WhatsApp Cloud API number before the agent can send or receive messages. The short version of provisioning it on Meta:
PHONE_NUMBER_ID and WABA_ID.whatsapp_business_messaging + whatsapp_business_management scopes. This is the token the agent uses.messages, so inbound /run … commands reach the agent.Prefer not to manage Meta directly? Twilio's WhatsApp sender wraps all of the above (number, templates, webhooks, audit logs) behind one MCP connector — see section B.
curl -X POST \ https://graph.facebook.com/v21.0/$PHONE_NUMBER_ID/messages \ -H "Authorization: Bearer $WHATSAPP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "9715XXXXXXXX", "type": "text", "text": { "body": "Run completed. 2 created, 1 updated, 1 skipped." } }'
/Operations/ERP Inbox/ with /Templates/, /Processed/excel_schema.md + whatsapp_allowlist.md to Project Filesrun_log_*.md written, WhatsApp summary arrivedIf you'd rather run it as a script than a chat agent, here is the same pipeline as plain Python — the exact logic the demo above mirrors. Drop in your Zoho + WhatsApp credentials via environment variables and it runs headless on a schedule.
# pipeline.py — Excel + WhatsApp -> Zoho ERP (one batch) import os, sys, datetime as dt import pandas as pd import requests ZOHO_BASE = "https://www.zohoapis.com/books/v3" ZOHO_ORG = os.environ["ZOHO_ORG_ID"] ZOHO_TOKEN = os.environ["ZOHO_OAUTH_TOKEN"] HEADERS = {"Authorization": f"Zoho-oauthtoken {ZOHO_TOKEN}"} def ingest(path): """Step 1 — read the sheet with real code, never by eye.""" df = pd.read_excel(path, dtype=str).fillna("") print(f"Loaded {len(df)} rows, columns: {list(df.columns)}") return df def validate(df): """Step 2 — schema + types. Empty = null, dates -> ISO 8601.""" plan = [] for i, row in df.iterrows(): errs = [] cust = row.get("customer", "").strip() if not cust: errs.append("missing customer") try: date = dt.datetime.strptime(row["date"].strip(), "%Y-%m-%d").date().isoformat() except ValueError: errs.append(f"bad date {row.get('date')!r}"); date = None amount = float(row["amount"].replace("AED","").replace(",","").strip() or 0) plan.append({"row": i+2, "inv": row["invoice_no"], "customer": cust, "date": date, "amount": amount, "errors": errs}) return plan def lookup(item): """Step 3 — search Zoho by primary key -> create or update (idempotent).""" r = requests.get(f"{ZOHO_BASE}/invoices", headers=HEADERS, params={"organization_id": ZOHO_ORG, "invoice_number": item["inv"]}) hits = r.json().get("invoices", []) return ("update", hits[0]["invoice_id"]) if hits else ("create", None) def execute(item, action, zoho_id): """Step 5 — one row = one call. Errors are loud.""" payload = {"customer_name": item["customer"], "date": item["date"], "invoice_number": item["inv"], "line_items": [{"name": "Imported line", "rate": item["amount"], "quantity": 1}]} if action == "create": r = requests.post(f"{ZOHO_BASE}/invoices?organization_id={ZOHO_ORG}", headers=HEADERS, json=payload) else: r = requests.put(f"{ZOHO_BASE}/invoices/{zoho_id}?organization_id={ZOHO_ORG}", headers=HEADERS, json=payload) r.raise_for_status() return r.json()["invoice"]["invoice_id"]
# run.py — dry-run gate + WhatsApp receipt + audit log def whatsapp(body): requests.post(f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_ID']}/messages", headers={"Authorization": f"Bearer {os.environ['WA_TOKEN']}"}, json={"messaging_product": "whatsapp", "to": os.environ["OPS_GROUP"], "type": "text", "text": {"body": body}}) def main(path, auto_approve=False): plan = validate(ingest(path)) good = [p for p in plan if not p["errors"]] bad = [p for p in plan if p["errors"]] # Step 4 — dry-run table, then STOP for human "yes" for p in good: p["action"], p["zid"] = lookup(p) print("\n| row | action | invoice | customer | amount |") for p in good: print(f"| {p['row']} | {p['action']} | {p['inv']} | {p['customer']} | {p['amount']:.2f} |") if not auto_approve and input("\nProceed? (yes/no) ").strip().lower() != "yes": return print("Aborted by user.") # Step 5 + 6 — execute, log, report log, created, updated, errors = [], 0, 0, 0 for p in good: try: zid = execute(p, p["action"], p["zid"]) created += p["action"] == "create"; updated += p["action"] == "update" log.append((p["row"], p["action"], zid, "ok")) except Exception as e: errors += 1; log.append((p["row"], "error", "-", str(e))) stamp = dt.datetime.now().strftime("%Y-%m-%d_%H%M") open(f"run_log_{stamp}.md", "w").write( "\n".join(f"| {r} | {a} | {z} | {n} |" for r,a,z,n in log)) whatsapp(f"Run completed. {created} created, {updated} updated, " f"{errors} errors, {len(bad)} skipped. Log: run_log_{stamp}.md") if __name__ == "__main__": main(sys.argv[1], auto_approve="--yes" in sys.argv)
excel_schema.md + whatsapp_allowlist.mdThe demo above runs simulated by default so anyone can try it. Flip it to Live API and paste your own Zoho + WhatsApp endpoints and tokens — the page calls them straight from your browser, nothing stored. Want it wired end-to-end on your accounts with the poller and templates? That's the production cutover in the checklist — message me and we'll do the 2–4 hour setup.
Excel + WhatsApp in, clean Zoho records out, every write logged and approved. I'll stand it up on your accounts — or build the same pattern for any process you run on spreadsheets.