When you publish the rule
A rule that grants nothing, or uses a value the gate cannot enforce, is refused at publish and at agent creation. You find out before the agent exists, not when it first tries to act.
The rules are what let you hand a wallet to an automated actor. An agent’s rulebook is a use of the platform rules engine, the same engine that governs every other entity, with predicates suited to an actor that spends money: trade limits, cooldowns, allocation caps, approval thresholds, operating windows and approved pairs. The rulebook is published on Hedera Consensus Service before the agent is created, and the agent is bound to it.
You build an agent rulebook with the SDK’s Rules.forAgent() builder. It produces the canonical agent rule (type: 'agent'). This one is for an agent that trades XRP on the XRPL DEX and may pay out only to its owner:
import { Rules } from '@hsuite/smart-engines-sdk';
const OWNER = 'rYourOwnerAddress';
export const agentRule = Rules.forAgent() .withName('xrp-trader') .withAgentType('trading') // Authority: DEX offers, and transfers to the owner only. .withActionTypeRestrictions({ allowed: ['offer', 'transfer'] }) .withRecipientAllowlist({ addresses: [OWNER] }) // Amounts are in the chain's smallest unit: drops on XRPL (1 XRP = 1,000,000 drops). .withTradeLimits({ maxPerTrade: '25000000', dailyLimit: '50000000', periodMs: 86_400_000 }) .withCooldown({ cooldownMs: 600_000 }) // 10 minutes between moves .withApprovalThreshold({ threshold: '10000000', controller: 'owner' }) // above 10 XRP, the owner approves .build();The builder defaults the rule to defaultSecurity: 'full' and defaultController: 'owner'.
The predicates an agent rule can use are tradeLimits, cooldown, allocationLimits, approvalThreshold, operationalWindow, approvedPairs, recipientAllowlist, actionTypeRestrictions and permissions, plus the rule-level timeRange, tokenGates and invariants that every rule shares. There is no stopLoss field. You contain losses with trade limits, allocation limits and the daily cap.
A few things to know when you write one:
actionTypeRestrictions.allowed is the coarse authority gate. An agent rule can grant transfer, mint, burn and offer. A DEX trade on XRPL is an OfferCreate, which counts as offer, so a trading agent must list offer, or every trade is denied before any limit is checked.recipientAllowlist does not apply to an offer. It still applies to every transfer. An offer’s spend is bounded by tradeLimits, approvedPairs, cooldown, operationalWindow and approvalThreshold. The amount checked is what the agent gives (the offer’s TakerGets).approvedPairs matches the token the agent gives. An offer that gives XRP has no token id, so it is denied while approvedPairs is set. For an agent that sells XRP, leave approvedPairs out and bound it with tradeLimits.operationalWindow is a bounded period (from and to, as unix seconds or a date string), not a recurring schedule. Daily opening hours cannot be expressed.approvalThreshold names owner or dao as the controller.A value the gate cannot enforce is refused when you publish, not ignored:
permissions.mode other than whitelist, wildcards, self-add, or an entry scope below executecooldown.perAction key that names no agent actiontradeLimits.periodMs other than one daydecimals other than 0approvedPairs.strictMode: falseapprovalThreshold.currencyoperationalWindow.timezone other than UTC, or inclusive: falseallocationLimits.min, or an allocationLimits.max without its tokenIdbaas.rules.simulate reports each of these as a schema error on its own path. A permissions entry’s expiresAt is enforced, and a cooldown.perAction entry overrides cooldownMs for that action. An approvedPairs pair applies on its own chain only, so the same token id on another chain is denied.
If you create an agent without supplying a rule, the platform uses its default agent rule (buildDefaultAgentValidatorRules() in the SDK), and that default denies everything. The agent can move nothing on-chain until you grant it something:
actionTypeRestrictions: { allowed: ['transfer'] }, so mint, burn and offer are denied by action type.recipientAllowlist: { addresses: ['__deny_all__'] }, a placeholder no real account matches, so every transfer is denied too.The schema requires non-empty lists, so “deny all” is written as “allow only a value nothing matches”.
Leaving a gate out withholds authority. It never grants it. An agent rule with no actionTypeRestrictions permits no on-chain operation at all, and one with no recipientAllowlist permits no transfer destination. Simulating such a rule reports actionTypeRestrictions absent — … (default-deny) for every action.
Because a rule like that looks unrestricted while granting nothing, it is refused rather than published. rules.publish, rules.simulate on an inline rule, the SDK’s forAgent().build() and Rules.validate, and agent creation all reject an agent rule that has no actionTypeRestrictions (or an empty one), or that allows transfer without a recipientAllowlist. Only transfer needs the allow-list: { allowed: ['offer'] } or { allowed: ['mint'] } without a recipientAllowlist is accepted, because neither sends value to a recipient.
Trading is one thing an agent can do. An agent declares a list of capabilities when it is created, and every action it takes runs one of them. Six capabilities exist:
| Capability | What it does | Params |
|---|---|---|
trade |
Places a DEX offer from the agent’s wallet (an XRPL OfferCreate) |
{ takerGets, takerPays }, each { currency, value, issuer? } (issuer required for anything but XRP; XRP values in drops) |
payout |
Transfers from the agent’s wallet | { destination, amount, tokenId? } |
token |
Mints, burns or transfers a token | { op, tokenId, amount, to? } |
notify |
Sends an outbound POST | { url, payload? or message?, headers? } |
read |
Sends an outbound GET | { url, headers? } |
function-invoke |
Runs one of your app’s functions as the agent | { appId, functionId, input? } |
Each capability turns into one or more steps:
trade, payout, token) are prepared from the agent’s wallet and checked against the rulebook before any validator signs. They also pick up approval thresholds.notify, read) reach only URLs on the agent’s rulesConfig.offchain.allowedUrls. Any other URL is refused before the request leaves.rulesConfig.functions.allowedFunctionIds, and only in the app the agent belongs to.An agent can only run a capability it declared. An undeclared one is refused before anything is prepared.
A function-invoke step runs one of your app’s functions under the agent’s own identity rather than the app’s, so its usage is attributed to the agent. The function holds no keys. If it returns a decision with a list of intents, each intent is dispatched as one of the agent’s capabilities and checked exactly like a direct one:
// what a function returns to propose actionsreturn { decision: { reason: 'spread above target' }, intents: [ { capability: 'trade', chain: 'xrpl', params: { takerGets: { currency: 'XRP', value: '5000000' }, // 5 XRP, in drops takerPays: { currency: 'USD', issuer: 'rUsdIssuer', value: '2.5' }, }, idempotencyKey: 'trade:2026-12-01T10', }, ],};A function can return at most eight intents. Each intent’s capability must be one the agent declared.
Two daily caps apply per agent: the number of function invocations (default 1,000 a day) and their estimated cost, priced against the app’s tier (default $100 a day). The agent has no tier of its own, so an agent cannot drain the app’s subscription with expensive calls. The cost cap is a cost control, not a security boundary: if a price can’t be computed, the call is allowed.
The rulebook is checked in two places, and both fail closed.
When you publish the rule
A rule that grants nothing, or uses a value the gate cannot enforce, is refused at publish and at agent creation. You find out before the agent exists, not when it first tries to act.
Before any signature
Every on-chain move the agent proposes is prepared as a transaction and checked against the bound rulebook before any validator signs. If the check denies it, no signature is produced.
sequenceDiagram
participant Agent as Agent (proposer)
participant Host as Platform
participant Gate as Rule check (fail-closed)
participant Val as Validators
Agent->>Host: run a capability (e.g. trade)
Host->>Host: agent active? capability declared?
Host->>Gate: check the prepared transaction
alt allowed
Gate->>Val: sign (threshold of validators)
Val-->>Host: signed transaction
else above the approval threshold
Gate-->>Host: parked for the owner
Host-->>Agent: awaiting approval
else any other denial
Gate-->>Host: denied, nothing signed
Host-->>Agent: refused
end
A denial for the approval threshold is not a rejection. The operation is stored as pending, the agent moves to awaiting_approval, and the owner calls approve or reject. Any other denial is final.
If the rule check cannot run, the operation is denied rather than let through. The same holds for transaction preparation and for storing a pending approval: a missing dependency means no operation, never an unchecked operation.
What you can inspect today:
ruleRef (where it was published) and rulesHash. Anyone can check which rule governs the agent.baas.agents.getEvents(agentId) returns the agent’s lifecycle record, newest first: its creation, every status change (including an operation parked for approval), rule changes, and the operations you approved or rejected.Anchoring each agent’s state history on Hedera Consensus Service is built, not yet enabled.
baas.agents.certification(agentId) (GET /baas/agents/:agentId/certification) returns the agent’s certification snapshot in one read:
entityId, rulesHash and ruleRefThe snapshot holds addresses and the rules hash, so it is not public. It is scoped like every other agent read: you see an agent in your own tenant that you own. When a Smart App acts for a customer, it sees only that customer’s agents; when it acts as itself, only the agents it owns. An agent outside your scope returns the same 404 as an id that doesn’t exist. See Identity & wallets.
baas.agents.updateRules(agentId, rulesConfig) replaces the agent’s rules config: its trade market, its off-chain and function allow-lists, and its triggers. It works only while the agent is paused, which forces a stop-edit-resume cycle instead of a hot swap. The change is recorded in the event log. Event-trigger subjects are checked again on every update.
updateRules does not change the rulebook the agent was created with. The limits and authority gates on this page stay bound to the agent’s ruleRef.