Rules
The building blocks that make decisions
Rules
Rules are the building blocks that make decisions. They evaluate input and return allow(), deny(), or skip() with optional metadata.
Basic Rule
A simple rule checks a condition and returns a result:
import { defineRule, allow, deny } from '@bantai-dev/core';
const adminRule = defineRule(
appContext,
'check-admin',
async (input) => {
if (input.role === 'admin') {
return allow({ reason: 'User is admin' });
}
return deny({ reason: 'Admin access required' });
}
);Rules with Hooks
Rules can have hooks that execute on allow or deny:
const adminRule = defineRule(
appContext,
'check-admin',
async (input) => {
if (input.role === 'admin') {
return allow({ reason: 'User is admin' });
}
return deny({ reason: 'Admin access required' });
},
{
onAllow: async (input, { tools }) => {
console.log(`Admin access granted to ${input.userId}`);
},
onDeny: async (input, { tools }) => {
console.log(`Admin access denied to ${input.userId}`);
},
}
);Hooks are useful for:
- Logging access attempts
- Updating counters
- Sending notifications
- Performing side effects
Async Rules
Rules can be asynchronous and perform async operations:
const asyncRule = defineRule(
appContext,
'check-database',
async (input, { tools }) => {
const user = await tools.database.getUser(input.userId);
if (user?.active) {
return allow({ reason: 'User is active' });
}
return deny({ reason: 'User is not active' });
}
);Using Tools in Rules
Rules can access tools from the context:
const rule = defineRule(
contextWithStorage,
'check-cache',
async (input, { tools }) => {
const cached = await tools.storage.get(`user:${input.userId}`);
if (cached) {
return allow({ reason: 'Found in cache' });
}
return deny({ reason: 'Not in cache' });
}
);Rule Result
Rules must return a RuleResult using allow(), deny(), or skip():
// Allow with optional reason and metadata
return allow({ reason: 'User is valid' });
return allow(); // Reason defaults to null
return allow({
reason: 'User is valid',
meta: { userId: '123', role: 'admin' }
});
// Deny with optional reason and metadata
return deny({ reason: 'User is invalid' });
return deny(); // Reason defaults to null
return deny({
reason: 'User is invalid',
meta: { errorCode: 'INVALID_USER', attempts: 3 }
});
// Skip with optional reason and metadata
return skip({ reason: 'Rule not applicable' });
return skip(); // Reason defaults to 'skipped'
return skip({
reason: 'Rule not applicable',
meta: { condition: 'country_not_provided' }
});The reason is useful for:
- Debugging
- User feedback
- Logging
- Understanding why a rule passed or failed
The meta field is useful for:
- Storing additional context about the decision
- Passing structured data to audit logs
- Including error codes or diagnostic information
- Tracking rule-specific metadata
Best Practices
- Keep Rules Focused: Each rule should check one specific thing
- Provide Clear Reasons: Help with debugging and user feedback
- Use Tools for Side Effects: Keep rule logic pure, use hooks for side effects
- Handle Errors Gracefully: Return deny with a reason if something goes wrong
Related Concepts
- Context - Rules are defined within a context
- Policies - Rules are combined into policies
- Tools Integration - Extend rules with tools