Interceptors
Use designer actions and descriptor metadata for standard low-code behavior first. Interceptors are an advanced extension point for adding JavaScript lifecycle logic when the generated CRUD flow needs validation, transformation, or replacement behavior.
Interceptors allow you to run custom JavaScript code before, after, or instead of Create, Update, and Delete operations on dynamic entities.
Interceptor Types
| Command | Type | When Executed |
|---|---|---|
Create |
Pre |
Before entity creation — validation, default values |
Create |
Post |
After entity creation — notifications, related data |
Create |
Replace |
Instead of entity creation — must return the new entity's Id (see below) |
Update |
Pre |
Before entity update — validation, authorization |
Update |
Post |
After entity update — sync, notifications |
Update |
Replace |
Instead of entity update — no return value needed |
Delete |
Pre |
Before entity deletion — dependency checks |
Delete |
Post |
After entity deletion — cleanup |
Delete |
Replace |
Instead of entity deletion — no return value needed |
Defining Interceptors in JSON Descriptors
The designer stores entity interceptors in the entity interceptors array:
{
"name": "LowCodeDemo.Customers.Customer",
"interceptors": [
{
"commandName": "Create",
"type": "Pre",
"javascript": "if (!args.getValue('Name')) {\n globalError = 'Name is required.';\n}"
}
]
}
Interceptor Descriptor
| Field | Type | Description |
|---|---|---|
commandName |
string | "Create", "Update", or "Delete" |
type |
string | "Pre", "Post", or "Replace" |
javascript |
string | JavaScript code to execute |
Defining Interceptors with Attributes
Use the [DynamicEntityCommandInterceptor] attribute on a C# class:
[DynamicEntity]
[DynamicEntityCommandInterceptor(
"Create",
InterceptorType.Pre,
"if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }"
)]
[DynamicEntityCommandInterceptor(
"Create",
InterceptorType.Post,
"context.log('Entity created: ' + context.commandArgs.entityId);"
)]
public class Organization
{
public string Name { get; set; }
}
The Name parameter must be one of: "Create", "Update", or "Delete". The InterceptorType can be Pre, Post, or Replace. When Replace is used, the default database operation is completely skipped and only your JavaScript handler executes. Multiple interceptors can be added to the same class (AllowMultiple = true).
Defining Interceptors with Fluent API
Use the Interceptors list on an EntityDescriptor to add interceptors programmatically in startup configuration:
AbpDynamicEntityConfig.EntityConfigurations.Configure(
"MyApp.Organizations.Organization",
entity =>
{
entity.Interceptors.Add(new CommandInterceptorDescriptor("Create")
{
Type = InterceptorType.Pre,
Javascript = "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }"
});
entity.Interceptors.Add(new CommandInterceptorDescriptor("Delete")
{
Type = InterceptorType.Post,
Javascript = "context.log('Deleted: ' + context.commandArgs.entityId);"
});
}
);
See Attributes & Fluent API for more details on Fluent API configuration.
JavaScript Context
Inside interceptor scripts, you have access to:
context.commandArgs
| Property / Method | Type | Description |
|---|---|---|
data |
object | Entity data dictionary (for Create/Update) |
entityId |
string | Entity ID (for Update/Delete) |
commandName |
string | Command name ("Create", "Update", or "Delete") |
entityName |
string | Full entity name |
getValue(name) |
function | Get a property value |
setValue(name, value) |
function | Set a property value (Pre-interceptors only) |
hasValue(name) |
function | Check if a property exists in the data |
removeValue(name) |
function | Remove a property from the data |
context.currentUser
| Property / Method | Type | Description |
|---|---|---|
isAuthenticated |
bool | Whether user is logged in |
id |
string | User ID |
userName |
string | Username |
email |
string | Email address |
name |
string | First name |
surName |
string | Last name |
phoneNumber |
string | Phone number |
phoneNumberVerified |
bool | Whether phone is verified |
emailVerified |
bool | Whether email is verified |
tenantId |
string | Tenant ID (for multi-tenant apps) |
roles |
string[] | User's role names |
isInRole(roleName) |
function | Check if user has a specific role |
context.emailSender
| Property / Method | Description |
|---|---|
isAvailable |
Whether the email sender is configured and available |
sendAsync(to, subject, body) |
Send a plain-text email |
sendAsync(from, to, subject, body) |
Send a plain-text email with an explicit sender |
sendHtmlAsync(to, subject, htmlBody) |
Send an HTML email |
sendHtmlAsync(from, to, subject, htmlBody) |
Send an HTML email with an explicit sender |
queueAsync(to, subject, body) |
Queue a plain-text email |
queueAsync(from, to, subject, body) |
Queue a plain-text email with an explicit sender |
queueHtmlAsync(to, subject, htmlBody) |
Queue an HTML email |
queueHtmlAsync(from, to, subject, htmlBody) |
Queue an HTML email with an explicit sender |
Logging
| Method | Description |
|---|---|
context.log(message) |
Log an informational message |
context.logWarning(message) |
Log a warning message |
context.logError(message) |
Log an error message |
Use these methods instead of
console.log(which is blocked in the sandbox).
db (Database API)
Full access to the Scripting API for querying and mutating data.
Interceptors can also use common scripting services such as user, tenant, auth, settings, features, config, http, events, jobs, blob, encryption, textTemplating, files, images, and attachments when they are enabled by the scripting capability profile.
globalError
Set this variable to a string to abort the operation and return an error:
globalError = 'Cannot delete this entity!';

globalError is answered like userFriendlyError(message). To choose the kind of error, call one of the error helpers instead. They abort the operation immediately, and ABP exception handling decides the HTTP status and the error response:
// 400 with a validation error for the Quantity field
if (context.commandArgs.getValue('Quantity') < 0) {
validationError('Quantity cannot be negative.', [{ message: 'Quantity cannot be negative.', members: ['Quantity'] }]);
}
// 403 with a coded business error; the caller sees the code's localized text
if (!context.currentUser.isInRole('admin')) {
businessError('Only administrators can close orders.', 'Acme.Orders:CloseRequiresAdmin');
}
Any other failure, such as an uncaught throw new Error(...), aborts the operation with the LowCode:ScriptExecutionFailed error and HTTP status 500, without the script's message. See Runtime Failures.
Page Interceptors and Execution Order
A page can define its own Create, Update, and Delete interceptors in its interceptors array. They run when the operation comes through that page. Each page interceptor can set entityExecutionOrder to control how it is combined with the entity interceptor of the same command and type:
entityExecutionOrder |
Behavior |
|---|---|
| Not set (default) | The page interceptor runs first, then the entity interceptor |
pageFirst |
Same as the default |
entityFirst |
The entity interceptor runs first, then the page interceptor |
skipEntity |
Only the page interceptor runs; the entity interceptor is skipped |
The order applies to both Pre and Post interceptors. Because the page interceptor runs first by default, it can set a value, such as an owner field, before the entity interceptor validates it. The entity interceptor is the rule shared by every write path, so it runs last unless a page interceptor asks for entityFirst.
A Replace interceptor of the page runs instead of the entity's Replace interceptor. entityExecutionOrder only affects Pre and Post interceptors.
Earlier versions ran page interceptors after the entity interceptor by default. An interceptor saved without
entityExecutionOrdernow runs first; setentityFirston the interceptors that must keep the old order.
{
"name": "my-campaigns",
"type": "dataGrid",
"entityName": "Acme.Campaigns.Campaign",
"interceptors": [
{
"commandName": "Create",
"type": "Pre",
"javascript": "context.commandArgs.setValue('OwnerId', context.currentUser.id);"
},
{
"commandName": "Delete",
"type": "Pre",
"entityExecutionOrder": "entityFirst",
"javascript": "context.log('Deleting ' + context.commandArgs.entityId);"
}
]
}
Examples
Pre-Create: Validation
{
"commandName": "Create",
"type": "Pre",
"javascript": "if(!context.commandArgs.data['Name']) {\n globalError = 'Organization name is required!';\n}"
}
Post-Create: Email Notification
{
"commandName": "Create",
"type": "Post",
"javascript": "if(context.currentUser.isAuthenticated && context.emailSender) {\n await context.emailSender.sendAsync(\n context.currentUser.email,\n 'New Order Created',\n 'Order total: $' + context.commandArgs.data['TotalAmount']\n );\n}"
}
Pre-Update: Role-Based Authorization
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data['IsDelivered']) {\n if(!context.currentUser.roles.includes('admin')) {\n globalError = 'Only administrators can mark orders as delivered!';\n }\n}"
}
Pre-Delete: Business Rule Check
{
"commandName": "Delete",
"type": "Pre",
"javascript": "var project = await db.get('LowCodeDemo.Projects.Project', context.commandArgs.entityId);\nif(project.Budget > 100000) {\n globalError = 'Cannot delete high-budget projects!';\n}"
}
Pre-Update: Negative Value Check
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data['Quantity'] < 0) {\n globalError = 'Quantity cannot be negative!';\n}"
}
Replace-Create: Custom Insert Logic
When you need to completely replace the default create operation with custom logic:
{
"commandName": "Create",
"type": "Replace",
"javascript": "var data = context.commandArgs.data;\ndata['Code'] = 'PRD-' + Date.now();\nvar result = await db.insert('LowCodeDemo.Products.Product', data);\ncontext.log('Product created with custom code: ' + data['Code']);\nreturn result.Id;"
}
Important:
Replace-Createinterceptors must return the new entity'sId(Guid). The system uses this value to fetch and return the created entity. Usereturn result.Id;afterdb.insert(...).
Replace-UpdateandReplace-Deleteinterceptors do not need to return a value.
Pre-Update: Self-Reference Check
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data.ParentCategoryId === context.commandArgs.entityId) {\n globalError = 'A category cannot be its own parent!';\n}"
}