This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
At the start of every session, read .claude/memory/memory.md to load project context.
After completing significant work (new patterns, architectural decisions, solved problems),
update .claude/memory/memory.md. Keep it under 300 lines — summarize when it grows.
- Frappe Framework is a full-stack web application framework that contains all the necessary components for building modern web applications.
- It provides background workers using Redis, real-time updates using sockets, and a database layer using MariaDB.
- Bench is the official command-line tool for managing Frappe applications.
-
Always use built-in functions for parsing JSON:
frappe.parse_json(handles dicts, lists, and JSON strings safely)
-
Never use
json.loadsdirectly on request data. -
For outbound HTTP requests (calling external APIs), use:
frappe.integration.utils.make_get_requestfrappe.integration.utils.make_post_requestfrappe.integration.utils.make_put_requestfrappe.integration.utils.make_patch_request
-
For converting datatypes (e.g. str → int, str → float, etc.) use built-in helpers:
frappe.utils.data.cintfrappe.utils.data.cstrfrappe.utils.data.fltfrappe.utils.data.getdatefrappe.utils.data.get_datetime
-
frappe.utils.datacontains most conversion and formatting helpers you will ever need:- date / datetime parsing
- currency formatting
- number formatting
-
Do NOT create custom utility functions for these conversions.
-
If unsure, ask before implementing.
-
When fetching an existing DocType, prefer:
frappe.get_cached_doc
-
Use
frappe.get_docwhen:-
creating a new document
-
To create a new doc go to bench console via bench --site sitename console and use frappe.new_doc("DocType") and then create the doc, don't create the doc via json as the validations doesn't run
-
- Don't use get_doc or get_cached_doc inside for loop it creates n+1 db problem use frappe.get_all with all the params required and then loop over that list
-
Prefer ORM methods:
frappe.get_allfrappe.get_listfrappe.db.get_value
-
Avoid raw SQL absolutely.
- Always respect user permissions.
- Use
ignore_permissions=Trueonly when absolutely required and justified.
-
For long-running or heavy operations, always use:
frappe.enqueue
-
Never block request-response cycles with heavy business logic.
- Use
frappe.throwor specific exceptions likefrappe.ValidationErrorfor user-facing errors. - Use
frappe.log_errorfor unexpected or system-level exceptions. - Avoid bare
except:blocks.
- Prefer framework conventions over custom implementations.
- Keep business logic out of controllers where possible.
- Write readable, predictable, and maintainable code.
-
Always use async/await; avoid callback-based patterns and nested promises.
-
Use Frappe-provided APIs for server calls:
frappe.callwithasync: true. Prefer Promise-based usage over callbacks. -
Use Frappe's global JS helpers instead of native JS equivalents:
cstr()instead ofString()cint()instead ofparseInt()flt()instead ofparseFloat()is_null()instead of manual null/undefined/empty checksformat_currency()for currency formatting
Always use gemini as much as possible for getting the context, to get the help use gemini --help
For checking if the site works you can use the agent-browser use agent-browser --help to get the context for it
# dev server
yarn dev # or: cd dashboard && yarn dev
# build for production
yarn build # outputs to buzz/public/dashboard + buzz/www/dashboard.html
# lint/format frontend
cd dashboard && yarn lintAlways run bench migrate after doctype schema changes.
# linting/formatting (via pre-commit)
pre-commit run --all-files
# run ruff directly
ruff check buzz/
ruff format buzz/
# install app to site
bench --site [site-name] install-app buzzUse bench --help to see how to work with frappe bench, e.g. bench execute, bench console, etc. are very useful
There are unit tests, run using bench run-tests. Site name is buzz.localhost, but if not found, ask user for it. The credentials are Administrator/admin.
- To test in UI, use agent-browser.
- For frontend changes use :8080 since yarn dev server is running.
- Use in headed mode unless specified
Three-tier stack:
- Backend: Frappe Framework (Python) - DocTypes, API, permissions, scheduler
- Dashboard: Vue 3 + FrappeUI + Vite - attendee/sponsor/checkin UI
Core entity: Buzz Event DocType drives everything (tickets, sponsors, schedule, payments).
Main modules (inside buzz/):
events/- Event, Venue, Category, Talks, Sponsors, Check-insticketing/- Bookings, Tickets, Add-ons, Cancellations, Couponsproposals/- Talk Proposals, Sponsorship Enquiriesbuzz/- Settings, Custom Fieldsapi.py- whitelisted API methods for dashboardpayments.py- integration with frappe/payments app
Frontend structure (inside dashboard/):
src/pages/- route components (BookTickets, TicketDetails, CheckInScanner, etc)src/components/- BookingForm, dialogs, shared UIsrc/composables/- reusable logic (useTicketValidation, usePaymentSuccess, etc)src/data/- frappe-ui resources for API calls- Vite builds to
buzz/public/dashboard/, router base is/dashboard
Key flows:
- Booking: load event data → fill form → create booking → generate payment link → on payment auth → submit booking → generate tickets + QR + email
- Ticket actions: transfer, cancel, change add-on (window checks from Buzz Settings)
- Sponsorship: enquiry → approval → payment link → payment auth → create sponsor record
- Check-in: scan QR → validate → create check-in record (requires Frontdesk Manager role)
Integrations:
frappe/paymentsrequired for payment gatewaysbuildwithhussain/zoom_integrationoptional for webinar creation/registration
Booking changes: buzz/api.py, buzz/ticketing/doctype/event_booking/, dashboard/src/components/BookingForm.vue
Ticket lifecycle: buzz/ticketing/doctype/event_ticket/, dashboard/src/pages/TicketDetails.vue
Sponsorships: buzz/proposals/doctype/sponsorship_enquiry/, dashboard/src/pages/SponsorshipDetails.vue
Check-in: buzz/api.py (validate_ticket_for_checkin, checkin_ticket), dashboard/src/pages/CheckInScanner.vue
Event config: buzz/events/doctype/buzz_event/
Reports: buzz/events/report/ and buzz/ticketing/report/
-
Ban
frappe.db.sqlin new code- Add a pre-commit rule or CI step that greps for
\.db\.sqland fails the build. - Legacy code => wrap in
frappe.db.sql("...", as_dict=1)and add a# TODO-QBcomment so the next refactor is trackable.
- Add a pre-commit rule or CI step that greps for
-
Use the typed entry point
from frappe.query_builder import DocType, Field from frappe.query_builder.functions import Count, Sum, Coalesce, Date
Never
import pypikadirectly; thefrappe.qbnamespace already returns the correctMariaDB/PostgreSQLdialect. -
Parameterise, never interpolate
# Bad frappe.db.sql(f"... {user_input}") # injection bomb # Good frappe.qb.from_(...).where(table.field == user_input) # auto-escaped
-
Prefer joins over N+1
so = DocType("Sales Order") si = DocType("Sales Invoice") query = ( frappe.qb.from_(so) .left_join(si) .on(so.name == si.sales_order) .select(so.name, si.name) .where(so.customer == customer) )
One round-trip, no loops.
-
Sub-queries > raw SQL strings Need "latest row per group"?
latest = ( frappe.qb.from_(si) .select(si.name) .where(si.sales_order == so.name) .orderby(si.creation, order=Order.desc) .limit(1) ) query = frappe.qb.from_(so).where(so.name == latest)
Keeps everything composable and dialect-agnostic.
-
Use
casefor conditional aggregatesfrom frappe.query_builder.functions import Case paid_amt = Sum( Case() .when(si.status == "Paid", si.grand_total) .else_(0) )
-
Respect Frappe field casing
- SQL column:
grand_total - Frappe field:
grand_total - No back-ticks needed; QB adds the correct quotes per DB.
- SQL column:
-
Use
as_dict=Trueor ORM objectsrows = query.run(as_dict=True) # list[dict] docs = query.run(as_dict=False) # list[tuple] obj = frappe.get_doc("Doctype", pk) # when you need the full DocType hooks
-
Pagination with
limit_page_lengthandlimit_startquery = query.limit(limit_page_length).offset(limit_start)
Same pattern the REST API uses.
-
Index-friendly WHERE order Put indexed columns first (
company,customer,status) so MariaDB/PostgreSQL can use composite indexes. -
Avoid
SELECT *in reports Explicit list of fields keeps wire-size small and prevents breaking changes when new fields are added. -
Cache heavy aggregations
@frappe.whitelist() @redis_cache(ttl=300) def get_dashboard_stats(company): inv = DocType("Sales Invoice") total = frappe.qb.from_(inv).select(Sum(inv.grand_total)).where(inv.company == company).run() return total[0][0] or 0
Legacy:
rows = frappe.db.sql("""
select name, grand_total
from `tabSales Invoice`
where customer = %s
and docstatus = 1
""", customer, as_dict=1)QB equivalent:
si = DocType("Sales Invoice")
rows = (
frappe.qb.from_(si)
.select(si.name, si.grand_total)
.where((si.customer == customer) & (si.docstatus == 1))
.run(as_dict=True)
)- Entry:
def execute(filters): return get_columns(), get_data(filters) - QB imports:
from frappe.query_builder import DocType+functions.Sum, Case, Count - Build lookup maps first, then loop + merge (avoid N+1)
- Caching:
@redis_cache(ttl=seconds)for conversion factors
- Default to
/model haikufor routine edits,/model sonnetfor moderate tasks - Use
/model opusONLY for architecture, debugging complex issues - Use
/compactafter completing each subtask - Use
gemini -p "prompt"via stdin to read/summarize files without burning Claude tokens - Scope tasks narrowly: one feature/fix per session
- Dump progress to
.claude/memory/scratch.mdbefore session ends - At session START: read
.claude/memory/scratch.md— if it has content, resume from there - At session END (or when user says "dump progress"): fill in scratch.md with current task state
- After resuming from scratch.md, clear it once the task is complete
- always use full names for variables don't use abbreviations for ex: use "for row in rows" instead of "for r in rows" proxy_sku = DocType("Proxy SKU") instead of ps = DocType("Proxy SKU")
- avoid starting python functions with underscore "_" unless it's a private version behind a whitelisted function (e.g.
get_dial_codes(whitelisted) ->_get_dial_codes(cached logic)) - use camelCase in JS and follow the surrounding code style in the project
- always put imports at the top of the file, never inside functions
- Read
ARCHITECTURE.mdfor comprehensive details on data model, API surface, flows