Overview
Typed standing automations ("if X do Y") for AI-agent products — safe for the agent itself to author.
@absolutejs/rulesv0.1.0alphaAIClosed, typed trigger/action vocabulary for standing automations that an AI agent can author without hallucinating behavior.
Typed standing automations ("if X do Y") for AI-agent products, designed so the agent itself can safely author rules on a member’s behalf. You define your triggers and actions once as a closed vocabulary with typed, bounded parameters, and everything derives from that single definition: the validator, the LLM tool schemas, and a capped, cooldown-guarded firing engine. A stored rule can never carry behavior your engine does not implement.
bun add @absolutejs/rulesTyped standing automations ("if X do Y") for AI-agent products — safe for the agent itself to author.
Used by the hosted AbsoluteJS.ai platform and available as a standalone rules package.
Letting an LLM create automations on a member's behalf is only safe if the rule language is closed. This package makes the vocabulary the contract: you define your triggers and actions once, with typed, bounded parameters, and everything derives from that single definition —
The validator (validateRuleInput): unknown triggers/actions reject with
the available options spelled out (an error the LLM can relay verbatim), unknown params strip, numbers clamp to their bounds, closed-set strings narrow. A stored rule can never carry behavior your engine doesn't implement.
The AI tool schemas (ruleToolSchemas): create/update tool inputs whose
trigger/action fields are enums of your vocabulary — the hallucination-proofing.
The firing engine (createRuleEngine): per-entity cooldown via a firing
ledger, daily firing + auto-execution caps, a kill switch, and your authoring policy re-checked at fire time (a rule authored under a looser policy can't outrun a tightened one).
The only free text a rule carries is guidance — a bounded style note your drafting pipeline applies to generated copy. It never selects behavior.
Storage is pluggable via the small RuleStore interface (list enabled rules, ledger reads/writes). createMemoryRuleStore ships for tests; a drizzle/ Postgres store is a few lines against your own tables (see the onSpark reference integration).
defineRuleVocabulary declares triggers and actions with typed, bounded parameters once; validator, tool schemas, and engine all derive from it.
validateRuleInput rejects unknown triggers and actions with the available options spelled out (an error the LLM can relay verbatim), strips unknown params, clamps numbers to bounds, and narrows closed-set strings.
ruleToolSchemas emits create/update tool inputs whose trigger and action fields are enums of your vocabulary, so the agent cannot invent behavior.
createRuleEngine fires occurrences with per-entity cooldowns via a firing ledger, daily firing and auto-execution caps, a kill switch, and your authoring policy re-checked at fire time.
Storage plugs in through the small RuleStore interface; createMemoryRuleStore ships for tests, and a Drizzle or Postgres store is a few lines against your own tables.
Outcomes
Typed standing automations ("if X do Y") for AI-agent products — safe for the agent itself to author.
Letting an LLM create automations on a member's behalf is only safe if the rule language is closed. This package makes the vocabulary the contract: you define your triggers and actions once, with typed, bounded parameters, and everything derives from that single definition —
Storage is pluggable via the small RuleStore interface (list enabled rules, ledger reads/writes). createMemoryRuleStore ships for tests; a drizzle/ Postgres store is a few lines against your own tables (see the onSpark reference integration).
Hardening checklist
Follow in order
Working example for Quick start.
import {
createMemoryRuleStore,
createRuleEngine,
defineRuleVocabulary,
ruleToolSchemas,
validateRuleInput
} from '@absolutejs/rules';
const vocabulary = defineRuleVocabulary({
triggers: {
no_reply: {
label: 'My outreach gets no reply',
paramsHelp: 'days (default 4)',
params: {
days: { type: 'number', min: 1, max: 30, defaultValue: 4 }
}
}
},
actions: {
draft_followup: {
label: 'Draft a follow-up for my approval',
paramsHelp: 'none (guidance styles the copy)',
capability: 'outbound'
}
}
});
// 1. Validate anything that wants to become a rule (AI tool, REST, forms):
const result = validateRuleInput(
vocabulary,
{
trigger: 'no_reply',
action: 'draft_followup',
triggerParams: { days: 45 }
},
{
canUseAction: (action) =>
memberTier !== 'restricted' ||
'Outbound rules need a higher score.',
canAutoSend: () =>
memberTier === 'trusted' || 'Auto-send needs the trusted tier.'
}
);
// result.ok.triggerParams.days === 30 (clamped)
// 2. Give your agent the tools (schemas only — you own the handlers):
const { createInput, updateInput, help } = ruleToolSchemas(vocabulary);
// 3. Fire occurrences from your signal hooks / sweeps:
const engine = createRuleEngine({
vocabulary,
store, // your RuleStore (drizzle, memory, …)
executeAction: async (rule, event, { autoSend }) => {
// queue a draft for approval, create a task, auto-execute…
return autoSend ? 'executed' : 'drafted';
}
});
await engine.fire(
ownerId,
{
trigger: 'no_reply',
entityId: `noreply:${matchId}`,
context: 'no reply from Brendan in 5 days',
signal: { days: 5 }
},
{
killSwitch: false,
cooldownDays: 3,
maxFiringsPerDay: 10,
maxAutoPerDay: 3,
canUseAction: () => true,
canAutoSend: () => true
}
);Define the vocabulary once; validation, agent tool schemas, and the guarded firing engine all derive from it.
import {
createRuleEngine,
defineRuleVocabulary,
ruleToolSchemas,
validateRuleInput
} from '@absolutejs/rules';
const vocabulary = defineRuleVocabulary({
triggers: {
no_reply: {
label: 'My outreach gets no reply',
paramsHelp: 'days (default 4)',
params: {
days: { type: 'number', min: 1, max: 30, defaultValue: 4 }
}
}
},
actions: {
draft_followup: {
label: 'Draft a follow-up for my approval',
paramsHelp: 'none (guidance styles the copy)',
capability: 'outbound'
}
}
});
// 1. Validate anything that wants to become a rule (AI tool, REST, forms):
const result = validateRuleInput(
vocabulary,
{
trigger: 'no_reply',
action: 'draft_followup',
triggerParams: { days: 45 }
},
{
canUseAction: () => true,
canAutoSend: () => 'Auto-send needs the trusted tier.'
}
);
// result.ok.triggerParams.days === 30 (clamped)
// 2. Give your agent the tools (schemas only; you own the handlers):
const { createInput, updateInput, help } = ruleToolSchemas(vocabulary);
// 3. Fire occurrences from your signal hooks / sweeps:
const engine = createRuleEngine({
vocabulary,
store, // your RuleStore (drizzle, memory, ...)
executeAction: async (rule, event, { autoSend }) =>
autoSend ? 'executed' : 'drafted'
});
await engine.fire(
ownerId,
{
trigger: 'no_reply',
entityId: `noreply:${matchId}`,
context: 'no reply from Brendan in 5 days',
signal: { days: 5 }
},
{
killSwitch: false,
cooldownDays: 3,
maxFiringsPerDay: 10,
maxAutoPerDay: 3,
canUseAction: () => true,
canAutoSend: () => true
}
);Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
Current package surface
Import surface · click to copy