frappe-web-forms
Frappe Web Forms
Build public-facing web forms for data collection, submissions, and customer self-service.
When to use
- Creating forms for external users (no Desk access)
- Building support/ticket submission forms
- Collecting customer feedback or registrations
- Enabling self-service data entry portals
- Replacing simple portal pages with form-based workflows
Inputs required
- Target DocType for form submissions
- Which fields to expose on the web form
- Authentication requirements (login required vs guest)
- Whether users can edit/resubmit entries
- File upload requirements
Procedure
0) Prerequisites
Ensure the target DocType exists and has the fields you want to expose.
1) Create the Web Form
- Type "new web form" in the awesomebar
- Enter a Title (becomes the URL slug)
- Select the DocType for record creation
- Add introduction text (optional, shown above the form)
- Click "Get Fields" to import all fields, or add fields manually
- Set field order and which are required
- Publish the form
2) Configure settings
| Setting | Purpose |
|---|---|
| Login Required | Require authentication before form access |
| Allow Edit | Let users edit their submitted entries |
| Allow Multiple | Let users submit more than one entry |
| Show as Card | Display in card layout style |
| Max Attachment Size | Limit file upload sizes |
| Success URL | Redirect after successful submission |
| Success Message | Custom message after submission |
3) Make it a Standard Web Form (app-bundled)
Check "Is Standard" (visible in Developer Mode) to export the form as files:
my_app/
└── my_module/
└── web_form/
└── contact_us/
├── contact_us.json # Web form metadata
├── contact_us.py # Server-side customization
└── contact_us.js # Client-side customization
4) Add server-side customization
# contact_us.py
import frappe
def get_context(context):
"""Add custom context variables to the web form."""
context.categories = frappe.get_all("Support Category",
filters={"enabled": 1},
fields=["name", "label"],
order_by="label asc"
)
def validate(doc):
"""Custom validation before the document is saved."""
if not doc.email:
frappe.throw("Email address is required")
# Prevent duplicate submissions
existing = frappe.db.exists("Support Ticket", {"email": doc.email, "status": "Open"})
if existing:
frappe.throw("You already have an open ticket. Please wait for a response.")
5) Add client-side customization
// contact_us.js
frappe.ready(function() {
// Handle field changes
frappe.web_form.on("field_change", function(field, value) {
if (field === "category" && value === "Urgent") {
frappe.web_form.set_df_property("description", "reqd", 1);
}
});
// Custom validation
frappe.web_form.validate = function() {
let data = frappe.web_form.get_values();
if (data.phone && !data.phone.match(/^\+?[0-9\-\s]+$/)) {
frappe.msgprint("Please enter a valid phone number");
return false;
}
return true;
};
// Custom after-save behavior
frappe.web_form.after_save = function() {
frappe.msgprint("Thank you for your submission!");
};
});
6) Control permissions
- Guest access: Uncheck "Login Required" for fully public forms
- Portal roles: Assign portal roles to control which logged-in users see the form
- User permissions: Set explicit document-level permissions on the target DocType
- Row-level access: Use User Permission rules to restrict which records users can edit
7) Style the web form
Web forms use the website theme by default. For custom styling:
<!-- Add custom CSS via Web Form → Custom CSS field -->
<style>
.web-form-container { max-width: 600px; margin: 0 auto; }
.web-form-container .form-group { margin-bottom: 1.5rem; }
.web-form-container .btn-primary { background-color: #2490EF; }
</style>
Verification
- Web form accessible at the correct URL (
/contact-us) - All fields render correctly
- Required field validation works
- Submission creates the correct DocType record
- Login requirement enforced (if configured)
- Edit and resubmit work (if configured)
- File uploads work within size limits
- Success message/redirect works after submission
- Custom Python validation runs on submit
Failure modes / debugging
- Form not accessible: Check if published; verify URL slug
- Permission denied on submit: Check DocType permissions for Website User or Guest
- Fields not showing: Ensure fields are added to the Web Form (not just on the DocType)
- Custom JS not loading: Check browser console; ensure file path is correct
- Validation not firing: Verify
validatefunction in Python file returns/throws correctly - Duplicate entries: Check "Allow Multiple" setting; add custom duplicate detection
Escalation
- For DocType schema →
frappe-doctype-development - For Frappe UI portal apps →
frappe-frontend-development - For API endpoint access →
frappe-api-development
References
- references/web-forms.md — Web Form creation and customization
Guardrails
- Validate input server-side: Never trust client validation; check in
validate()Python method - Use captcha for public forms: Enable reCAPTCHA for guest-accessible forms to prevent spam
- Sanitize output: Escape user-submitted data when displaying; use
frappe.utils.escape_html() - Limit file uploads: Set max file size and allowed types for attachment fields
- Check rate limits: Consider throttling form submissions from same IP
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
| Missing DocType permissions | "Permission denied" on submit | Grant Create permission to Website User or Guest role |
| Not handling file uploads | Files don't attach to record | Configure Attach field properly; check upload limits |
| XSS vulnerabilities | Security risk | Escape user input in display; use ` |
| Forgetting to publish form | 404 error | Check "Published" checkbox in Web Form |
| Client-only validation | Invalid data in database | Add validate() method in web form Python file |
| Not testing as guest user | Works for admin, fails for users | Test in incognito/logged out mode |
More from lubusin/agent-skills
frappe-frontend-development
Build modern Vue 3 frontend apps using Frappe UI with components, data fetching, and portal pages. Use when creating custom frontends, SPAs, or portal interfaces for Frappe applications.
94frappe-app-development
Scaffold and architect custom Frappe apps including app structure, hooks, background jobs, service layers, and production hardening. Use when creating new apps, setting up app architecture, or implementing cross-cutting patterns like caching, logging, and error handling.
85frappe-router
Route to the appropriate Frappe skill based on task type. Use as the entry point when working on Frappe projects to determine which specialized skill to apply.
82frappe-api-development
Build REST and RPC APIs in Frappe including whitelisted methods, authentication, and permission handling. Use when creating custom endpoints, integrating with external systems, or exposing business logic via API.
82frappe-desk-customization
Customize Frappe Desk UI with form scripts, list view scripts, report scripts, dialogs, and client-side JavaScript APIs. Use when building interactive Desk experiences, adding custom buttons, or scripting form behavior.
80frappe-doctype-development
Create and modify Frappe DocTypes including schema design, controllers, child tables, and customization. Use when building data models, adding fields, or implementing document lifecycle logic.
78