Define and implement bound/unbound OData actions (POST) and functions (GET). Pass parameters and return complex types.
CRUD covers entity lifecycle; actions and functions cover business operations — approve an order, calculate a quote, trigger a workflow. They're the OData equivalent of RPC calls and are first-class citizens in CAP.
// srv/orders-service.cds
service OrdersService {
entity Orders as projection on my.Orders;
// Action: modifies state, uses POST
action submitOrder (orderId : UUID) returns Order;
// Function: read-only, uses GET
function getOrderTotal (orderId : UUID) returns Decimal;
function topProducts (limit : Integer default 10) returns array of Product;
}
service OrdersService {
entity Orders as projection on my.Orders {
// bound to Orders entity
action cancel () returns Orders;
action clone (newTitle : String) returns Orders;
function daysUntilDelivery () returns Integer;
}
}
// srv/orders-service.js
this.on('submitOrder', async (req) => {
const { orderId } = req.data // named parameters from CDS
const order = await SELECT.one.from('Orders').where({ ID: orderId })
if (!order) return req.reject(404, `Order ${orderId} not found`)
if (order.status !== 'DRAFT') return req.reject(409, 'Order already submitted')
await UPDATE('Orders').set({ status: 'SUBMITTED', submittedAt: new Date() })
.where({ ID: orderId })
return SELECT.one.from('Orders').where({ ID: orderId }) // return updated order
})
// Bound actions receive the bound entity key in req.params
this.on('cancel', 'Orders', async (req) => {
const [{ ID }] = req.params // key of the bound entity
await UPDATE('Orders').set({ status: 'CANCELLED' }).where({ ID })
return SELECT.one.from('Orders').where({ ID })
})
this.on('getOrderTotal', async (req) => {
const { orderId } = req.data
const items = await SELECT.from('OrderItems').where({ order_ID: orderId })
return items.reduce((sum, i) => sum + i.price * i.quantity, 0)
})
### Unbound action (POST, JSON body with named params)
POST http://localhost:4004/orders/submitOrder
Content-Type: application/json
{ "orderId": "c0f0b8a0-..." }
### Bound action (POST to entity instance)
POST http://localhost:4004/orders/Orders(c0f0b8a0-...)/cancel
### Unbound function (GET with query parameters)
GET http://localhost:4004/orders/getOrderTotal(orderId='c0f0b8a0-...')
### Function returning collection
GET http://localhost:4004/orders/topProducts(limit=5)
type OrderSummary {
ID : UUID;
total : Decimal;
itemCount : Integer;
canCancel : Boolean;
}
service OrdersService {
function getOrderSummary (orderId : UUID) returns OrderSummary;
}
this.on('getOrderSummary', async (req) => {
const { orderId } = req.data
const order = await SELECT.one.from('Orders').where({ ID: orderId })
const items = await SELECT.from('OrderItems').where({ order_ID: orderId })
return {
ID: order.ID,
total: items.reduce((s, i) => s + i.price * i.qty, 0),
itemCount: items.length,
canCancel: order.status === 'SUBMITTED'
}
})
// In a Fiori controller
const oModel = this.getView().getModel()
oModel.callFunction('/submitOrder', {
method: 'POST',
urlParameters: { orderId: sOrderId },
success: (data) => MessageToast.show('Order submitted!'),
error: (err) => MessageBox.error(err.message)
})
| Mistake | Symptom | Fix |
|---|---|---|
| Using GET for an action | 405 Method Not Allowed | Actions always POST |
| Returning plain value from function that declares complex type | Serialization error | Return an object matching the CDS type |
Forgetting req.params for bound actions |
ID is undefined |
Use const [{ ID }] = req.params |
No returns clause in CDS |
CAP infers void return | Add returns <type> for typed responses |
action addToCart (bookId: UUID, quantity: Integer) returns Cart; to your servicereq.user.id, upsert the item, return the updated cartfunction cartTotal () returns Decimal; and implement itreq.paramsthis.on()