Frappe_Claude_Skill_Package frappe-impl-whitelisted
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/impl/frappe-impl-whitelisted" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-impl-whitelisted && rm -rf "$T"
manifest:
skills/source/impl/frappe-impl-whitelisted/SKILL.mdsource content
Frappe Whitelisted Methods Implementation Workflow
Step-by-step workflows for building API endpoints. For decorator syntax, see
frappe-syntax-whitelisted.
Version: v14/v15/v16 (version-specific features noted)
Master Decision: What Type of Endpoint?
WHAT ARE YOU BUILDING? │ ├─► Public API (no login required)? │ └─► allow_guest=True + STRICT input validation + rate limiting │ ├─► Authenticated API for logged-in users? │ └─► Default @frappe.whitelist() + document permission checks │ ├─► Admin-only API? │ └─► frappe.only_for("System Manager") │ ├─► Document-specific method (called from form)? │ └─► Controller method + frm.call() from JS │ ├─► Standalone utility API? │ └─► Separate api.py + frappe.call() from JS │ ├─► External webhook receiver? │ └─► allow_guest=True + signature verification │ └─► Background job trigger? └─► Authenticated API that calls frappe.enqueue()
Workflow 1: Design the Endpoint
Step 1: Choose Location
WHERE SHOULD THE CODE LIVE? │ ├─► Related to a DocType, called from its form? │ └─► doctype/xxx/xxx.py (controller method) │ Client: frm.call('method_name', args) │ ├─► Related to a DocType, standalone? │ └─► doctype/xxx/xxx_api.py or myapp/api/module.py │ Client: frappe.call('myapp.api.module.method') │ ├─► General app utility? │ └─► myapp/api.py (small app) or myapp/api/module.py (large app) │ └─► External integration? └─► myapp/integrations/service_name.py
Step 2: Choose Permission Model
WHO CAN CALL THIS API? │ ├─► Anyone (public) → allow_guest=True │ ⚠️ MUST validate ALL input, sanitize for XSS, rate limit │ ├─► Any logged-in user → Default (no allow_guest) │ Still check document permissions per record! │ ├─► Specific role(s) → frappe.only_for("Role") │ └─► Document-level → frappe.has_permission(doctype, ptype, doc)
Step 3: Choose HTTP Method
WHAT DOES THE API DO? │ ├─► Read-only → methods=["GET"] ├─► Creates/modifies data → methods=["POST"] └─► Both or default → omit methods parameter (all allowed)
Workflow 2: Implement an Authenticated API
Step-by-Step
Step 1: Create the function
# myapp/api.py import frappe from frappe import _ @frappe.whitelist() def get_customer_balance(customer): """Get outstanding balance for a customer.""" # 1. Permission check if not frappe.has_permission("Customer", "read", customer): frappe.throw(_("Not permitted"), frappe.PermissionError) # 2. Validate input if not customer or not frappe.db.exists("Customer", customer): frappe.throw(_("Customer not found"), frappe.DoesNotExistError) # 3. Fetch and return balance = frappe.db.sql(""" SELECT COALESCE(SUM(outstanding_amount), 0) FROM `tabSales Invoice` WHERE customer = %s AND docstatus = 1 """, customer)[0][0] return {"customer": customer, "balance": balance}
Step 2: Call from Client Script
frappe.call({ method: 'myapp.api.get_customer_balance', args: { customer: 'CUST-00001' }, callback(r) { if (r.message) console.log(r.message.balance); } });
Step 3: Test with curl
# Authenticate first curl -X POST https://site.com/api/method/login \ -d 'usr=admin&pwd=password' # Call the API curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \ -H "Content-Type: application/json" \ -d '{"customer": "CUST-00001"}' \ --cookie cookies.txt # Or use token auth curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \ -H "Authorization: token api_key:api_secret" \ -H "Content-Type: application/json" \ -d '{"customer": "CUST-00001"}'
Workflow 3: Implement a Public (Guest) API
Step-by-Step
Step 1: Create with strict validation
@frappe.whitelist(allow_guest=True, methods=["POST"]) def submit_inquiry(name, email, phone=None, message=None): """Public contact form — strict validation required.""" # 1. Validate required fields if not all([name, email]): frappe.throw(_("Name and email are required")) # 2. Validate email format if not frappe.utils.validate_email_address(email): frappe.throw(_("Invalid email address")) # 3. Sanitize ALL input name = frappe.utils.strip_html(name)[:100] email = email.strip().lower()[:200] phone = frappe.utils.strip_html(phone)[:20] if phone else None message = frappe.utils.strip_html(message)[:2000] if message else None # 4. Create record with ignore_permissions lead = frappe.get_doc({ "doctype": "Lead", "lead_name": name, "email_id": email, "phone": phone, "notes": message, "source": "Website" }) lead.insert(ignore_permissions=True) return {"success": True, "message": _("Thank you")}
Step 2: Add rate limiting (v15+)
from frappe.rate_limiter import rate_limit @frappe.whitelist(allow_guest=True, methods=["POST"]) @rate_limit(limit=5, seconds=60) # 5 calls per minute def submit_inquiry(name, email, phone=None, message=None): ...
Critical Rules for Guest APIs
- ALWAYS validate and sanitize every input parameter
- ALWAYS use
for data-writing endpointsmethods=["POST"] - ALWAYS add rate limiting (v15+ decorator or manual cache-based throttle on v14)
- NEVER expose internal error details — log with
, show generic messagefrappe.log_error() - NEVER return sensitive data (internal IDs, file paths, stack traces)
- NEVER pass raw user input to
— use explicit field mappingfrappe.get_doc()
Workflow 4: Implement a Controller Method
Step-by-Step
Step 1: Add method to DocType controller
# myapp/doctype/sales_order/sales_order.py class SalesOrder(Document): @frappe.whitelist() def calculate_shipping(self, carrier): """Called from form via frm.call().""" if not self.shipping_address: frappe.throw(_("Shipping address required")) rate = get_shipping_rate(self.shipping_address, carrier) return {"carrier": carrier, "rate": rate}
Step 2: Call from form JS
frm.call('calculate_shipping', { carrier: 'FedEx' }).then(r => { frm.set_value('shipping_amount', r.message.rate); });
Key difference: Frappe automatically checks document permissions for controller methods called via
frm.call(). No manual permission check needed for the document itself.
Workflow 5: Implement Error Handling
Standard Pattern
@frappe.whitelist() def process_payment(invoice, amount): try: # Validate if not invoice: frappe.throw(_("Invoice required"), frappe.ValidationError) if not frappe.has_permission("Sales Invoice", "write", invoice): frappe.throw(_("Not permitted"), frappe.PermissionError) # Process result = do_payment(invoice, float(amount)) return {"success": True, "data": result} except (frappe.ValidationError, frappe.PermissionError): raise # Let Frappe handle (417 / 403) except frappe.DoesNotExistError: frappe.throw(_("Not found"), frappe.DoesNotExistError) # 404 except Exception: frappe.log_error(frappe.get_traceback(), "Payment Error") frappe.local.response["http_status_code"] = 500 return {"success": False, "error": _("Internal error")}
HTTP Status Code Reference
| Code | Frappe Exception | When |
|---|---|---|
| 200 | — | Success |
| 403 | PermissionError | Access denied |
| 404 | DoesNotExistError | Not found |
| 409 | DuplicateEntryError | Duplicate |
| 417 | ValidationError | Validation failed |
| 429 | — | Rate limit exceeded (v15+) |
| 500 | Exception | Server error |
Workflow 6: File Upload Endpoint
@frappe.whitelist() def upload_attachment(doctype, docname): """Handle file upload attached to a document.""" if not frappe.has_permission(doctype, "write", docname): frappe.throw(_("Not permitted"), frappe.PermissionError) file = frappe.request.files.get('file') if not file: frappe.throw(_("No file provided")) file_doc = frappe.get_doc({ "doctype": "File", "file_name": file.filename, "attached_to_doctype": doctype, "attached_to_name": docname, "content": file.read(), "is_private": 1 }) file_doc.insert(ignore_permissions=True) return {"file_url": file_doc.file_url}
Workflow 7: Background Job Endpoint
@frappe.whitelist() def start_heavy_export(doctype, filters=None): """Trigger a background job — returns immediately.""" frappe.only_for("System Manager") frappe.enqueue( "myapp.tasks.export_data", queue="long", timeout=1500, doctype=doctype, filters=filters, user=frappe.session.user ) return {"status": "queued", "message": _("Export started")}
Client Integration Patterns
frappe.call() with Options
frappe.call({ method: 'myapp.api.my_method', args: { param: 'value' }, freeze: true, freeze_message: __('Processing...'), callback(r) { console.log(r.message); }, error(r) { frappe.msgprint(__('Error')); } });
Async/Await Pattern
const r = await frappe.call({ method: 'myapp.api.get_data', args: { id: 123 } }); console.log(r.message);
REST API from External System
# Token auth (recommended for integrations) curl -H "Authorization: token api_key:api_secret" \ https://site.com/api/method/myapp.api.method # Bearer auth (OAuth) curl -H "Authorization: Bearer access_token" \ https://site.com/api/method/myapp.api.method
Security Rules (ALWAYS/NEVER)
- ALWAYS check permissions before accessing any document
- ALWAYS use parameterized queries — NEVER use f-strings in SQL
- ALWAYS validate and sanitize input for guest APIs
- ALWAYS check role with
before usingfrappe.only_for()ignore_permissions=True - NEVER expose internal errors — log details, return generic message
- NEVER use
for endpoints that modify datamethods=["GET"] - NEVER trust
dicts from guest APIs — use explicit parameter namesdata - ALWAYS include
header in fetch() calls from browserX-Frappe-CSRF-Token
Migration: Server Script API to Whitelisted Method
| Step | Action |
|---|---|
| 1 | Copy Server Script logic to |
| 2 | Add decorator with same permission model |
| 3 | Update all references to new dotted path |
| 4 | Disable or delete the Server Script |
| 5 | Run |
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| @frappe.whitelist() | Yes | Yes | Yes |
| allow_guest | Yes | Yes | Yes |
| methods parameter | Yes | Yes | Yes |
| Type annotation validation | No | Yes | Yes |
| @rate_limit decorator | No | Yes | Yes |
| API v2 endpoints | No | Yes | Yes |
v15+ Type Validation
@frappe.whitelist() def typed_api(customer: str, limit: int = 10) -> dict: """v15+ validates types from annotations automatically.""" return {"customer": customer, "limit": limit}
Reference Files
| File | Contents |
|---|---|
| decision-tree.md | Complete API type and permission selection guide |
| workflows.md | Step-by-step implementation patterns (10+ workflows) |
| examples.md | Production-ready code examples |