sapui5tutors SAPUI5 • Fiori • SAP BTP Step-by-step tutorials Real project examples Interview Q&A
Practical SAPUI5 • Fiori • SAP BTP tutorials and interview prep

CAPM Custom Actions and Functions: A Practical Guide

When CRUD isn't enough

Standard OData gives you create, read, update, delete. Real business needs verbs: approve this order, calculate the total, send the reminder. In CAP, those are actions and functions.

Functions read (GET, no side effects). Actions change things (POST, side effects allowed). That distinction drives everything.


Unbound actions and functions

Declared at service level, callable without an entity instance:

service OrderService {
  entity Orders as projection on my.Orders;
  function getOrderStats() returns {
    total   : Integer;
    revenue : Decimal;
  };
  action sendReminders();
}

The function computes and returns data; the action does work and returns nothing (or a status).


Bound actions: operating on an entity

service OrderService {
  entity Orders as projection on my.Orders
    actions {
      action approve();
      action cancel(reason : String);
    };
}

Bound actions appear in the URL under the entity instance — POST /Orders(ID)/approve — and in Fiori Elements as toolbar buttons automatically.


Implementing the handler (Node.js)

const cds = require('@sap/cds');

module.exports = cds.service.impl(async function () {
  const { Orders } = this.entities;

  this.on('approve', 'Orders', async (req) => {
    const { ID } = req.params[0];
    await UPDATE(Orders).set({ status: 'Approved' }).where({ ID });
    req.info(`Order ${ID} approved`);
  });
});

Note this.on('approve', 'Orders', ...) — the second argument binds the handler to the entity. req.params[0] carries the key of the bound instance.

Bound actions + Fiori Elements = free UI buttons. The framework wires the action to a toolbar button with zero UI code.


Practical guidelines

  • Prefer bound actions over unbound when the operation belongs to an entity — the semantics and UI integration are better.
  • Keep functions side-effect free — OData clients and caches assume GET is safe.
  • Validate inside the action — check preconditions (status, authorizations) before mutating.
  • Return meaningful results — a status message or the updated entity beats an empty 204 for debugging.

Error handling in actions

Actions change state, so they fail in interesting ways. Use req.error() for business-rule violations (order already approved) and req.reject() for hard failures (remote system unreachable).

Always validate preconditions first: check the entity's current status, verify the user is allowed to perform this specific action, then mutate. An action that half-applies is worse than one that refuses.

For long-running work, return immediately with a "started" status and process asynchronously via messaging. OData clients time out; your action shouldn't.


Verbs make APIs usable

Well-designed actions turn a generic CRUD API into something that speaks the business language. Start with the verbs your users actually say.

Building a tricky action? Describe it in the comments.