Desk v2 — REST API v2 everywhere
Child map of Desk v2 — the root map. Chartered 2026-09-16 from the user's ruling, recorded as a program principle on the root map: desk v2 calls REST API v2 and nothing else.
Destination
Every request the desk v2 frontend and @framework/ui send goes to /api/v2. Document reads, lists, counts, meta, saves, deletes, uploads, boot, search and every dotted RPC use the v2 routes. No /api/method/…, /api/resource/… or /api/v1/… URL remains in frontend/src or ui/src, and a check in CI keeps it that way. Where v2 has no route for something the desk needs, the map decides whether v2 grows the route or the desk stops needing it.
Notes
- Domain:
frappe/api/v2.pyondesk-v2(mounted at/api/v2, seefrappe/api/__init__.py) and every call site infrontend/srcandui/src. frappe-ui is pinned at1.0.0-beta.63; itsuseListanduseDocalready call/api/v2/document/…, while itscall()andcreateResourceprefix a bare method with/api/method/. - Measured 2026-09-16 (grep over
.ts/.js/.vue):/api/methodliteral 7 infrontend/src, 5 inui/src;createResource18 and 57;call(44 and 12;createListResource9 inui/src;/api/v2literal 0 and 0. The one v2 consumer is the list rows composablefrontend/src/list/useListRows.ts, which imports frappe-ui'suseList. Its row count beside it is a v1frappe.client.get_count. - What v2 offers today:
/document/<doctype>(GET list, POST create),/document/<doctype>/<name>/(GET, PATCH/PUT, DELETE),/document/<doctype>/<name>/copy,/doctype/<doctype>/meta,/doctype/<doctype>/count,/method/<dotted.method>,/method/<doctype>/<method>,/method/login|logout|ping|upload_file|bulk_delete|bulk_update, and/discovery/…. - Both branches: the desk changes go to
desk-v2, and so does the fetch wrapper@framework/ui/api(user ruling 2026-09-17: it will change a lot while the regions move onto it, so it stays with the desk). Otherui/-only changes go todevelopas their own PR. A change tofrappe/api/v2.pyitself is framework surface. User ruling 2026-09-18: v2 API changes do not merge intodevelopyet. A region build lands its framework half ondesk-v2with its page half, and opens thedevelopPR as a draft to be merged whendevelopis ready to take the v2 surface (first held draft: #42981). - This map carries execution, like the frappe-ui map: each region of call sites is a task ticket that ends in a merged PR, and the final ticket adds the CI check.
- The rule (grilling, 2026-09-17): every request from
frontend/srcandui/srcgoes through one desk-owned fetch wrapper inui/that sends to/api/v2only, always sendsmodifiedon a save, and passes filters through opaque. A v1 function moves to/api/v2/method/<dotted.name>only after its region's grilling finds no better v2 shape. Backend code changes when it makes the framework better: a v2 route grows when the desk needs it (framework half ondevelop, then the sync map). Judge every shape by the request a script author, and therefore an LLM, would send. - The
includepattern (fixed 2026-09-17): a read grows named parts withinclude=<part>,<part>beside its document or rows, each part under its own key. The record read (permissions, attachments, seen, …) and the list read (count) both use it; every later region uses it for anything a read must return beside the data. - Skills:
/grilling+/domain-modelingfor grilling tickets,/researchfor research,/frappe-app-dev+/code-stylefor code,/quality-code-reviewbefore a build ticket closes. - Judge every choice by the request a script author, and therefore an LLM, would send: the root map's UI API principle applies to the wire too.
Decisions so far
Research: every v1 call the desk sends, and its v2 route — every dotted RPC already has a v2 route under
/api/v2/method/and only the envelope changes (datanotmessage,errorsarray notexc_type); the gaps are docinfo, link titles and the child-meta bundle (document routes return the doc or one meta alone),or_filtersandlimit=0on/document/, and frappe-uicall/createResourcereadingmessage(useuseCall/useList/useDoc); PATCH withmodifiedin the body keeps the timestamp conflict.Grilling: what v2 everywhere means at the four gaps — one desk-owned wrapper in
ui/over frappe-ui'suseCall(frappe-ui has no stable 1.0.0, and its author will replace the v2 toolkit); save is PATCH with the whole document andmodifiedalways sent; the record read growsincludeparts (permissions, attachments, assignments, shares, tags, favourites, comments, seen) replacing docinfo, meta growsinclude=children, the list route acceptsor_filters; a dotted method is allowed only after its region's grilling; the map is reshaped into one grilling per region, a wrapper task, a record task and a lint task.Task: desk-owned v2 fetch wrapper in ui/ —
@framework/ui/api(PR #42947 ondesk-v2, MERGED 2026-09-18 ase51a57100e): promise functions that return the whole envelope (dataplus the keys beside it), ownfetchtransport because frappe-ui'suseCallhides those keys,ApiErrorfromerrors[0]with.type, a save withoutmodifiedis refused,filters/or_filters/includeare pass-throughs, upload goes through the chunk loop to/api/v2/method/upload_file; screens own their reactive state.Grilling: the lists and search region on v2 — the list read grows
include=count(capped at a fixed 1000 withcount_capped,nullon timeout) and the exact count stays on the count route, sent only on click; link search becomesGET /api/v2/doctype/<dt>/searchwithlimit/start, a shim oversearch_link; no all-rows mode, the invite dialog filters by the typed emails; list settings stay dotted methods onDocType View; both User pickers move to the search route; the wrapper exposeslistwithinclude,countandsearch. Build: Task: lists and search region on v2.Grilling: the collaboration region on v2 — tag, assign, share, favourite, follow, comment — each write is a document sub-resource named after its read part:
POST/DELETE /api/v2/document/<dt>/<name>/{assignments|shares|tags|favourites|follows|comments}[/<key>], handlers call the existing functions and return the refreshed part; the read growsinclude=follows;everyoneis a reserved share user; ToDo stays the storage but the wire never saysallocated_to(the table rethink is root-map fog). Build: Task: collaboration region on v2.Task: record region on v2 — read with include parts, meta with children, save — MERGED to
desk-v22026-09-18 as3e5e0676d2(PR #42990; the develop half #42981 is a held draft): the record page loads with oneincluderead (permissions, assignments, shares, tags, favourites, users, link_titles, seen) and one meta read withchildren; save is PATCH withmodified; delete and upload go through the wrapper; two parts beyond the ticket,users(names for the people the parts name) andlink_titles;includeis a comma string only; meta carriesmasked_fields;favouritesreads theFavouritetable on this branch.Grilling: the session and boot region on v2 — boot stays one dotted GET (
frappe.shell.boot.get_boot, the desk's startup state) and embeds asessionobject; v2 growsGET /api/v2/session(user, roles, lang, timezone, defaults; a Guest gets a Guest body) as the one script-facing route;ui/reads it throughSessionKey/useSession()with a one-shot fetch fallback, never awindowglobal; translations stay a separate cacheable dotted GET; arrangement stays dotted; logout is the existing v2 route; mark-one-read is a PATCH, mark-all and hide-indicator stay dotted; wrapper addsgetSession,logout,getTranslations. Build: Task: session and boot region on v2.Grilling: the files region on v2 — detached upload is
POST /api/v2/document/Filewith multipart (partfileplus File fields, chunk fields kept, base64contentJSON stays valid); attach is theattachmentssub-resource,POST/DELETE /document/<dt>/<name>/attachments[/<file>], returning the refreshed part, which growscreationandowner;file_urlis the address for bytes, no content route; the v2 handler learns to pass a file response through so the data import template stays a dotted GET; upload limits are boot keys read by injection. Build: Task: files region on v2.Grilling: the remaining v1 functions the desk reuses — activity, invitations, data import, onboarding, addresses — activity is a document sub-resource,
GET /document/<dt>/<name>/activitywithtypes, andstream+startfor one stream's next page; invitations sit on theUser Invitationdocument routes (list filtered on status and app,cancel/resendon the doc-method route) while invite-by-email stays a dotted POST; data import uses the doc-method route for preview and start, a list read for logs, andmeta?include=childrenfor the bundle; onboarding stays two dotted calls withappnotappName; the address table stays a cached dotted GET folded into the session task; link titles and user settings have no caller. Build: Task: the remaining v1 functions on v2.Task: collaboration region on v2 — document sub-resources for assign, share, tag, favourite, follow, comment — BUILT 2026-09-18, PR #43004 on
desk-v2and held draft #43005 ondevelop: the six writes are literal sub-resource rules, not a<part>placeholder, because the greedy name segment made a placeholder break plain document PATCH and DELETE for any name holding a slash; an unknown part is therefore a router 404. A write that names people also answers withusers;tagsandfollowsdo not.followsis a boolean. The route checkswriteitself for tags, because the tag functions check nothing, and removes a share through the permission setter, because the share remover checks nothing and cannot addresseveryone. The desk panel, star, new follow control and the already-migrated delete all go through the wrapper, and the wrapper's own tests now run in the frontend suite.Task: lists and search region on v2 — include=count, search route, pickers, invite dialog — MERGED to
desk-v22026-09-18 asbe47bdf8a0(PR #43002; the develop half #43003 is a held draft stacked on #42981): the list read carriescount/count_capped, capped at 1000 by reading one row past it, so exactly 1000 is not capped, andgroup_byreaches the count as it did on v1; the search route shimssearch_widgetplusbuild_for_autosuggestrather thansearch_link, which has nostartand whose cache decorator needs the v1 path, andlimitbelow 1 falls back to 10 so no all-rows mode exists. A searchfiltersmust be a dict wherever the doctype has a standard query:Userbroke on list filters. The count query became the plainreportview.count_rowswith named arguments, so no request state is swapped and the repo'soverusing-argsrule stays happy.usePagedListanduseLinkSearchinui/src/composables/are the shared shapes every later picker and list should reuse. Lost on purpose: search rows carry no avatar, and the notifications feed no longer caches in local storage.
Not yet specified
- Nothing. Every region is decided and has a build task; the lint task blocks on all of them.
Out of scope
- CRM's own frontend and any app's frontend. This map moves the framework's desk and
@framework/ui; an app that copies a v1 call into its own scripts is its own problem, and the check runs on frappe's tree only. - Desk v1. It stays on
/api/methodfor as long as it exists. - Adding new features to v2 that no desk call site needs.
- Nested filter groups such as
(A AND B) OR (C AND B). The server takes two flat lists today,filters(AND) andor_filters(OR), on v1 and v2 alike; a nested grammar is new server work, the same whichever toolkit the desk uses. The wrapper's opaque filter pass-through keeps call sites unchanged when it arrives. Ruled 2026-09-17 in the grilling; noted as fog on the root map.
Source: frappe/frappe