Guardrails for agents
Put VAKYN between an agent and the APIs it calls. Before a call goes out, ask whether to allow it, hold it for a person or block it.
When to use it
- An agent can take actions with real effects: refunds, emails to customers, changes to accounts or data.
- Some of those actions are hard to undo, and you want a person to see them first.
- You want a check on every call, not only on the ones you already suspect.
How it works
When the agent proposes a call, send the call, the agent's job and the context as the state. Ask a choice for the action (allow, hold for a person, block) and ask the facts that decide it as their own questions: can it be undone, how much damage could a mistake do, is it the agent's job. When every answer is sure, your code does what the action says: run the call or block it. Otherwise the call waits for a person.
Agent
Proposes a call
- POST /payments/refunds
Check
State: call + job + context
- actionchoice
- irreversiblenoul
- damagescore
- within_jobnoul
every answer sure
Allow runs the call; block stops it.
any answer unsure
The call waits for a person.
The example below runs this check on two EUR 900 refunds from a returns agent, with real answers. Step by step: How VAKYN works.
Clear case
Acts automatically{ "agent": { "name": "Returns assistant at Orla Cycles", "job": "Handles returns. May refund returned items to the original payment method, up to EUR 1,000 per order, once the warehouse has checked the return in." }, "call": { "api": "POST /payments/refunds", "body": { "order": "OC-58213", "amount": 900, "currency": "EUR", "to": "original payment method", "reason": "Returned, unused" } }, "context": { "order": "OC-58213, one e-bike battery, paid EUR 900.00 by card on 2026-09-28", "return": "Checked in by the warehouse on 2026-10-09: sealed, unused, serial number matches the order", "earlier_refunds_on_order": 0, "customer": "Customer since 2022, 11 orders, no earlier refunds, same card on file since 2022" }}
Question Answer Confidence action allow 1.00 irreversible yes 0.14 0.86 damage 0.21 of 3 0.79 within_job yes 0.99 0.99 Every answer is at least 0.70 sure (lowest: damage 0.79), so your code acts on it.
Borderline case
Goes to a person{ "agent": { "name": "Returns assistant at Orla Cycles", "job": "Handles returns. May refund returned items to the original payment method, up to EUR 1,000 per order, once the warehouse has checked the return in." }, "call": { "api": "POST /payments/refunds", "body": { "order": "OC-60477", "amount": 900, "currency": "EUR", "to": "original payment method", "reason": "Parcel never arrived" } }, "context": { "order": "OC-60477, one e-bike battery, paid EUR 900.00 by card on 2026-10-02", "return": "None: the customer says the parcel never arrived", "carrier": "Tracking says delivered to a parcel locker on 2026-10-05", "earlier_refunds_on_order": 0, "customer": "First order, account opened 2026-10-01" }}
Question Answer Confidence action block 0.93 irreversible yes 0.29 0.71 damage 0.77 of 3 0.23 within_job yes 0.42 0.58 damage is only 0.23 sure, under 0.70, so a person decides.
curl -s "https://api.vakyn.com/v1/systemone" \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -o answer.json \ -d @- <<'JSON'{ "model": "jev-latest", "state": { "agent": { "name": "Returns assistant at Orla Cycles", "job": "Handles returns. May refund returned items to the original payment method, up to EUR 1,000 per order, once the warehouse has checked the return in." }, "call": { "api": "POST /payments/refunds", "body": { "order": "OC-58213", "amount": 900, "currency": "EUR", "to": "original payment method", "reason": "Returned, unused" } }, "context": { "order": "OC-58213, one e-bike battery, paid EUR 900.00 by card on 2026-09-28", "return": "Checked in by the warehouse on 2026-10-09: sealed, unused, serial number matches the order", "earlier_refunds_on_order": 0, "customer": "Customer since 2022, 11 orders, no earlier refunds, same card on file since 2022" } }, "questions": { "action": { "type": "choice", "instructions": "What should the guardrail do with this API call?", "criteria": { "allow": "Let the agent make the call now", "ask_human": "Hold the call until a person approves it", "block": "Stop the call and tell the agent why" } }, "irreversible": { "type": "noul", "instructions": "Would this call be hard or impossible to undo once made?" }, "damage": { "type": "score", "instructions": "If this call is a mistake, how hard is the damage to put right?", "criteria": [ "Easy", "Takes some work", "Hard", "Impossible" ] }, "within_job": { "type": "noul", "instructions": "Is this call part of the agent's job as described?" } }}JSON # Act when every answer is at least 0.70 sure; otherwise a person approves the call.jq -r '.answers as $a | $a | map_values(if .type == "noul" then [.noul, 1 - .noul] | max else .confidence end) | to_entries | min_by(.value) as $weakest | if $weakest.value >= 0.70 then "act: \($a.action.choice) (damage \($a.damage.score) of 3)" else "human: \($weakest.key) is only \($weakest.value * 100 | round / 100) sure" end' answer.jsonThe borderline case runs the same code with its own state. damage is only 0.23 sure, under 0.70, so it prints a human line and a person decides. Change the threshold to match what a wrong answer costs you.
Tips
- Write the agent's job into the state in plain words: what it is for, what it may do and its limits. The question "is it the agent's job" is only as good as that description.
- Ask the deciding facts as separate questions instead of one "is this safe?". When a call is held, the person sees why, and you can tune the rule per fact.
- Log every check with its answers. It is a record of what the agent tried to do and why it was allowed.
- The same rule as routing by confidence, applied to actions instead of messages.