Custom Handlers for Entities in SAP CAP

The first time I faced a two-level composition hierarchy with draft-enabled entities in a CAP project, I stared at my screen wondering which custom handlers I actually needed to implement. The Fiori Elements UI works beautifully out of the box—users can create, edit, save, and discard changes without any custom code. But the moment you need business logic, validations, or calculations, you realize you’re dancing with an intricate system of events that needs careful choreography. Nowadays, while shifting from one project to another or after sometime without ‘coding’. I’m wondering which  is the correct one that I have to use. For that reason I created this blog post to helps me surf between the events and their lifecycle in draft entities.

In this post, I’ll walk through each Fiori Elements action and explain exactly what custom handlers you need to implement for a draft-enabled entity with nested compositions. My mind loves finding patterns, and CAP’s draft system is essentially a beautiful pattern that becomes clearer once you understand the rhythm.

Understanding the Scenario

Let’s establish our data model first. We have:

Entity A: Draft-enabled parent entityEntity B: Composition of Entity A (automatically draft-enabled)

Here’s the critical insight: when a parent entity is draft-enabled, all compositions in the hierarchy automatically inherit draft behavior. There’s no such thing as mixing draft and non-draft entities in a composition tree—CAP handles the entire hierarchy as one cohesive draft unit.

This means when a user edits Entity A, CAP creates draft copies of all related Entity B records. When they activate (save) the draft, all changes across the hierarchy are committed atomically. This transactional behavior is what makes the draft system powerful, but it also means your custom handlers need to respect this hierarchy.

Entity A (draft-enabled)
└── Entity B[] (composition, inherits draft)

The Fiori Elements Action Lifecycle

When users interact with a Fiori Elements application, their actions trigger specific CAP events. Let’s break down each action, the events it triggers, and the custom handlers you need.

Quick Reference Matrix

User ActionCAP EventTarget EntityEntities AffectedWhen To useCreate (New)NEWEntityA.drafts, EntityB.draftsEntityA draft; EntityB when adding a child rowSet defaults, initialize structureEdit (Existing)EDITEntityAAll entities (A, B) – active → draft copyValidate edit allowed, enrich draft contextUpdate (Modify fields)PATCHEntityA.drafts, EntityB.draftsSpecific draft entity being changedField validation, calculations, cascade updatesSave/Activate

SAVE then 

CREATE/UPDATE

EntityA.drafts  EntityAAll entities (draft → active)Critical: Validate all entities, business logicDeleteDELETE

EntityA or EntityA.drafts; 

EntityB.drafts for individual child rows

All entities via cascade (root delete); specific child rowPrevent deletion, cleanup resourcesCancel/DiscardDISCARDEntityA.draftsAll draft entities deletedCleanup temp resources

Important Notes:

SAVE is special: It triggers BOTH a SAVE event on drafts AND either CREATE (for new entities) or UPDATE (for edited entities) on the active entity. You need handlers for all three.Compositions inherit draft: When EntityA is draft-enabled, EntityB automatically becomes draft-enabled. No way to mix draft/non-draft in a composition tree.Deep operations: CAP handles the entire hierarchy automatically during EDIT and SAVE. Trust it unless you have specific needs.Use .drafts qualifier: Always distinguish between EntityA (active) and EntityA.drafts (draft) in your handlers.

Action 1: NEW (Creating a New Draft)

User Action: Clicks the “Create” button in the list report, or adds a new row to a composition child tableCAP Event: NEW on EntityA.drafts (new parent draft) or NEW on EntityB.drafts (new child row added during editing)Entities Affected: EntityA draft when creating; EntityB draft when the user adds a row to the composition tableWhen You Need a Handler: To set default values, initialize related entities, or prepare the initial structure

import { Request } from @sap/cds;

// Fires when the user clicks “Create” in the list report
srv.before(NEW, EntityA.drafts, async (req: Request) => {
// Set default values for the new draft
req.data.status = NEW;
req.data.createdBy = req.user.id;
});

srv.after(NEW, EntityA.drafts, async (data: EntityA, req: Request) => {
// Enrich the created draft with calculated fields
// or fetch additional data to display
const { ID } = data;

// Example: Calculate some initial values based on user context
await UPDATE(EntityA.drafts)
.set({ calculatedField: someCalculation() })
.where({ ID });
});

// Fires when the user adds a new row to the EntityB composition table
// This is independent from the parent NEW — it fires on the child entity directly
srv.before(NEW, EntityB.drafts, async (req: Request) => {
// Set defaults for the new child row
req.data.position = await getNextPosition(req.data.parent_ID);
req.data.lineStatus = OPEN;
});

srv.after(NEW, EntityB.drafts, async (data: EntityB, req: Request) => {
// Post-process the newly created child row
const { ID } = data;
await UPDATE(EntityB.drafts)
.set({ computedField: computeDefault(data) })
.where({ ID });
});

Action 2: EDIT (Editing an Active Entity)

User Action: Clicks the “Edit” button on an existing recordCAP Event: EDIT action on EntityA, which internally triggers draft creationEntities Affected: All entities in the hierarchy (active → draft copy)When You Need a Handler: To customize the draft preparation, load additional context, or handle special composition copying logic

srv.before(EDIT, EntityA, async (req: Request) => {
// Fetch the active entity before it’s copied to draft
const { ID } = req.data;
const entity = await SELECT.one.from(EntityA).where({ ID });

// You can perform validations before allowing edit
if (entity.locked) {
req.error(423, This entity is locked and cannot be edited);
}
});

srv.after(EDIT, EntityA, async (data: EntityA, req: Request) => {
// The draft has been created with deep copy of all compositions
// You can now enrich the draft with additional data
const { ID } = data;

// Example: Load some context data into the draft
await UPDATE(EntityA.drafts)
.set({ editContext: User edited at + new Date().toISOString() })
.where({ ID });

// CAP automatically handles the deep copy of EntityB
// You typically don’t need to manually copy compositions
});

Action 3: PATCH (Modifying a Draft)

User Action: Changes field values while editing (each field change triggers this)CAP Event: PATCH on EntityA.drafts or EntityB.draftsEntities Affected: The specific draft entity being modifiedWhen You Need a Handler: For field-level validation, calculated fields, or cascading updates

srv.before(PATCH, EntityA.drafts, async (req: Request) => {
// Validate the changes before they’re applied
const { amount, currency } = req.data;

if (amount && amount < 0) {
req.error(400, Amount must be positive);
}

// Perform calculations based on changed fields
if (req.data.quantity && req.data.pricePerUnit) {
req.data.totalPrice = req.data.quantity * req.data.pricePerUnit;
}
});

srv.after(PATCH, EntityA.drafts, async (data: EntityA, req: Request) => {
// Cascade updates to child entities if needed
const { ID } = data;

// Example: When parent changes, update all children
if (req.data.status) {
await UPDATE(EntityB.drafts)
.set({ parentStatus: req.data.status })
.where({ parent_ID: ID });
}
});

// Handle updates on child entities
srv.before(PATCH, EntityB.drafts, async (req: Request) => {
// Validate child entity changes
// These fire independently when users edit composition tables
if (req.data.invalidField) {
req.error(400, Invalid field value);
}
});

Action 4: SAVE/Activate (Persisting Draft Changes)

User Action: Clicks the “Save” buttonCAP Event: SAVE on EntityA.drafts, which triggers either CREATE or UPDATE on the active EntityAEntities Affected: All entities in the hierarchy (draft → active)When You Need a Handler: This is where most business logic lives—validation, computation, and composition management

// IMPORTANT: The SAVE handler runs on the draft entity
srv.before(SAVE, EntityA.drafts, async (req: Request) => {
// This is your last chance to validate before activation
const draft = await SELECT.one
.from(EntityA.drafts)
.where({ ID: req.data.ID })
.columns([ID, requiredField, status]);

if (!draft.requiredField) {
req.error(400, Required field is missing);
}

// Validate all child entities in the composition
const childrenB: EntityB[] = await SELECT.from(EntityB.drafts)
.where({ parent_ID: draft.ID });

for (const childB of childrenB) {
if (!childB.mandatoryField) {
req.error(400, `EntityB ${childB.ID} is missing mandatory field`);
}
}
});

// Handle new entity creation (when activating a draft created via “Create”)
srv.on(CREATE, EntityA, async (req: Request, next: Function) => {
// The draft is being activated and will become a new active entity
// CAP handles the deep insert of all compositions automatically

// But you might need to customize the creation
const result = await next(); // Let CAP do the default deep insert

// Post-creation logic
await postProcessNewEntity(result.ID);

return result;
});

// Handle entity update (when activating a draft created via “Edit”)
srv.on(UPDATE, EntityA, async (req: Request, next: Function) => {
// The draft changes are being applied to the active entity
// CAP handles the deep update of all compositions

// You can intercept to add custom logic
const { ID } = req.data;

// CAP’s deep update uses a “full set” approach:
// – Existing child records not in the draft are deleted
// – New child records in the draft are created
// – Modified child records are updated

const result = await next(); // Let CAP handle the deep update

// Post-update logic
await notifyRelatedSystems(ID);

return result;
});

srv.after(SAVE, EntityA.drafts, async (data: EntityA, req: Request) => {
// The draft has been successfully activated
// This fires AFTER the CREATE or UPDATE completes

// Good place for notifications, logging, or async processes
await sendNotification({
message: Entity A has been saved,
entityId: data.ID
});
});

Action 5: DELETE (Removing an Entity)

User Action: Deletes a draft or active recordCAP Event: DELETE on EntityA or EntityA.draftsEntities Affected: All entities in the hierarchy via cascade deletionWhen You Need a Handler: To prevent deletion under certain conditions, clean up related data, or handle cascade manually

srv.before(DELETE, EntityA, async (req: Request) => {
// Validate deletion is allowed
const { ID } = req.data;
const entity = await SELECT.one.from(EntityA).where({ ID });

if (entity.status === LOCKED) {
req.error(403, Cannot delete locked entities);
}

// CAP will automatically cascade delete to EntityB
// due to the composition relationship

// But you might want to clean up related data outside the composition
await deleteRelatedResources(ID);
});

srv.after(DELETE, EntityA, async (data: EntityA, req: Request) => {
// Entity and all compositions have been deleted
// Clean up, log, or notify
await logDeletion(data.ID, req.user.id);
});

// Deleting the entire draft (when user deletes the whole unsaved record)
srv.before(DELETE, EntityA.drafts, async (req: Request) => {
// Usually no custom logic needed here
// CAP handles cascade deletion of draft compositions
// But you might want to log or clean up
});

// Fires when the user removes an individual row from the EntityB composition table
// This is different from deleting the whole parent draft — it targets a specific child row
srv.before(DELETE, EntityB.drafts, async (req: Request) => {
const { ID } = req.data;
const item = await SELECT.one.from(EntityB.drafts).where({ ID });

if (item.protected) {
req.error(403, `Item ${item.ID} cannot be removed`);
}
});

srv.after(DELETE, EntityB.drafts, async (data: EntityB, req: Request) => {
// Child row has been removed from the draft composition
// You might want to recalculate totals or reorder positions on the parent draft
await recalculateTotals(data.parent_ID);
});

Action 6: CANCEL (Discarding Draft Changes)

User Action: Clicks the “Cancel” or “Discard Draft” buttonCAP Event: Draft discard actionEntities Affected: All draft entities in the hierarchy are deletedWhen You Need a Handler: To clean up temporary data or resources created during draft editing

srv.before(DISCARD, EntityA.drafts, async (req: Request) => {
// Clean up any temporary resources or cached data
const { ID } = req.data;

// Example: Delete temporary files uploaded to the draft
await cleanupTemporaryFiles(ID);

// CAP will automatically delete all draft entities in the composition
});

srv.after(DISCARD, EntityA.drafts, async (data: EntityA, req: Request) => {
// Draft has been discarded
// Log the discard action if needed
await logDraftDiscard(data.ID, req.user.id);
});

Key Patterns & Best Practices

After working with draft-enabled compositions, here are the patterns I’ve found most useful:

1. Use the .drafts qualifier consistently: Always be explicit about whether you’re handling drafts or active entities. EntityA.drafts vs EntityA are different entities with different behavior.

2. Handle both CREATE and UPDATE during SAVE: When a draft is activated, it triggers either CREATE (new entity) or UPDATE (edited entity). You need handlers for both scenarios—they’re not interchangeable.

3. Trust CAP’s deep operations: CAP automatically handles deep INSERT and UPDATE for your entire composition hierarchy. Don’t try to manually manage child entities during activation unless you have specific requirements.

4. Validate in before(‘SAVE’): The SAVE event is your last checkpoint before draft activation. Put comprehensive validation here, including validation of child and grandchild entities.

5. Manage transactions carefully: All operations during draft activation are transactional. If you throw an error or call req.error(), the entire activation (including all compositions) will roll back.

6. Use after handlers for side effects: Send notifications, update external systems, or trigger async processes in after handlers. These run only after successful completion, ensuring consistency.

 

Rules for AI Code Assistants (CAP Draft)

Use this a template for your rules.

# SAP CAP Draft Handler Events

## Draft root entity (EntityA.drafts)
– Create new draft → NEW (not CREATE)
– Edit field in draft → PATCH (not UPDATE)
– Save/activate draft → SAVE → then CREATE (new) or UPDATE (edit) on active entity
– Edit existing → EDIT on active entity (not drafts)
– Discard draft → DISCARD (not discard)
– Delete → DELETE on EntityA or EntityA.drafts

## Non-draft entities — standard CRUD only
– CREATE / READ / UPDATE / DELETE on the entity directly
– No .drafts qualifier, no NEW/PATCH/SAVE/EDIT/DISCARD

## Key rules
– Always use .drafts qualifier to distinguish draft from active entity
– SAVE handlers go on EntityA.drafts; activate handlers on active EntityA
– before(‘SAVE’) is the last validation checkpoint before activation
– after() handlers are for side effects (notifications, external calls)
– All activation operations are transactional; req.error() rolls back everything

In case that  the files are very big think to add as an external reference as: 

## To choose event and target entity for a Custom Handler for entity check `<relative_project_root>/<code_assistant>/docs/cap_handlers.md`

This documentation is based in amazing job in https://cap.cloud.sap/docs/node.js/fiori

 

​ The first time I faced a two-level composition hierarchy with draft-enabled entities in a CAP project, I stared at my screen wondering which custom handlers I actually needed to implement. The Fiori Elements UI works beautifully out of the box—users can create, edit, save, and discard changes without any custom code. But the moment you need business logic, validations, or calculations, you realize you’re dancing with an intricate system of events that needs careful choreography. Nowadays, while shifting from one project to another or after sometime without ‘coding’. I’m wondering which  is the correct one that I have to use. For that reason I created this blog post to helps me surf between the events and their lifecycle in draft entities.In this post, I’ll walk through each Fiori Elements action and explain exactly what custom handlers you need to implement for a draft-enabled entity with nested compositions. My mind loves finding patterns, and CAP’s draft system is essentially a beautiful pattern that becomes clearer once you understand the rhythm.Understanding the ScenarioLet’s establish our data model first. We have:Entity A: Draft-enabled parent entityEntity B: Composition of Entity A (automatically draft-enabled)Here’s the critical insight: when a parent entity is draft-enabled, all compositions in the hierarchy automatically inherit draft behavior. There’s no such thing as mixing draft and non-draft entities in a composition tree—CAP handles the entire hierarchy as one cohesive draft unit.This means when a user edits Entity A, CAP creates draft copies of all related Entity B records. When they activate (save) the draft, all changes across the hierarchy are committed atomically. This transactional behavior is what makes the draft system powerful, but it also means your custom handlers need to respect this hierarchy.Entity A (draft-enabled)
└── Entity B[] (composition, inherits draft)The Fiori Elements Action LifecycleWhen users interact with a Fiori Elements application, their actions trigger specific CAP events. Let’s break down each action, the events it triggers, and the custom handlers you need.Quick Reference MatrixUser ActionCAP EventTarget EntityEntities AffectedWhen To useCreate (New)NEWEntityA.drafts, EntityB.draftsEntityA draft; EntityB when adding a child rowSet defaults, initialize structureEdit (Existing)EDITEntityAAll entities (A, B) – active → draft copyValidate edit allowed, enrich draft contextUpdate (Modify fields)PATCHEntityA.drafts, EntityB.draftsSpecific draft entity being changedField validation, calculations, cascade updatesSave/ActivateSAVE then CREATE/UPDATEEntityA.drafts → EntityAAll entities (draft → active)Critical: Validate all entities, business logicDeleteDELETEEntityA or EntityA.drafts; EntityB.drafts for individual child rowsAll entities via cascade (root delete); specific child rowPrevent deletion, cleanup resourcesCancel/DiscardDISCARDEntityA.draftsAll draft entities deletedCleanup temp resourcesImportant Notes:SAVE is special: It triggers BOTH a SAVE event on drafts AND either CREATE (for new entities) or UPDATE (for edited entities) on the active entity. You need handlers for all three.Compositions inherit draft: When EntityA is draft-enabled, EntityB automatically becomes draft-enabled. No way to mix draft/non-draft in a composition tree.Deep operations: CAP handles the entire hierarchy automatically during EDIT and SAVE. Trust it unless you have specific needs.Use .drafts qualifier: Always distinguish between EntityA (active) and EntityA.drafts (draft) in your handlers.Action 1: NEW (Creating a New Draft)User Action: Clicks the “Create” button in the list report, or adds a new row to a composition child tableCAP Event: NEW on EntityA.drafts (new parent draft) or NEW on EntityB.drafts (new child row added during editing)Entities Affected: EntityA draft when creating; EntityB draft when the user adds a row to the composition tableWhen You Need a Handler: To set default values, initialize related entities, or prepare the initial structureimport { Request } from ‘@sap/cds’;

// Fires when the user clicks “Create” in the list report
srv.before(‘NEW’, ‘EntityA.drafts’, async (req: Request) => {
// Set default values for the new draft
req.data.status = ‘NEW’;
req.data.createdBy = req.user.id;
});

srv.after(‘NEW’, ‘EntityA.drafts’, async (data: EntityA, req: Request) => {
// Enrich the created draft with calculated fields
// or fetch additional data to display
const { ID } = data;

// Example: Calculate some initial values based on user context
await UPDATE(‘EntityA.drafts’)
.set({ calculatedField: someCalculation() })
.where({ ID });
});

// Fires when the user adds a new row to the EntityB composition table
// This is independent from the parent NEW — it fires on the child entity directly
srv.before(‘NEW’, ‘EntityB.drafts’, async (req: Request) => {
// Set defaults for the new child row
req.data.position = await getNextPosition(req.data.parent_ID);
req.data.lineStatus = ‘OPEN’;
});

srv.after(‘NEW’, ‘EntityB.drafts’, async (data: EntityB, req: Request) => {
// Post-process the newly created child row
const { ID } = data;
await UPDATE(‘EntityB.drafts’)
.set({ computedField: computeDefault(data) })
.where({ ID });
});
Action 2: EDIT (Editing an Active Entity)User Action: Clicks the “Edit” button on an existing recordCAP Event: EDIT action on EntityA, which internally triggers draft creationEntities Affected: All entities in the hierarchy (active → draft copy)When You Need a Handler: To customize the draft preparation, load additional context, or handle special composition copying logicsrv.before(‘EDIT’, ‘EntityA’, async (req: Request) => {
// Fetch the active entity before it’s copied to draft
const { ID } = req.data;
const entity = await SELECT.one.from(‘EntityA’).where({ ID });

// You can perform validations before allowing edit
if (entity.locked) {
req.error(423, ‘This entity is locked and cannot be edited’);
}
});

srv.after(‘EDIT’, ‘EntityA’, async (data: EntityA, req: Request) => {
// The draft has been created with deep copy of all compositions
// You can now enrich the draft with additional data
const { ID } = data;

// Example: Load some context data into the draft
await UPDATE(‘EntityA.drafts’)
.set({ editContext: ‘User edited at ‘ + new Date().toISOString() })
.where({ ID });

// CAP automatically handles the deep copy of EntityB
// You typically don’t need to manually copy compositions
});
Action 3: PATCH (Modifying a Draft)User Action: Changes field values while editing (each field change triggers this)CAP Event: PATCH on EntityA.drafts or EntityB.draftsEntities Affected: The specific draft entity being modifiedWhen You Need a Handler: For field-level validation, calculated fields, or cascading updatessrv.before(‘PATCH’, ‘EntityA.drafts’, async (req: Request) => {
// Validate the changes before they’re applied
const { amount, currency } = req.data;

if (amount && amount < 0) {
req.error(400, ‘Amount must be positive’);
}

// Perform calculations based on changed fields
if (req.data.quantity && req.data.pricePerUnit) {
req.data.totalPrice = req.data.quantity * req.data.pricePerUnit;
}
});

srv.after(‘PATCH’, ‘EntityA.drafts’, async (data: EntityA, req: Request) => {
// Cascade updates to child entities if needed
const { ID } = data;

// Example: When parent changes, update all children
if (req.data.status) {
await UPDATE(‘EntityB.drafts’)
.set({ parentStatus: req.data.status })
.where({ parent_ID: ID });
}
});

// Handle updates on child entities
srv.before(‘PATCH’, ‘EntityB.drafts’, async (req: Request) => {
// Validate child entity changes
// These fire independently when users edit composition tables
if (req.data.invalidField) {
req.error(400, ‘Invalid field value’);
}
});
Action 4: SAVE/Activate (Persisting Draft Changes)User Action: Clicks the “Save” buttonCAP Event: SAVE on EntityA.drafts, which triggers either CREATE or UPDATE on the active EntityAEntities Affected: All entities in the hierarchy (draft → active)When You Need a Handler: This is where most business logic lives—validation, computation, and composition management// IMPORTANT: The SAVE handler runs on the draft entity
srv.before(‘SAVE’, ‘EntityA.drafts’, async (req: Request) => {
// This is your last chance to validate before activation
const draft = await SELECT.one
.from(‘EntityA.drafts’)
.where({ ID: req.data.ID })
.columns([‘ID’, ‘requiredField’, ‘status’]);

if (!draft.requiredField) {
req.error(400, ‘Required field is missing’);
}

// Validate all child entities in the composition
const childrenB: EntityB[] = await SELECT.from(‘EntityB.drafts’)
.where({ parent_ID: draft.ID });

for (const childB of childrenB) {
if (!childB.mandatoryField) {
req.error(400, `EntityB ${childB.ID} is missing mandatory field`);
}
}
});

// Handle new entity creation (when activating a draft created via “Create”)
srv.on(‘CREATE’, ‘EntityA’, async (req: Request, next: Function) => {
// The draft is being activated and will become a new active entity
// CAP handles the deep insert of all compositions automatically

// But you might need to customize the creation
const result = await next(); // Let CAP do the default deep insert

// Post-creation logic
await postProcessNewEntity(result.ID);

return result;
});

// Handle entity update (when activating a draft created via “Edit”)
srv.on(‘UPDATE’, ‘EntityA’, async (req: Request, next: Function) => {
// The draft changes are being applied to the active entity
// CAP handles the deep update of all compositions

// You can intercept to add custom logic
const { ID } = req.data;

// CAP’s deep update uses a “full set” approach:
// – Existing child records not in the draft are deleted
// – New child records in the draft are created
// – Modified child records are updated

const result = await next(); // Let CAP handle the deep update

// Post-update logic
await notifyRelatedSystems(ID);

return result;
});

srv.after(‘SAVE’, ‘EntityA.drafts’, async (data: EntityA, req: Request) => {
// The draft has been successfully activated
// This fires AFTER the CREATE or UPDATE completes

// Good place for notifications, logging, or async processes
await sendNotification({
message: ‘Entity A has been saved’,
entityId: data.ID
});
});
Action 5: DELETE (Removing an Entity)User Action: Deletes a draft or active recordCAP Event: DELETE on EntityA or EntityA.draftsEntities Affected: All entities in the hierarchy via cascade deletionWhen You Need a Handler: To prevent deletion under certain conditions, clean up related data, or handle cascade manuallysrv.before(‘DELETE’, ‘EntityA’, async (req: Request) => {
// Validate deletion is allowed
const { ID } = req.data;
const entity = await SELECT.one.from(‘EntityA’).where({ ID });

if (entity.status === ‘LOCKED’) {
req.error(403, ‘Cannot delete locked entities’);
}

// CAP will automatically cascade delete to EntityB
// due to the composition relationship

// But you might want to clean up related data outside the composition
await deleteRelatedResources(ID);
});

srv.after(‘DELETE’, ‘EntityA’, async (data: EntityA, req: Request) => {
// Entity and all compositions have been deleted
// Clean up, log, or notify
await logDeletion(data.ID, req.user.id);
});

// Deleting the entire draft (when user deletes the whole unsaved record)
srv.before(‘DELETE’, ‘EntityA.drafts’, async (req: Request) => {
// Usually no custom logic needed here
// CAP handles cascade deletion of draft compositions
// But you might want to log or clean up
});

// Fires when the user removes an individual row from the EntityB composition table
// This is different from deleting the whole parent draft — it targets a specific child row
srv.before(‘DELETE’, ‘EntityB.drafts’, async (req: Request) => {
const { ID } = req.data;
const item = await SELECT.one.from(‘EntityB.drafts’).where({ ID });

if (item.protected) {
req.error(403, `Item ${item.ID} cannot be removed`);
}
});

srv.after(‘DELETE’, ‘EntityB.drafts’, async (data: EntityB, req: Request) => {
// Child row has been removed from the draft composition
// You might want to recalculate totals or reorder positions on the parent draft
await recalculateTotals(data.parent_ID);
});
Action 6: CANCEL (Discarding Draft Changes)User Action: Clicks the “Cancel” or “Discard Draft” buttonCAP Event: Draft discard actionEntities Affected: All draft entities in the hierarchy are deletedWhen You Need a Handler: To clean up temporary data or resources created during draft editingsrv.before(‘DISCARD’, ‘EntityA.drafts’, async (req: Request) => {
// Clean up any temporary resources or cached data
const { ID } = req.data;

// Example: Delete temporary files uploaded to the draft
await cleanupTemporaryFiles(ID);

// CAP will automatically delete all draft entities in the composition
});

srv.after(‘DISCARD’, ‘EntityA.drafts’, async (data: EntityA, req: Request) => {
// Draft has been discarded
// Log the discard action if needed
await logDraftDiscard(data.ID, req.user.id);
});
Key Patterns & Best PracticesAfter working with draft-enabled compositions, here are the patterns I’ve found most useful:1. Use the .drafts qualifier consistently: Always be explicit about whether you’re handling drafts or active entities. EntityA.drafts vs EntityA are different entities with different behavior.2. Handle both CREATE and UPDATE during SAVE: When a draft is activated, it triggers either CREATE (new entity) or UPDATE (edited entity). You need handlers for both scenarios—they’re not interchangeable.3. Trust CAP’s deep operations: CAP automatically handles deep INSERT and UPDATE for your entire composition hierarchy. Don’t try to manually manage child entities during activation unless you have specific requirements.4. Validate in before(‘SAVE’): The SAVE event is your last checkpoint before draft activation. Put comprehensive validation here, including validation of child and grandchild entities.5. Manage transactions carefully: All operations during draft activation are transactional. If you throw an error or call req.error(), the entire activation (including all compositions) will roll back.6. Use after handlers for side effects: Send notifications, update external systems, or trigger async processes in after handlers. These run only after successful completion, ensuring consistency. Rules for AI Code Assistants (CAP Draft)Use this a template for your rules.# SAP CAP Draft Handler Events

## Draft root entity (EntityA.drafts)
– Create new draft → NEW (not CREATE)
– Edit field in draft → PATCH (not UPDATE)
– Save/activate draft → SAVE → then CREATE (new) or UPDATE (edit) on active entity
– Edit existing → EDIT on active entity (not drafts)
– Discard draft → DISCARD (not discard)
– Delete → DELETE on EntityA or EntityA.drafts

## Non-draft entities — standard CRUD only
– CREATE / READ / UPDATE / DELETE on the entity directly
– No .drafts qualifier, no NEW/PATCH/SAVE/EDIT/DISCARD

## Key rules
– Always use .drafts qualifier to distinguish draft from active entity
– SAVE handlers go on EntityA.drafts; activate handlers on active EntityA
– before(‘SAVE’) is the last validation checkpoint before activation
– after() handlers are for side effects (notifications, external calls)
– All activation operations are transactional; req.error() rolls back everythingIn case that  the files are very big think to add as an external reference as: ## To choose event and target entity for a Custom Handler for entity check `<relative_project_root>/<code_assistant>/docs/cap_handlers.md`This documentation is based in amazing job in https://cap.cloud.sap/docs/node.js/fiori   Read More Technology Blog Posts by Members articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author