name: frappe-impl-workspace description: > Use when creating or customizing Workspace pages in Frappe v14-v16. Covers Workspace DocType structure, shortcuts, number cards, dashboard charts, custom HTML blocks, JSON content format, shipping workspaces with custom apps, and role-based access control. Prevents common mistakes with content/child-table desync and missing fixtures. Keywords: workspace, desk, dashboard, number card, chart, shortcut,, customize desk, dashboard setup, add shortcut, module page, sidebar customize. workspace builder, module, fixtures, sidebar. license: MIT compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16." metadata: author: OpenAEC-Foundation version: "3.0"
Frappe Workspace Implementation Workflow
Step-by-step workflows for creating and customizing Workspace pages. Workspaces are the block-based dashboard/navigation pages in Frappe Desk.
Version: v14/v15/v16 (version-specific features noted)
Quick Reference
| Concept | Description |
|---|---|
| Workspace | Block-based page with 12-column grid layout |
| Public Workspace | Visible to all permitted users; requires Workspace Manager role to edit |
| Private Workspace | Per-user dashboard under "My Workspaces"; any Desk User can create |
| Content field | JSON array storing the block layout |
| Child tables | 6 tables: charts, shortcuts, links, quick_lists, number_cards, custom_blocks |
| Module association | Primary access control mechanism |
Master Decision: What Do You Need?
NEED A WORKSPACE?
│
├─► Default DocType landing page?
│ └─► NO workspace needed — Frappe auto-generates list views
│
├─► Custom dashboard for a module?
│ └─► Create PUBLIC Workspace (Workspace Manager role required)
│
├─► Personal dashboard for a user?
│ └─► Create PRIVATE Workspace (appears under "My Workspaces")
│
└─► Navigation link in sidebar?
└─► type="Link" (internal) or type="URL" (external)
ADDING COMPONENTS?
│
├─► Key metrics (counts, sums) → Number Cards
├─► Trend / time-series data → Dashboard Charts
├─► Quick navigation links → Shortcuts
├─► Grouped link categories → Link Cards (Card Break + Links)
├─► Custom HTML/JS content → Custom HTML Blocks
└─► Recent record lists → Quick Lists
Workspace DocType Structure
Key Fields
| Field | Type | Purpose |
|---|---|---|
label | Data | Display name in sidebar |
title | Data | Page title (defaults to label) |
module | Link → Module Def | Associates workspace with a module for access control |
parent_page | Link → Workspace | Nesting under another workspace in sidebar |
icon | Data | Sidebar icon (e.g., "chart-line") |
type | Select | Workspace / Link / URL (v15+) |
sequence_id | Int | Sidebar ordering |
content | JSON | Block layout as JSON array |
for_user | Data | If set, workspace is private to that user |
roles | Table → Has Role | Role-based access restrictions |
app | Data | Owning app identifier (v15+) |
indicator_color | Color | Sidebar indicator dot (v15+) |
Child Tables (6 total)
| Child Table | DocType | Purpose |
|---|---|---|
charts | Workspace Chart | Dashboard Chart references |
shortcuts | Workspace Shortcut | DocType/Report/Page/URL shortcuts |
links | Workspace Link | Grouped navigation links |
quick_lists | Workspace Quick List | Recent record lists |
number_cards | Workspace Number Card | Metric card references |
custom_blocks | Workspace Custom Block | HTML block references |
CRITICAL: The
contentJSON and the child tables MUST stay in sync. ALWAYS use the Workspace Builder UI or programmatic API — NEVER manually edit thecontentJSON without updating child tables. Seereferences/anti-patterns.md.
Content JSON Format
The content field is a JSON array. Each element represents a block in the 12-column grid:
[
{
"id": "unique-block-id",
"type": "header",
"data": {"text": "Overview", "level": 4, "col": 12}
},
{
"id": "unique-block-id-2",
"type": "chart",
"data": {
"chart_name": "Sales Trends",
"col": 12
}
},
{
"id": "unique-block-id-3",
"type": "number_card",
"data": {
"number_card_name": "Open Orders",
"col": 4
}
},
{
"id": "unique-block-id-4",
"type": "shortcut",
"data": {
"shortcut_name": "New Sales Order",
"col": 4
}
},
{
"id": "unique-block-id-5",
"type": "spacer",
"data": {"col": 12}
}
]
Block Types
| Type | data fields | Description |
|---|---|---|
header | text, level, col | Section heading (h3/h4/h5) |
chart | chart_name, col | References a Dashboard Chart doc |
number_card | number_card_name, col | References a Number Card doc |
shortcut | shortcut_name, col | References a Workspace Shortcut child |
card | card_name, col | Card break for grouped links |
quick_list | quick_list_name, col | Recent records for a DocType |
custom_block | custom_block_name, col | References a Custom HTML Block doc |
text | body, col | Rich text / Markdown block |
spacer | col | Empty vertical space |
onboarding | onboarding_name, col | Module onboarding widget |
colvalues MUST be 1-12 and represent grid column width. Blocks in the same row MUST sum to ≤ 12.
Implementation Workflows
Workflow 1: Create a Public Workspace via UI
- Navigate to
/app/workspace→ click + New Workspace - Set Label (appears in sidebar), Module, Icon
- Use the Workspace Builder to drag-and-drop blocks
- Add components: Charts, Number Cards, Shortcuts, Links
- Click Save → workspace appears in sidebar for permitted users
- In developer mode: JSON auto-exports to your app directory
Workflow 2: Create a Workspace Programmatically
import frappe
import json
workspace = frappe.new_doc("Workspace")
workspace.label = "Project Dashboard"
workspace.module = "Projects"
workspace.icon = "project"
workspace.type = "Workspace"
workspace.sequence_id = 10
# Build content blocks
workspace.content = json.dumps([
{
"id": frappe.generate_hash(length=10),
"type": "header",
"data": {"text": "Project Overview", "level": 4, "col": 12}
},
{
"id": frappe.generate_hash(length=10),
"type": "number_card",
"data": {"number_card_name": "Active Projects", "col": 4}
},
{
"id": frappe.generate_hash(length=10),
"type": "chart",
"data": {"chart_name": "Project Status", "col": 12}
}
])
# Add child table entries (MUST match content JSON)
workspace.append("number_cards", {
"number_card_name": "Active Projects"
})
workspace.append("charts", {
"chart_name": "Project Status"
})
# Role restrictions (optional)
workspace.append("roles", {"role": "Projects Manager"})
workspace.insert(ignore_permissions=True)
frappe.db.commit()
ALWAYS add corresponding child-table rows when setting
contentJSON programmatically.
Workflow 3: Create Supporting Documents First
Before adding components to a workspace, create the referenced documents:
Number Card:
card = frappe.new_doc("Number Card")
card.label = "Active Projects"
card.document_type = "Project"
card.function = "Count"
card.filters_json = json.dumps([["Project", "status", "=", "Open"]])
card.is_public = 1
card.insert(ignore_permissions=True)
Dashboard Chart:
chart = frappe.new_doc("Dashboard Chart")
chart.chart_name = "Project Status"
chart.chart_type = "Group By"
chart.document_type = "Project"
chart.group_by_type = "Count"
chart.group_by_based_on = "status"
chart.type = "Donut"
chart.is_public = 1
chart.insert(ignore_permissions=True)
Shortcut: Shortcuts are child-table entries on the Workspace, not standalone docs:
workspace.append("shortcuts", {
"label": "New Project",
"type": "DocType",
"link_to": "Project",
"color": "Blue",
"format": "{} Active",
"stats_filter": json.dumps([["Project", "status", "=", "Open"]])
})
See references/workspace-components.md for complete component reference.
Permission Model
Three Layers of Access Control
Layer 1: Module Access (PRIMARY)
└─► User must have access to the workspace's module
└─► Controlled via "Module Def" and user's "Block Modules" list
Layer 2: Role Restrictions (OPTIONAL)
└─► workspace.roles child table
└─► If populated: ONLY users with listed roles see the workspace
└─► If empty: ALL users with module access see it
Layer 3: Workspace Manager Role
└─► Required to create/edit PUBLIC workspaces
└─► NOT required for private workspaces
Rules
- ALWAYS set
moduleon public workspaces — without it, the workspace is visible to ALL Desk users - ALWAYS add role restrictions for sensitive dashboards (financial, HR)
- NEVER set
for_useron workspaces shipped with an app — it creates a private workspace
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| Workspace Builder UI | Basic | Redesigned (drag-drop grid) | Incremental fixes |
type field (Workspace/Link/URL) | Not available | Added | Available |
app field | Not available | Added | Available |
indicator_color | Not available | Added | Available |
| Name collision protection | Manual | Manual | Auto-deduplicate |
| Welcome header config | Not available | Not available | Added |
| Content JSON format | Same | Same | Same |
Migration Notes
- v14 → v15: Workspace Builder UI changed significantly; existing JSON content remains compatible
- v15 → v16: Minor field additions; no breaking changes to workspace structure
- ALWAYS test workspace rendering after major version upgrades
Shipping Workspaces with a Custom App
Directory Structure
myapp/
└── mymodule/
└── workspace/
└── my_workspace/
└── my_workspace.json
Export Process
- Enable Developer Mode (
frappe.conf.developer_mode = 1) - Create/edit workspace via Workspace Builder UI
- On save, Frappe auto-exports to the app directory above
- Commit the JSON file to version control
CRITICAL: Ship Dependencies Too
A workspace JSON alone is NOT sufficient. You MUST also ship:
| Component | How to Ship |
|---|---|
| Number Cards | fixtures in hooks.py OR myapp/fixtures/ |
| Dashboard Charts | fixtures in hooks.py OR myapp/fixtures/ |
| Custom HTML Blocks | fixtures in hooks.py OR myapp/fixtures/ |
| Linked Reports | Already shipped via report directory structure |
| Linked Pages | Already shipped via page directory structure |
# hooks.py
fixtures = [
{"dt": "Number Card", "filters": [["module", "=", "My Module"]]},
{"dt": "Dashboard Chart", "filters": [["module", "=", "My Module"]]},
{"dt": "Custom HTML Block", "filters": [["name", "in", ["My Block"]]]},
]
See references/shipping-with-app.md for complete shipping guide.
Common Patterns
Pattern 1: Module Dashboard with KPIs
[Header: "Key Metrics"]
[Number Card: Open Orders (col=3)] [Number Card: Revenue (col=3)]
[Number Card: Pending (col=3)] [Number Card: Overdue (col=3)]
[Spacer]
[Header: "Trends"]
[Chart: Monthly Revenue (col=12)]
[Header: "Quick Access"]
[Shortcut: New Order (col=4)] [Shortcut: Reports (col=4)] [Shortcut: Settings (col=4)]
Pattern 2: Role-Based Workspace
# Sales Manager sees full dashboard; Sales User sees limited view
# Option A: Two separate workspaces with different role restrictions
# Option B: One workspace — use Number Card/Chart permissions to filter
# Option A implementation:
ws_manager = frappe.get_doc({"doctype": "Workspace", "label": "Sales Management", ...})
ws_manager.append("roles", {"role": "Sales Manager"})
ws_user = frappe.get_doc({"doctype": "Workspace", "label": "Sales Overview", ...})
ws_user.append("roles", {"role": "Sales User"})
Pattern 3: Sidebar Hierarchy
# Parent workspace
parent = frappe.get_doc({"doctype": "Workspace", "label": "CRM", "module": "CRM"})
# Child workspaces (nested in sidebar)
child = frappe.get_doc({
"doctype": "Workspace",
"label": "Lead Pipeline",
"module": "CRM",
"parent_page": "CRM" # References parent workspace label
})
Reference Files
| File | Content |
|---|---|
references/workspace-components.md | Number Cards, Dashboard Charts, Shortcuts, Custom Blocks — full API |
references/shipping-with-app.md | JSON format, fixtures, module structure, install hooks |
references/anti-patterns.md | Common workspace mistakes and how to avoid them |