#42920·frappe

Desk v2 — REST API v2 everywhere

Author: shariquerikCreated Sep 16, 2026Updated Sep 18, 2026
Labelswayfinder:map

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.py on desk-v2 (mounted at /api/v2, see frappe/api/__init__.py) and every call site in frontend/src and ui/src. frappe-ui is pinned at 1.0.0-beta.63; its useList and useDoc already call /api/v2/document/…, while its call() and createResource prefix a bare method with /api/method/.
  • Measured 2026-09-16 (grep over .ts/.js/.vue): /api/method literal 7 in frontend/src, 5 in ui/src; createResource 18 and 57; call( 44 and 12; createListResource 9 in ui/src; /api/v2 literal 0 and 0. The one v2 consumer is the list rows composable frontend/src/list/useListRows.ts, which imports frappe-ui's useList. Its row count beside it is a v1 frappe.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). Other ui/-only changes go to develop as their own PR. A change to frappe/api/v2.py itself is framework surface. User ruling 2026-09-18: v2 API changes do not merge into develop yet. A region build lands its framework half on desk-v2 with its page half, and opens the develop PR as a draft to be merged when develop is 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/src and ui/src goes through one desk-owned fetch wrapper in ui/ that sends to /api/v2 only, always sends modified on 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 on develop, then the sync map). Judge every shape by the request a script author, and therefore an LLM, would send.
  • The include pattern (fixed 2026-09-17): a read grows named parts with include=<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-modeling for grilling tickets, /research for research, /frappe-app-dev + /code-style for code, /quality-code-review before 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 (data not message, errors array not exc_type); the gaps are docinfo, link titles and the child-meta bundle (document routes return the doc or one meta alone), or_filters and limit=0 on /document/, and frappe-ui call/createResource reading message (use useCall/useList/useDoc); PATCH with modified in the body keeps the timestamp conflict.

  • Grilling: what v2 everywhere means at the four gaps — one desk-owned wrapper in ui/ over frappe-ui's useCall (frappe-ui has no stable 1.0.0, and its author will replace the v2 toolkit); save is PATCH with the whole document and modified always sent; the record read grows include parts (permissions, attachments, assignments, shares, tags, favourites, comments, seen) replacing docinfo, meta grows include=children, the list route accepts or_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 on desk-v2, MERGED 2026-09-18 as e51a57100e): promise functions that return the whole envelope (data plus the keys beside it), own fetch transport because frappe-ui's useCall hides those keys, ApiError from errors[0] with .type, a save without modified is refused, filters/or_filters/include are 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 with count_capped, null on timeout) and the exact count stays on the count route, sent only on click; link search becomes GET /api/v2/doctype/<dt>/search with limit/start, a shim over search_link; no all-rows mode, the invite dialog filters by the typed emails; list settings stay dotted methods on DocType View; both User pickers move to the search route; the wrapper exposes list with include, count and search. 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 grows include=follows; everyone is a reserved share user; ToDo stays the storage but the wire never says allocated_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-v2 2026-09-18 as 3e5e0676d2 (PR #42990; the develop half #42981 is a held draft): the record page loads with one include read (permissions, assignments, shares, tags, favourites, users, link_titles, seen) and one meta read with children; save is PATCH with modified; delete and upload go through the wrapper; two parts beyond the ticket, users (names for the people the parts name) and link_titles; include is a comma string only; meta carries masked_fields; favourites reads the Favourite table 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 a session object; v2 grows GET /api/v2/session (user, roles, lang, timezone, defaults; a Guest gets a Guest body) as the one script-facing route; ui/ reads it through SessionKey/useSession() with a one-shot fetch fallback, never a window global; 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 adds getSession, logout, getTranslations. Build: Task: session and boot region on v2.

  • Grilling: the files region on v2 — detached upload is POST /api/v2/document/File with multipart (part file plus File fields, chunk fields kept, base64 content JSON stays valid); attach is the attachments sub-resource, POST/DELETE /document/<dt>/<name>/attachments[/<file>], returning the refreshed part, which grows creation and owner; file_url is 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>/activity with types, and stream+start for one stream's next page; invitations sit on the User Invitation document routes (list filtered on status and app, cancel/resend on 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, and meta?include=children for the bundle; onboarding stays two dotted calls with app not appName; 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-v2 and held draft #43005 on develop: 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 with users; tags and follows do not. follows is a boolean. The route checks write itself for tags, because the tag functions check nothing, and removes a share through the permission setter, because the share remover checks nothing and cannot address everyone. 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-v2 2026-09-18 as be47bdf8a0 (PR #43002; the develop half #43003 is a held draft stacked on #42981): the list read carries count/count_capped, capped at 1000 by reading one row past it, so exactly 1000 is not capped, and group_by reaches the count as it did on v1; the search route shims search_widget plus build_for_autosuggest rather than search_link, which has no start and whose cache decorator needs the v1 path, and limit below 1 falls back to 10 so no all-rows mode exists. A search filters must be a dict wherever the doctype has a standard query: User broke on list filters. The count query became the plain reportview.count_rows with named arguments, so no request state is swapped and the repo's overusing-args rule stays happy. usePagedList and useLinkSearch in ui/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/method for 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) and or_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.