| name | fiori-draft |
| description | Use when implementing SAP Fiori Draft support in CAP: @odata.draft.enabled, draft lifecycle events (NEW, EDIT, SAVE, CANCEL, DISCARD), draft validation, Fiori draft messages (CAP Aug 2025 GA), lean draft, or draft-enabled entity queries in SAP CAP Node.js or Java projects.
|
| metadata | {"version":"1.1.0","keywords":["@odata.draft.enabled","draft","IsActiveEntity","draftActivate","draftEdit","draftDiscard","draft validation","draft messages","lean draft","_drafts table"],"related":{"fiori-annotations":"UI annotations for draft-enabled entities","service-handlers":"draft lifecycle event handlers","cds-modeling":"entities with draft enabled"}} |
Fiori Draft — CAP Best Practices
Primary reference: https://cap.cloud.sap/docs/node.js/fiori
Fiori Draft overview: https://cap.cloud.sap/docs/guides/fiori#draft-support
Enabling draft
service OrderService {
@odata.draft.enabled
entity Orders as projection on db.Orders;
}
One annotation — CAP auto-generates the _drafts shadow table, all OData draft actions (draftEdit, draftActivate, draftDiscard), and wires up the Fiori Elements UI automatically.
Draft lifecycle events
module.exports = class OrderService extends cds.ApplicationService {
async init() {
const { Orders } = this.entities
this.before('NEW', Orders.drafts, req => {
req.data.status = 'Draft'
})
this.on('EDIT', Orders, async (req) => {
const { ID } = req.params[0]
const order = await SELECT.one(Orders).where({ ID })
if (order?.status === 'Closed') req.reject(409, 'CANNOT_EDIT_CLOSED')
return next()
})
this.before('CREATE', Orders, this.validateOnSave)
this.before('UPDATE', Orders, this.validateOnSave)
this.on('CANCEL', Orders.drafts, req => {
})
this.on('DISCARD', Orders.drafts, req => { })
return super.init()
}
async validateOnSave(req) {
const { title, amount } = req.data
if (!title) req.error({ code: 400, message: 'Title required', target: 'title' })
if (amount <= 0) req.error({ code: 422, message: 'Amount must be positive', target: 'amount' })
req.reject()
}
}
Draft Messages (GA since CAP August 2025)
Persistent Fiori draft messages provide fast inline feedback while the user is editing (on each PATCH, not just on Save):
{
"cds": {
"fiori": {
"draft_messages": true
}
}
}
To run validations on every PATCH (not just on Save):
this.before('PATCH', Orders.drafts, req => {
if (req.data.amount !== undefined && req.data.amount < 0) {
req.error({ code: 422, message: 'Amount must be positive', target: 'amount' })
}
req.reject()
})
Note: Draft Messages require a database schema update. Run cds deploy after enabling.
Not supported on OData V2 UIs or UI5 < 1.135.0.
Querying draft entities
const orders = await SELECT.from(Orders)
const drafts = await SELECT.from(Orders.drafts)
const hasDraft = await SELECT.one(Orders.drafts).where({ ID: id })
Programmatic draft operations (Node.js)
await srv.new(Orders.drafts, { title: 'New Order', amount: 0 })
await srv.edit(Orders, { ID: id })
await srv.save(Orders.drafts, { ID: id })
await srv.discard(Orders.drafts, { ID: id })
Calculated elements in drafts (GA since CAP Node.js 9.x)
entity Orders : cuid, managed {
amount : Decimal(9,2);
taxRate : Decimal(4,2);
grossAmount = amount * (1 + taxRate) : Decimal(9,2); // calculated element
}
Calculated elements are now properly evaluated when reading from the _drafts table — no need for virtual elements + custom code.
Lean Draft (simplified mode)
For simpler use cases without full draft lifecycle:
service OrderService {
@odata.draft.enabled: { mode: 'exclusive' }
entity Orders as projection on db.Orders;
}
Bypass Drafts by Default (CDS 10+)
Since CDS 10, direct access to active entities bypasses draft choreography by default. Non-Fiori clients (e.g. AI agents, REST clients) can now work with active data directly:
GET /odata/v4/Orders(:id)
PATCH /odata/v4/Orders(:id)
DELETE /odata/v4/Orders(:id)
GET /odata/v4/Orders(:id)/DraftAdministrativeData
PATCH /odata/v4/Orders(:id)/DraftAdministrativeData/InProcessByUserDescription
Impact on handlers: Validation must now account for both paths:
this.before('PATCH', Orders, ...)
this.before('DELETE', Orders, ...)
this.on('draft', Orders, ...)
Opt-out (if you need pre-CDS 10 behavior):
// srv/cat-service.js
cds.fiori.bypass_draft = false
draft_new_action (optional): move draft creation to a named action so that plain POST creates active entities:
{
"cds": { "fiori": { "draft_new_action": true } }
}
With this, POST /Books { ... } creates an active entity; use POST /Books/draftNew to create a draft.
Common mistakes to avoid
- ❌ Registering EDIT handler on
Orders.drafts — EDIT must be on the active entity Orders
- ❌ Registering SAVE validation in
before('SAVE', ...) — it doesn't exist; use before('CREATE', Orders, ...) and before('UPDATE', Orders, ...) (these fire on draft activation)
- ❌ Manually merging active + draft results for "All" filter — let CAP do this
- ❌ Forgetting
req.reject() after accumulating req.error() calls — errors won't be thrown
- ❌ Using
LargeBinary fields with draft + hdb driver (causes deadlock) — switch to hana-client
- ❌ Not running schema migration after enabling
draft_messages
- ❌ Using virtual elements for calculated values in drafts — use CDS calculated elements instead (CAP 9+)