Custom commands let a rule call application code using RUN.
Use a custom command when the rule needs to trigger behaviour that does not fit naturally into a pure expression or assignment. Typical cases include sending a notification, calling a service, writing to an audit log, or performing a side effect after the rule has matched.
commandRegistry().keyword that rules will use with RUN.RUN keyword { ... }.immediate or deferred.The RUN syntax looks like this:
if person.age >= 18 then RUN notify_customer { name: Person.name, age: Person.age }
Argument names in the rule must match the names declared on the command.
An immediate command runs during normal rule processing. Its result is available right away, triggering forward-chaining in the rule engine.
import { Workspace } from '@samatawy/rules';
const space = new Workspace({ strict_inputs: false, strict_outputs: false });
space.typeRegistry().addRootType({
key: 'Person',
properties: {
name: 'string',
age: 'number'
}
});
space.commandRegistry().register({
keyword: 'score_bonus',
name: 'Score Bonus',
immediate: true,
arguments: {
age: 'number'
},
execute: (params) => {
return params.age >= 65 ? 20 : 5;
}
});
space.addRule('IF Person.age > 0 THEN RUN score_bonus { age: Person.age }');
space.addRule('IF score_bonus >= 20 THEN Person.priority = "gold" ELSE Person.priority = "standard"');
const ctx = space.loadContext({
Person: {
name: 'Alice',
age: 72
}
});
space.process(ctx);
console.log(ctx.getOutput().score_bonus); // 20
console.log(ctx.getOutput().Person.priority); // gold
In this example, the command result is written to score_bonus, which makes it available to the next rule immediately.
A deferred command is queued during rule processing and runs later through the command handler. Use this when the command represents a side effect that should happen after the engine finishes evaluating rules.
import { Workspace } from '@samatawy/rules';
const sentMessages: string[] = [];
const space = new Workspace({ strict_inputs: false, strict_outputs: false });
space.typeRegistry().addRootType({
key: 'Person',
properties: {
name: 'string',
age: 'number'
}
});
space.commandRegistry().register({
keyword: 'send_welcome_email',
name: 'Send Welcome Email',
immediate: false,
arguments: {
name: 'string'
},
execute: (params) => {
sentMessages.push(`Welcome ${params.name}`);
return true;
}
});
space.addRule('IF Person.age >= 18 THEN RUN send_welcome_email { name: Person.name }');
const ctx = space.loadContext({
Person: {
name: 'Alice',
age: 30
}
});
space.process(ctx);
console.log(sentMessages.length); // 0
await ctx.commandHandler().executeDeferred();
console.log(sentMessages); // ["Welcome Alice"]
This pattern keeps rule evaluation separate from side effects.
execute or executeAsync.executeAsync.You can create commands as published classes. But remember that commands must be instantiated before registration.
Parameters may need to be passed to your command constructor (e.g. credentials or settings).
A command class must provide the following:
/**
* A unique name for the command, used for identification and debugging purposes.
* This is not necessarily the same as the keyword used to invoke the command in rules, but it should be descriptive of the command's purpose.
*/
name: string;
/**
* Indicates whether the command should be executed immediately when invoked.
* Immediate commands are executed synchronously and cannot have an asynchronous executeAsync function.
*/
immediate: boolean;
/**
* The keyword used to invoke the command in rules.
* This must be unique across all registered commands, and should ideally be expressive but short.
* No spaces or special characters are allowed in the keyword - only letters, numbers, and underscores.
*/
keyword: string;
/**
* A record of argument names and their expected types for this command.
* This is used for validating command invocations and providing better error messages when arguments are missing or of the wrong type.
* The types should be defined using the AtomicType interface from the rules engine's type system.
*/
arguments: Record<string, AtomicType>;
/**
* The function to execute when the command is invoked.
* This function is called with the arguments specified in the command's arguments property.
* @param args The arguments passed to the command when it is invoked.
* @returns The result of the command execution.
*/
execute?(...args: any[]): any;
/**
* The asynchronous function to execute when the command is invoked.
* This function is called with the arguments specified in the command's arguments property.
* @param args The arguments passed to the command when it is invoked.
* @returns A promise that resolves with the result of the command execution.
*/
executeAsync?(...args: any[]): Promise<any>;