Skip to main content
Most rules people ask for sound like missing features and are actually one missing fact. “Hold any payment to a buyer whose account is less than 30 days old” reads like it needs a buyer model, a join, and a release. It needs a number, computed by the service that already knows it, sent on the request.

Before you start

Identifiers
The amounts here assume the agent’s starter policy is disabled, as the quickstart does in step 2. Leave it on and it holds anything over $100 for a person before your own rules get a say.

What a rule can read

A decision is a pure function of the request. Kordio never calls out to your service while it decides, so a rule reads seven facts Kordio holds (action_type, resource, cost_cents, currency, agent_id, agent_mode, budget_id) and any metadata.* dot path you attached. metadata is the extension point. Anything your side can work out before it asks for authorization, a rule can decide on.

1. Send the fact you already have

Your service knows when the buyer signed up. Turn that into a number before the call:
TypeScript
201 Created
Nothing reads account_age_days yet. The fact is on the request and no rule mentions it.

2. Compare it in one predicate

Now the feature request becomes a rule. Hold a payment when the buyer is new, and hold it just as firmly when nobody said how old the buyer is:
201 Created
The same payment, from a buyer who signed up twelve days ago, now waits for a person:
202 Accepted

What to watch when you send facts

A missing fact never raises. It resolves to nil and the predicate is false, so the rule quietly does not fire. That is why the second rule above exists: exists is the only operator that tells “absent” apart from “false”. Ordered comparisons are numeric. gt, gte, lt and lte accept a number or a string that parses as one, so 29 and "29" both work. Anything else compares as false rather than erroring, which means a typo in a path fails silently. Dot paths go as deep as your object. metadata.buyer.account_age_days reads a nested object, so you can send the shape your service already produces. Facts can be compared to each other. A predicate can take a value_field instead of a value and compare two things on the same request, which is how a refund gets capped at the order it belongs to. See stop an agent over-refunding.
Send only what a rule needs to read. metadata is stored on the action intent, returned by the API, and visible to anyone with read on the workspace, so it is the wrong place for card numbers, tokens, or anything you would not put in an audit log.

Metadata decides, decision_context records

On POST /control/v1/agent/payment_intents the two fields look interchangeable and are not. decision_context is provenance: it is stored on the payment intent and never evaluated. metadata is evaluated and, on the payment path, not stored. So a payment whose approval hinged on account_age_days should carry the number in both: metadata so the rule can hold it, decision_context so the record says why.

Next steps

Require a person above a threshold

What happens to the payment this rule just held.

Stop an agent over-refunding

Compare one field on the request to another.

Writing a policy

Every operator, and what each one does to a missing field.

How a decision is reached

Why the first denial wins, and where approvals sit in the order.